diff --git a/OutOfSchool/OutOfSchool.Common/OutOfSchool.Common.csproj b/OutOfSchool/OutOfSchool.Common/OutOfSchool.Common.csproj
index 1e36afcdae..30eba535d2 100644
--- a/OutOfSchool/OutOfSchool.Common/OutOfSchool.Common.csproj
+++ b/OutOfSchool/OutOfSchool.Common/OutOfSchool.Common.csproj
@@ -16,7 +16,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/ImageStorageException.cs b/OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/FileStorageException.cs
similarity index 60%
rename from OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/ImageStorageException.cs
rename to OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/FileStorageException.cs
index ad91f7eb83..09fa2373a8 100644
--- a/OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/ImageStorageException.cs
+++ b/OutOfSchool/OutOfSchool.DataAccess/Common/Exceptions/FileStorageException.cs
@@ -8,29 +8,28 @@ namespace OutOfSchool.Services.Common.Exceptions
/// to work with image storage.
///
[Serializable]
- public class ImageStorageException : Exception
+ public class FileStorageException : Exception
{
-
- public ImageStorageException()
+ public FileStorageException()
{
}
- public ImageStorageException(Exception ex)
+ public FileStorageException(Exception ex)
: this("Unhandled exception", ex)
{
}
- public ImageStorageException(string message)
+ public FileStorageException(string message)
: base(message)
{
}
- public ImageStorageException(string message, Exception innerException)
+ public FileStorageException(string message, Exception innerException)
: base(message, innerException)
{
}
- protected ImageStorageException(SerializationInfo info, StreamingContext context)
+ protected FileStorageException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageImagesSourceConfig.cs b/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageImagesSourceConfig.cs
new file mode 100644
index 0000000000..3d9eaa49ea
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageImagesSourceConfig.cs
@@ -0,0 +1,9 @@
+namespace OutOfSchool.Services.Contexts.Configuration
+{
+ ///
+ /// Contains a configuration that is essential for operations with images in Google Cloud Storage.
+ ///
+ public class GcpStorageImagesSourceConfig : GcpStorageSourceConfig
+ {
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageSourceConfig.cs b/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageSourceConfig.cs
new file mode 100644
index 0000000000..64a27db37d
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Contexts/Configuration/GcpStorageSourceConfig.cs
@@ -0,0 +1,18 @@
+namespace OutOfSchool.Services.Contexts.Configuration
+{
+ ///
+ /// Contains a configuration that is essential for Google Cloud Storage.
+ ///
+ public abstract class GcpStorageSourceConfig
+ {
+ ///
+ /// Gets or sets a file of Google credential.
+ ///
+ public string CredentialFilePath { get; set; } // Should be set in environment variables
+
+ ///
+ /// Gets or sets a bucket name of Google Storage.
+ ///
+ public string BucketName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Contexts/GcpStorageContext.cs b/OutOfSchool/OutOfSchool.DataAccess/Contexts/GcpStorageContext.cs
new file mode 100644
index 0000000000..221db949d1
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Contexts/GcpStorageContext.cs
@@ -0,0 +1,18 @@
+using System;
+using Google.Cloud.Storage.V1;
+
+namespace OutOfSchool.Services.Contexts
+{
+ public class GcpStorageContext : IGcpStorageContext
+ {
+ public GcpStorageContext(StorageClient client, string bucketName)
+ {
+ StorageClient = client ?? throw new ArgumentNullException(nameof(client));
+ BucketName = bucketName ?? throw new ArgumentNullException(nameof(bucketName));
+ }
+
+ public StorageClient StorageClient { get; }
+
+ public string BucketName { get; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Contexts/IGcpStorageContext.cs b/OutOfSchool/OutOfSchool.DataAccess/Contexts/IGcpStorageContext.cs
new file mode 100644
index 0000000000..fda76ce33d
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Contexts/IGcpStorageContext.cs
@@ -0,0 +1,11 @@
+using Google.Cloud.Storage.V1;
+
+namespace OutOfSchool.Services.Contexts
+{
+ public interface IGcpStorageContext
+ {
+ StorageClient StorageClient { get; }
+
+ string BucketName { get; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Extensions/GcpExtension.cs b/OutOfSchool/OutOfSchool.DataAccess/Extensions/GcpExtension.cs
new file mode 100644
index 0000000000..80f54ff6c6
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Extensions/GcpExtension.cs
@@ -0,0 +1,18 @@
+using Google.Apis.Auth.OAuth2;
+using OutOfSchool.Services.Contexts.Configuration;
+
+namespace OutOfSchool.Services.Extensions
+{
+ public static class GcpExtension
+ {
+ public static GoogleCredential RetrieveGoogleCredential(this GcpStorageSourceConfig config)
+ {
+ if (string.IsNullOrEmpty(config.CredentialFilePath))
+ {
+ return GoogleCredential.GetApplicationDefault();
+ }
+
+ return GoogleCredential.FromFile(config.CredentialFilePath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Models/FileModel.cs b/OutOfSchool/OutOfSchool.DataAccess/Models/FileModel.cs
new file mode 100644
index 0000000000..1481e89aaa
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Models/FileModel.cs
@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace OutOfSchool.Services.Models
+{
+ public class FileModel
+ {
+ public Stream ContentStream { get; set; }
+
+ public string ContentType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Models/Images/ExternalImageModel.cs b/OutOfSchool/OutOfSchool.DataAccess/Models/Images/ImageFileModel.cs
similarity index 52%
rename from OutOfSchool/OutOfSchool.DataAccess/Models/Images/ExternalImageModel.cs
rename to OutOfSchool/OutOfSchool.DataAccess/Models/Images/ImageFileModel.cs
index 6a525e7786..5e4044628b 100644
--- a/OutOfSchool/OutOfSchool.DataAccess/Models/Images/ExternalImageModel.cs
+++ b/OutOfSchool/OutOfSchool.DataAccess/Models/Images/ImageFileModel.cs
@@ -5,10 +5,7 @@
namespace OutOfSchool.Services.Models.Images
{
- public class ExternalImageModel
+ public class ImageFileModel : FileModel
{
- public Stream ContentStream { get; set; }
-
- public string ContentType { get; set; }
}
}
diff --git a/OutOfSchool/OutOfSchool.DataAccess/OutOfSchool.DataAccess.csproj b/OutOfSchool/OutOfSchool.DataAccess/OutOfSchool.DataAccess.csproj
index 9074c73e43..821b86060f 100644
--- a/OutOfSchool/OutOfSchool.DataAccess/OutOfSchool.DataAccess.csproj
+++ b/OutOfSchool/OutOfSchool.DataAccess/OutOfSchool.DataAccess.csproj
@@ -23,6 +23,7 @@
+
@@ -32,7 +33,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/ExternalImageStorage.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/ExternalImageStorage.cs
index 82d0d75e5f..267686e83f 100644
--- a/OutOfSchool/OutOfSchool.DataAccess/Repository/ExternalImageStorage.cs
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/ExternalImageStorage.cs
@@ -27,7 +27,7 @@ public ExternalImageStorage(MongoDb db)
gridFsBucket = db.GetContext();
}
- public async Task GetByIdAsync(string imageId)
+ public async Task GetByIdAsync(string imageId)
{
_ = imageId ?? throw new ArgumentNullException(nameof(imageId));
try
@@ -35,31 +35,31 @@ public async Task GetByIdAsync(string imageId)
var result = await gridFsBucket.OpenDownloadStreamAsync(new ObjectId(imageId))
?? throw new InvalidOperationException($"Unreal to get non-nullable {nameof(GridFSDownloadStream)} instance."); // think about searching by file name
var contentType = result.FileInfo.Metadata[ContentType].AsString;
- return new ExternalImageModel { ContentStream = result, ContentType = contentType };
+ return new ImageFileModel { ContentStream = result, ContentType = contentType };
}
catch (Exception ex)
{
- throw new ImageStorageException(ex);
+ throw new FileStorageException(ex);
}
}
- public async Task UploadImageAsync(ExternalImageModel imageModel, CancellationToken cancellationToken = default)
+ public async Task UploadImageAsync(ImageFileModel imageFileModel, CancellationToken cancellationToken = default)
{
- _ = imageModel ?? throw new ArgumentNullException(nameof(imageModel));
+ _ = imageFileModel ?? throw new ArgumentNullException(nameof(imageFileModel));
try
{
- imageModel.ContentStream.Position = uint.MinValue;
+ imageFileModel.ContentStream.Position = uint.MinValue;
var options = new GridFSUploadOptions
{
- Metadata = new BsonDocument(ContentType, imageModel.ContentType),
+ Metadata = new BsonDocument(ContentType, imageFileModel.ContentType),
};
- var objectId = await gridFsBucket.UploadFromStreamAsync(Guid.NewGuid().ToString(), imageModel.ContentStream, options, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var objectId = await gridFsBucket.UploadFromStreamAsync(Guid.NewGuid().ToString(), imageFileModel.ContentStream, options, cancellationToken: cancellationToken).ConfigureAwait(false);
return objectId.ToString();
}
catch (Exception ex)
{
- throw new ImageStorageException(ex);
+ throw new FileStorageException(ex);
}
}
@@ -72,7 +72,7 @@ public async Task DeleteImageAsync(string imageId, CancellationToken cancellatio
}
catch (Exception ex)
{
- throw new ImageStorageException(ex);
+ throw new FileStorageException(ex);
}
}
}
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpFilesStorageBase.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpFilesStorageBase.cs
new file mode 100644
index 0000000000..511a10b0eb
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpFilesStorageBase.cs
@@ -0,0 +1,101 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Apis.Auth.OAuth2;
+using Google.Cloud.Storage.V1;
+using OutOfSchool.Services.Common.Exceptions;
+using OutOfSchool.Services.Contexts;
+using OutOfSchool.Services.Contexts.Configuration;
+using OutOfSchool.Services.Extensions;
+using OutOfSchool.Services.Models;
+
+namespace OutOfSchool.Services.Repository.Files
+{
+ public abstract class GcpFilesStorageBase : IFilesStorage
+ where TFile : FileModel, new()
+ {
+ protected GcpFilesStorageBase(IGcpStorageContext storageContext)
+ {
+ StorageClient = storageContext.StorageClient;
+ BucketName = storageContext.BucketName;
+ }
+
+ private protected StorageClient StorageClient { get; }
+
+ private protected string BucketName { get; }
+
+ ///
+ public virtual async Task GetByIdAsync(string fileId, CancellationToken cancellationToken = default)
+ {
+ _ = fileId ?? throw new ArgumentNullException(nameof(fileId));
+ try
+ {
+ var fileObject = await StorageClient.GetObjectAsync(
+ BucketName,
+ fileId,
+ cancellationToken: cancellationToken);
+
+ var fileStream = new MemoryStream();
+ await StorageClient.DownloadObjectAsync(
+ fileObject,
+ fileStream,
+ cancellationToken: cancellationToken);
+
+ fileStream.Position = 0;
+ return new TFile { ContentStream = fileStream, ContentType = fileObject.ContentType };
+ }
+ catch (Exception ex)
+ {
+ throw new FileStorageException(ex);
+ }
+ }
+
+ ///
+ public virtual async Task UploadAsync(TFile file, CancellationToken cancellationToken = default)
+ {
+ _ = file ?? throw new ArgumentNullException(nameof(file));
+
+ try
+ {
+ var fileId = GenerateFileId();
+ var dataObject = await StorageClient.UploadObjectAsync(
+ BucketName,
+ fileId,
+ file.ContentType,
+ file.ContentStream,
+ cancellationToken: cancellationToken);
+ return dataObject.Name;
+ }
+ catch (Exception ex)
+ {
+ throw new FileStorageException(ex);
+ }
+ }
+
+ ///
+ public virtual async Task DeleteAsync(string fileId, CancellationToken cancellationToken = default)
+ {
+ _ = fileId ?? throw new ArgumentNullException(nameof(fileId));
+ try
+ {
+ await StorageClient.DeleteObjectAsync(BucketName, fileId, cancellationToken: cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ throw new FileStorageException(ex);
+ }
+ }
+
+ ///
+ /// This method generates a unique value that is used as file identifier.
+ ///
+ ///
+ /// The result contains a string value of the file if it's uploaded.
+ ///
+ protected virtual string GenerateFileId()
+ {
+ return Guid.NewGuid().ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpImagesStorage.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpImagesStorage.cs
new file mode 100644
index 0000000000..4bcebddac8
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/GcpImagesStorage.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using OutOfSchool.Services.Contexts;
+using OutOfSchool.Services.Contexts.Configuration;
+using OutOfSchool.Services.Models.Images;
+
+namespace OutOfSchool.Services.Repository.Files
+{
+ public class GcpImagesStorage : GcpFilesStorageBase, IImageFilesStorage
+ {
+ public GcpImagesStorage(IGcpStorageContext storageContext)
+ : base(storageContext)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IFilesStorage.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IFilesStorage.cs
new file mode 100644
index 0000000000..bd1cca110d
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IFilesStorage.cs
@@ -0,0 +1,39 @@
+using System.Threading;
+using System.Threading.Tasks;
+using OutOfSchool.Services.Models;
+using OutOfSchool.Services.Models.Images;
+
+namespace OutOfSchool.Services.Repository.Files
+{
+ public interface IFilesStorage
+ where TFile : FileModel
+ {
+ ///
+ /// Asynchronously gets a file by its id.
+ ///
+ /// File id.
+ /// CancellationToken.
+ /// A representing the result of the asynchronous operation.
+ /// The task result contains a file of type .
+ ///
+ Task GetByIdAsync(TIdentifier fileId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Asynchronously uploads a file into the storage.
+ ///
+ /// File.
+ /// CancellationToken.
+ /// A representing the result of the asynchronous operation.
+ /// The task result contains an identifier of the file if it's uploaded.
+ ///
+ Task UploadAsync(TFile file, CancellationToken cancellationToken = default);
+
+ ///
+ /// Asynchronously deletes a file from the storage.
+ ///
+ /// File id.
+ /// CancellationToken.
+ /// A representing the result of the asynchronous operation.
+ Task DeleteAsync(TIdentifier fileId, CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IImageFilesStorage.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IImageFilesStorage.cs
new file mode 100644
index 0000000000..9db56f1daf
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/Files/IImageFilesStorage.cs
@@ -0,0 +1,8 @@
+using OutOfSchool.Services.Models.Images;
+
+namespace OutOfSchool.Services.Repository.Files
+{
+ public interface IImageFilesStorage : IFilesStorage
+ {
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.DataAccess/Repository/IExternalImageStorage.cs b/OutOfSchool/OutOfSchool.DataAccess/Repository/IExternalImageStorage.cs
index 63d4413fdf..6f60e57f68 100644
--- a/OutOfSchool/OutOfSchool.DataAccess/Repository/IExternalImageStorage.cs
+++ b/OutOfSchool/OutOfSchool.DataAccess/Repository/IExternalImageStorage.cs
@@ -11,9 +11,9 @@ namespace OutOfSchool.Services.Repository
{
public interface IExternalImageStorage
{
- Task GetByIdAsync(string imageId);
+ Task GetByIdAsync(string imageId);
- Task UploadImageAsync(ExternalImageModel imageModel, CancellationToken cancellationToken = default);
+ Task UploadImageAsync(ImageFileModel imageFileModel, CancellationToken cancellationToken = default);
Task DeleteImageAsync(string imageId, CancellationToken cancellationToken = default);
}
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/OutOfSchool.IdentityServer.csproj b/OutOfSchool/OutOfSchool.IdentityServer/OutOfSchool.IdentityServer.csproj
index c17ac7716c..389e951a75 100644
--- a/OutOfSchool/OutOfSchool.IdentityServer/OutOfSchool.IdentityServer.csproj
+++ b/OutOfSchool/OutOfSchool.IdentityServer/OutOfSchool.IdentityServer.csproj
@@ -27,7 +27,7 @@
-
+
diff --git a/OutOfSchool/OutOfSchool.Redis/OutOfSchool.Redis.csproj b/OutOfSchool/OutOfSchool.Redis/OutOfSchool.Redis.csproj
index 1f45f0b194..69ecb69c0a 100644
--- a/OutOfSchool/OutOfSchool.Redis/OutOfSchool.Redis.csproj
+++ b/OutOfSchool/OutOfSchool.Redis/OutOfSchool.Redis.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/OutOfSchool/OutOfSchool.WebApi.Tests/OutOfSchool.WebApi.Tests.csproj b/OutOfSchool/OutOfSchool.WebApi.Tests/OutOfSchool.WebApi.Tests.csproj
index 895fe3febc..b43f495c04 100644
--- a/OutOfSchool/OutOfSchool.WebApi.Tests/OutOfSchool.WebApi.Tests.csproj
+++ b/OutOfSchool/OutOfSchool.WebApi.Tests/OutOfSchool.WebApi.Tests.csproj
@@ -30,7 +30,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/OutOfSchool/OutOfSchool.WebApi.Tests/Services/Images/ImageServiceTests.cs b/OutOfSchool/OutOfSchool.WebApi.Tests/Services/Images/ImageServiceTests.cs
index bcff87c114..f5208923f2 100644
--- a/OutOfSchool/OutOfSchool.WebApi.Tests/Services/Images/ImageServiceTests.cs
+++ b/OutOfSchool/OutOfSchool.WebApi.Tests/Services/Images/ImageServiceTests.cs
@@ -17,6 +17,7 @@
using OutOfSchool.Services.Common.Exceptions;
using OutOfSchool.Services.Models.Images;
using OutOfSchool.Services.Repository;
+using OutOfSchool.Services.Repository.Files;
using OutOfSchool.WebApi.Common;
using OutOfSchool.WebApi.Common.Resources.Codes;
using OutOfSchool.WebApi.Services.Images;
@@ -28,12 +29,12 @@ internal class ImageServiceTests
{
#region TestData
- private static readonly IReadOnlyList> ExternalImageModelsWithMockedStreamTestDataSource;
+ private static readonly IReadOnlyList> ExternalImageModelsWithMockedStreamTestDataSource;
private static readonly IReadOnlyList> ImageIdsTestDataSource;
#endregion
- private Mock externalStorageMock;
+ private Mock externalStorageMock;
private Mock serviceProviderMock;
private Mock> loggerMock;
private IImageService imageService;
@@ -47,7 +48,7 @@ static ImageServiceTests()
[SetUp]
public void SetUp()
{
- externalStorageMock = new Mock();
+ externalStorageMock = new Mock();
serviceProviderMock = new Mock();
loggerMock = new Mock>();
imageService = new ImageService(
@@ -64,7 +65,7 @@ public async Task GetById_WhenImageWithThisIdExists_ShouldReturnSuccessfulResult
// Arrange
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
var externalImageModel = TakeFirstFromTestData(ExternalImageModelsWithMockedStreamTestDataSource);
- externalStorageMock.Setup(x => x.GetByIdAsync(imageId)).
+ externalStorageMock.Setup(x => x.GetByIdAsync(imageId, CancellationToken.None)).
ReturnsAsync(externalImageModel);
// Act
@@ -81,7 +82,7 @@ public async Task GetById_WhenImageWithThisIdNotExists_ShouldReturnFailedResultO
{
// Arrange
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
- externalStorageMock.Setup(x => x.GetByIdAsync(It.IsAny())).ThrowsAsync(new ImageStorageException());
+ externalStorageMock.Setup(x => x.GetByIdAsync(It.IsAny(), CancellationToken.None)).ThrowsAsync(new FileStorageException());
// Act
var result = await imageService.GetByIdAsync(imageId);
@@ -115,7 +116,7 @@ public async Task
SetUpValidatorWithOperationResult(true);
var queue = new Queue(imageIds);
externalStorageMock
- .Setup(x => x.UploadImageAsync(It.IsAny(), It.IsAny()))
+ .Setup(x => x.UploadAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(queue.Dequeue);
// Act
@@ -159,7 +160,7 @@ public void
{
// Arrange
var images = CreateMockedFormFiles(2);
- serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidatorService))).Returns(null);
+ serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidator))).Returns(null);
// Act & Assert
imageService.Invoking(x => x.UploadManyImagesAsync(images)).Should()
@@ -174,16 +175,16 @@ public async Task
const byte countOfImages = 4, countOfValidImages = 2;
var images = CreateMockedFormFiles(countOfImages);
var imageIds = TakeFromTestData(ImageIdsTestDataSource, countOfValidImages);
- var validator = new Mock>();
+ var validator = new Mock();
validator.SetupSequence(x => x.Validate(It.IsAny()))
.Returns(OperationResult.Success)
.Returns(OperationResult.Failed())
.Returns(OperationResult.Failed())
.Returns(OperationResult.Success);
- serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidatorService))).Returns(validator.Object);
+ serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidator))).Returns(validator.Object);
var queue = new Queue(imageIds);
externalStorageMock
- .Setup(x => x.UploadImageAsync(It.IsAny(), It.IsAny()))
+ .Setup(x => x.UploadAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(queue.Dequeue);
// Act
@@ -205,10 +206,10 @@ public async Task
var imageIds = TakeFromTestData(ImageIdsTestDataSource, countOfUploadedImages);
SetUpValidatorWithOperationResult(true);
externalStorageMock
- .SetupSequence(x => x.UploadImageAsync(It.IsAny(), It.IsAny()))
+ .SetupSequence(x => x.UploadAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(imageIds[0])
- .ThrowsAsync(new ImageStorageException())
- .ThrowsAsync(new ImageStorageException())
+ .ThrowsAsync(new FileStorageException())
+ .ThrowsAsync(new FileStorageException())
.ReturnsAsync(imageIds[1]);
// Act
@@ -229,7 +230,7 @@ public async Task UploadImage_WhenImageIsValid_ShouldReturnSuccessfulResultWithS
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
SetUpValidatorWithOperationResult(true);
externalStorageMock
- .Setup(x => x.UploadImageAsync(It.IsAny(), It.IsAny()))
+ .Setup(x => x.UploadAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(imageId);
// Act
@@ -275,8 +276,8 @@ public async Task
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
SetUpValidatorWithOperationResult(true);
externalStorageMock
- .Setup(x => x.UploadImageAsync(It.IsAny(), It.IsAny()))
- .ThrowsAsync(new ImageStorageException());
+ .Setup(x => x.UploadAsync(It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new FileStorageException());
// Act
var result = await imageService.UploadImageAsync(file);
@@ -291,7 +292,7 @@ public void
{
// Arrange
var image = new Mock().Object;
- serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidatorService))).Returns(null);
+ serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidator))).Returns(null);
// Act & Assert
imageService.Invoking(x => x.UploadImageAsync(image)).Should()
@@ -308,7 +309,7 @@ public async Task
{
// Arrange
externalStorageMock
- .Setup(x => x.DeleteImageAsync(It.IsAny(), It.IsAny()))
+ .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
// Act
@@ -354,10 +355,10 @@ public async Task
const byte countOfImageIds = 3, countOfDeleted = 1;
var imageIds = TakeFromTestData(ImageIdsTestDataSource, countOfImageIds);
externalStorageMock
- .SetupSequence(x => x.DeleteImageAsync(It.IsAny(), It.IsAny()))
+ .SetupSequence(x => x.DeleteAsync(It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask)
- .Throws()
- .Throws();
+ .Throws()
+ .Throws();
// Act
var result = await imageService.RemoveManyImagesAsync(imageIds);
@@ -375,7 +376,7 @@ public async Task RemoveImage_WhenImageIdIsRight_ShouldReturnSuccessfulOperation
// Arrange
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
externalStorageMock
- .Setup(x => x.DeleteImageAsync(It.IsAny(), It.IsAny()))
+ .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
// Act
@@ -414,8 +415,8 @@ public async Task RemoveImage_WhenImageIdWasNotRemoved_BecauseItIsIncorrectOrNot
// Arrange
var imageId = TakeFirstFromTestData(ImageIdsTestDataSource);
externalStorageMock
- .Setup(x => x.DeleteImageAsync(It.IsAny(), It.IsAny()))
- .Throws();
+ .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny()))
+ .Throws();
// Act
var result = await imageService.RemoveImageAsync(imageId);
@@ -494,16 +495,16 @@ private static T TakeFirstFromTestData(IReadOnlyList> testDataSource)
#region Initializators
// Shouldn't make these collections smaller
- private static IReadOnlyList> InitializeExternalImageModelsWithMockedStreamsTestDataSource()
+ private static IReadOnlyList> InitializeExternalImageModelsWithMockedStreamsTestDataSource()
{
- return new List>
+ return new List>
{
- () => new ExternalImageModel
+ () => new ImageFileModel
{
ContentStream = new Mock().Object,
ContentType = "image/jpeg",
},
- () => new ExternalImageModel
+ () => new ImageFileModel
{
ContentStream = new Mock().Object,
ContentType = "image/png",
@@ -535,9 +536,9 @@ private void SetUpValidatorWithOperationResult(bool succeeded)
result = () => OperationResult.Failed();
}
- var validator = new Mock>();
+ var validator = new Mock();
validator.Setup(x => x.Validate(It.IsAny())).Returns(result);
- serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidatorService))).Returns(validator.Object);
+ serviceProviderMock.Setup(x => x.GetService(typeof(IImageValidator))).Returns(validator.Object);
}
}
}
diff --git a/OutOfSchool/OutOfSchool.WebApi/Config/DataAccess/GcpStorageConfigConstants.cs b/OutOfSchool/OutOfSchool.WebApi/Config/DataAccess/GcpStorageConfigConstants.cs
new file mode 100644
index 0000000000..1b1916c588
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.WebApi/Config/DataAccess/GcpStorageConfigConstants.cs
@@ -0,0 +1,18 @@
+namespace OutOfSchool.WebApi.Config.DataAccess
+{
+ ///
+ /// Contains configuration path keys for Google Cloud Storage.
+ ///
+ public static class GcpStorageConfigConstants
+ {
+ ///
+ /// Points Google Cloud Storage Section.
+ ///
+ public const string GcpStorageBaseSection = "GoogleCloudPlatform:Storage:";
+
+ ///
+ /// Points section for images.
+ ///
+ public const string GcpStorageImagesConfig = GcpStorageBaseSection + "OosImages";
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.WebApi/Controllers/V1/PublicImageController.cs b/OutOfSchool/OutOfSchool.WebApi/Controllers/V1/PublicImageController.cs
index 4a7343116d..cc9c05e449 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Controllers/V1/PublicImageController.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Controllers/V1/PublicImageController.cs
@@ -28,15 +28,15 @@ public PublicImageController(IImageService imageService)
///
/// Gets for a given pictureId.
///
- /// This is the image id.
+ /// This is the image id.
/// The result of .
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileStreamResult))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
- [HttpGet("{imageMetadataId}")]
- public async Task GetByIdAsync(string imageMetadataId)
+ [HttpGet("{imageId}")]
+ public async Task GetByIdAsync(string imageId)
{
- var imageData = await imageService.GetByIdAsync(imageMetadataId).ConfigureAwait(false);
+ var imageData = await imageService.GetByIdAsync(imageId).ConfigureAwait(false);
if (imageData.Succeeded)
{
diff --git a/OutOfSchool/OutOfSchool.WebApi/Extensions/Startup/FileStorageExtensions.cs b/OutOfSchool/OutOfSchool.WebApi/Extensions/Startup/FileStorageExtensions.cs
new file mode 100644
index 0000000000..9c1292309c
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.WebApi/Extensions/Startup/FileStorageExtensions.cs
@@ -0,0 +1,25 @@
+using Google.Cloud.Storage.V1;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OutOfSchool.Services.Contexts;
+using OutOfSchool.Services.Contexts.Configuration;
+using OutOfSchool.Services.Extensions;
+using OutOfSchool.Services.Repository.Files;
+
+namespace OutOfSchool.WebApi.Extensions.Startup
+{
+ public static class FileStorageExtensions
+ {
+ public static IServiceCollection AddImagesStorage(this IServiceCollection services)
+ {
+ return services.AddTransient(provider =>
+ {
+ var config = provider.GetRequiredService>();
+ var googleCredential = config.Value.RetrieveGoogleCredential();
+ var storageClient = StorageClient.Create(googleCredential);
+ var storageContext = new GcpStorageContext(storageClient, config.Value.BucketName);
+ return new GcpImagesStorage(storageContext);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.WebApi/OutOfSchool.WebApi.csproj b/OutOfSchool/OutOfSchool.WebApi/OutOfSchool.WebApi.csproj
index 143d55dbb9..141629ace8 100644
--- a/OutOfSchool/OutOfSchool.WebApi/OutOfSchool.WebApi.csproj
+++ b/OutOfSchool/OutOfSchool.WebApi/OutOfSchool.WebApi.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidatorService.cs b/OutOfSchool/OutOfSchool.WebApi/Services/IFileValidator.cs
similarity index 52%
rename from OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidatorService.cs
rename to OutOfSchool/OutOfSchool.WebApi/Services/IFileValidator.cs
index c22a48ea1d..fb59db9734 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidatorService.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Services/IFileValidator.cs
@@ -1,13 +1,9 @@
-using System.IO;
+using System.IO;
using OutOfSchool.WebApi.Common;
-namespace OutOfSchool.WebApi.Services.Images
+namespace OutOfSchool.WebApi.Services
{
- ///
- /// Provides APIs for validating images by their options.
- ///
- /// This type encapsulates data for which should get validating options.
- public interface IImageValidatorService
+ public interface IFileValidator
{
///
/// Determines if the given context is valid.
@@ -21,21 +17,13 @@ public interface IImageValidatorService
///
/// Image size.
/// The value which shows the validation state.
- bool ImageSizeValid(long size);
-
- ///
- /// Determines if the given resolution is valid.
- ///
- /// Image width.
- /// Image height.
- /// The value which shows the validation state.
- bool ImageResolutionValid(int width, int height);
+ bool FileSizeValid(long size);
///
/// Determines if the given format is valid.
///
/// Image format.
/// The value which shows the validation state.
- bool ImageFormatValid(string format);
+ bool FileFormatValid(string format);
}
-}
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidator.cs b/OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidator.cs
new file mode 100644
index 0000000000..d77d39b751
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.WebApi/Services/Images/IImageValidator.cs
@@ -0,0 +1,19 @@
+using System.IO;
+using OutOfSchool.WebApi.Common;
+
+namespace OutOfSchool.WebApi.Services.Images
+{
+ ///
+ /// Provides APIs for validating images by their options.
+ ///
+ public interface IImageValidator : IFileValidator
+ {
+ ///
+ /// Determines if the given resolution is valid.
+ ///
+ /// Image width.
+ /// Image height.
+ /// The value which shows the validation state.
+ bool ImageResolutionValid(int width, int height);
+ }
+}
diff --git a/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageService.cs b/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageService.cs
index b6bb1c4b00..8a96d30697 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageService.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageService.cs
@@ -11,6 +11,7 @@
using OutOfSchool.Services.Models;
using OutOfSchool.Services.Models.Images;
using OutOfSchool.Services.Repository;
+using OutOfSchool.Services.Repository.Files;
using OutOfSchool.WebApi.Common;
using OutOfSchool.WebApi.Common.Resources;
using OutOfSchool.WebApi.Common.Resources.Codes;
@@ -25,19 +26,19 @@ namespace OutOfSchool.WebApi.Services.Images
///
public class ImageService : IImageService
{
- private readonly IExternalImageStorage externalStorage;
+ private readonly IImageFilesStorage imageStorage;
private readonly IServiceProvider serviceProvider;
private readonly ILogger logger;
///
/// Initializes a new instance of the class.
///
- /// Storage for images.
+ /// Storage for images.
/// Provides access to the app services.
/// Logger.
- public ImageService(IExternalImageStorage externalStorage, IServiceProvider serviceProvider, ILogger logger)
+ public ImageService(IImageFilesStorage imageStorage, IServiceProvider serviceProvider, ILogger logger)
{
- this.externalStorage = externalStorage;
+ this.imageStorage = imageStorage;
this.serviceProvider = serviceProvider;
this.logger = logger;
}
@@ -54,7 +55,12 @@ public async Task> GetByIdAsync(string imageId)
try
{
- var externalImageModel = await externalStorage.GetByIdAsync(imageId).ConfigureAwait(false);
+ var externalImageModel = await imageStorage.GetByIdAsync(imageId).ConfigureAwait(false);
+
+ if (externalImageModel == null)
+ {
+ return Result.Failed(ImagesOperationErrorCode.ImageNotFoundError.GetOperationError());
+ }
var imageDto = new ImageDto
{
@@ -65,7 +71,7 @@ public async Task> GetByIdAsync(string imageId)
logger.LogDebug($"Image with id {imageId} was successfully got.");
return Result.Success(imageDto);
}
- catch (ImageStorageException ex)
+ catch (FileStorageException ex)
{
logger.LogError(ex, $"Image with id {imageId} wasn't found.");
return Result.Failed(ImagesOperationErrorCode.ImageNotFoundError.GetOperationError());
@@ -236,12 +242,12 @@ private async Task> UploadImageProcessAsync(Stream contentStream,
{
try
{
- var imageStorageId = await externalStorage.UploadImageAsync(new ExternalImageModel { ContentStream = contentStream, ContentType = contentType })
+ var imageStorageId = await imageStorage.UploadAsync(new ImageFileModel { ContentStream = contentStream, ContentType = contentType })
.ConfigureAwait(false);
return Result.Success(imageStorageId);
}
- catch (ImageStorageException ex)
+ catch (FileStorageException ex)
{
logger.LogError(ex, $"Unable to upload image into an external storage because of {ex.Message}.");
return Result.Failed(ImagesOperationErrorCode.ImageStorageError.GetOperationError());
@@ -252,19 +258,19 @@ private async Task RemovingImageProcessAsync(string imageId)
{
try
{
- await externalStorage.DeleteImageAsync(imageId).ConfigureAwait(false);
+ await imageStorage.DeleteAsync(imageId).ConfigureAwait(false);
return OperationResult.Success;
}
- catch (ImageStorageException ex)
+ catch (FileStorageException ex)
{
logger.LogError(ex, $"Unreal to delete image with an external id = {imageId}.");
return OperationResult.Failed(ImagesOperationErrorCode.RemovingError.GetOperationError());
}
}
- private IImageValidatorService GetValidator()
+ private IImageValidator GetValidator()
{
- return (IImageValidatorService)serviceProvider.GetService(typeof(IImageValidatorService))
+ return (IImageValidator)serviceProvider.GetService(typeof(IImageValidator))
?? throw new NullReferenceException($"Unable to receive ImageValidatorService of type {nameof(T)}");
}
}
diff --git a/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidatorService.cs b/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidator.cs
similarity index 80%
rename from OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidatorService.cs
rename to OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidator.cs
index 10410c1606..209ef40ef5 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidatorService.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Services/Images/ImageValidator.cs
@@ -15,18 +15,18 @@ namespace OutOfSchool.WebApi.Services.Images
/// Provides APIs for validating images by their options.
///
/// This type encapsulates data for which should get validating options.
- public class ImageValidatorService : IImageValidatorService
+ public class ImageValidator : IImageValidator
{
private readonly ImageOptions options;
- private readonly ILogger> logger;
+ private readonly ILogger> logger;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// Image options.
/// Logger.
- public ImageValidatorService(IOptions> options, ILogger> logger)
+ public ImageValidator(IOptions> options, ILogger> logger)
{
this.options = options.Value;
this.logger = logger;
@@ -35,15 +35,16 @@ public ImageValidatorService(IOptions> options, ILogger
public OperationResult Validate(Stream stream)
{
- if (!ImageSizeValid(stream.Length))
- {
- return OperationResult.Failed(ImagesOperationErrorCode.InvalidSizeError.GetOperationError());
- }
-
try
{
+ _ = stream ?? throw new ArgumentNullException(nameof(stream));
+ if (!FileSizeValid(stream.Length))
+ {
+ return OperationResult.Failed(ImagesOperationErrorCode.InvalidSizeError.GetOperationError());
+ }
+
using var image = Image.FromStream(stream); // check disposing, using memory
- if (!ImageFormatValid(image.RawFormat.ToString()))
+ if (!FileFormatValid(image.RawFormat.ToString()))
{
return OperationResult.Failed(ImagesOperationErrorCode.InvalidFormatError.GetOperationError());
}
@@ -68,7 +69,7 @@ public OperationResult Validate(Stream stream)
}
///
- public bool ImageSizeValid(long size)
+ public bool FileSizeValid(long size)
{
return size <= options.MaxSizeBytes;
}
@@ -85,7 +86,7 @@ public bool ImageResolutionValid(int width, int height)
}
///
- public bool ImageFormatValid(string format)
+ public bool FileFormatValid(string format)
{
return options.SupportedFormats.Contains(format, StringComparer.OrdinalIgnoreCase);
}
diff --git a/OutOfSchool/OutOfSchool.WebApi/Startup.cs b/OutOfSchool/OutOfSchool.WebApi/Startup.cs
index 01620a9ee0..142fb4c48d 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Startup.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Startup.cs
@@ -4,6 +4,7 @@
using System.Net.Http;
using System.Text.Json.Serialization;
using AutoMapper;
+using Google.Cloud.Storage.V1;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -13,6 +14,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
using Microsoft.FeatureManagement;
using OutOfSchool.Common;
using OutOfSchool.Common.Config;
@@ -28,7 +30,9 @@
using OutOfSchool.Services.Models;
using OutOfSchool.Services.Models.ChatWorkshop;
using OutOfSchool.Services.Repository;
+using OutOfSchool.Services.Repository.Files;
using OutOfSchool.WebApi.Config;
+using OutOfSchool.WebApi.Config.DataAccess;
using OutOfSchool.WebApi.Config.Images;
using OutOfSchool.WebApi.Extensions;
using OutOfSchool.WebApi.Extensions.Startup;
@@ -153,6 +157,7 @@ public void ConfigureServices(IServiceCollection services)
services.Configure>(Configuration.GetSection($"Images:{nameof(Teacher)}:Limits"));
// Image options
+ services.Configure(Configuration.GetSection(GcpStorageConfigConstants.GcpStorageImagesConfig));
services.Configure(Configuration.GetSection(ExternalImageSourceConfig.Name));
services.AddSingleton();
services.Configure>(Configuration.GetSection($"Images:{nameof(Workshop)}:Specs"));
@@ -222,8 +227,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
services.AddScoped();
- services.AddScoped, ImageValidatorService>();
- services.AddScoped, ImageValidatorService>();
+ services.AddScoped>();
+ services.AddScoped>();
services.AddTransient();
services.AddTransient();
services.AddScoped();
@@ -260,7 +265,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
+ //services.AddTransient();
+ services.AddImagesStorage();
services.AddTransient();
services.AddTransient();
diff --git a/OutOfSchool/OutOfSchool.WebApi/appsettings.json b/OutOfSchool/OutOfSchool.WebApi/appsettings.json
index d42b85a113..81e6b10aa2 100644
--- a/OutOfSchool/OutOfSchool.WebApi/appsettings.json
+++ b/OutOfSchool/OutOfSchool.WebApi/appsettings.json
@@ -50,6 +50,14 @@
]
}
},
+ "GoogleCloudPlatform": {
+ "Storage": {
+ "OosImages": {
+ "CredentialFilePath": "",
+ "BucketName": ""
+ }
+ }
+ },
"Images": {
"Workshop": {
"Specs": {
diff --git a/OutOfSchool/Tests/OutOfSchool.WebApi.IntegrationTests/OutOfSchool.WebApi.IntegrationTests.csproj b/OutOfSchool/Tests/OutOfSchool.WebApi.IntegrationTests/OutOfSchool.WebApi.IntegrationTests.csproj
index 8b67f390fa..c45d0ad7f5 100644
--- a/OutOfSchool/Tests/OutOfSchool.WebApi.IntegrationTests/OutOfSchool.WebApi.IntegrationTests.csproj
+++ b/OutOfSchool/Tests/OutOfSchool.WebApi.IntegrationTests/OutOfSchool.WebApi.IntegrationTests.csproj
@@ -21,7 +21,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive