Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provicevko/files cloud storage #611

Merged
merged 9 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion OutOfSchool/OutOfSchool.Common/OutOfSchool.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,28 @@ namespace OutOfSchool.Services.Common.Exceptions
/// to work with image storage.
/// </summary>
[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)
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OutOfSchool.Services.Contexts.Configuration
{
/// <summary>
/// Contains a configuration that is essential for operations with images in Google Cloud Storage.
/// </summary>
public class GcpStorageImagesSourceConfig : GcpStorageSourceConfig
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace OutOfSchool.Services.Contexts.Configuration
{
/// <summary>
/// Contains a configuration that is essential for Google Cloud Storage.
/// </summary>
public abstract class GcpStorageSourceConfig
{
/// <summary>
/// Gets or sets a file of Google credential.
/// </summary>
public string CredentialFilePath { get; set; } // Should be set in environment variables

/// <summary>
/// Gets or sets a bucket name of Google Storage.
/// </summary>
public string BucketName { get; set; }
}
}
18 changes: 18 additions & 0 deletions OutOfSchool/OutOfSchool.DataAccess/Contexts/GcpStorageContext.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
11 changes: 11 additions & 0 deletions OutOfSchool/OutOfSchool.DataAccess/Contexts/IGcpStorageContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Google.Cloud.Storage.V1;

namespace OutOfSchool.Services.Contexts
{
public interface IGcpStorageContext
{
StorageClient StorageClient { get; }

string BucketName { get; }
}
}
18 changes: 18 additions & 0 deletions OutOfSchool/OutOfSchool.DataAccess/Extensions/GcpExtension.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
11 changes: 11 additions & 0 deletions OutOfSchool/OutOfSchool.DataAccess/Models/FileModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.IO;

namespace OutOfSchool.Services.Models
{
public class FileModel
{
public Stream ContentStream { get; set; }

public string ContentType { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.7.0" />
<PackageReference Include="H3Lib" Version="3.7.2" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="5.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.11" />
Expand All @@ -32,7 +33,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="5.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.13.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,39 @@ public ExternalImageStorage(MongoDb db)
gridFsBucket = db.GetContext();
}

public async Task<ExternalImageModel> GetByIdAsync(string imageId)
public async Task<ImageFileModel> GetByIdAsync(string imageId)
{
_ = imageId ?? throw new ArgumentNullException(nameof(imageId));
try
{
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<string> UploadImageAsync(ExternalImageModel imageModel, CancellationToken cancellationToken = default)
public async Task<string> 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);
}
}

Expand All @@ -72,7 +72,7 @@ public async Task DeleteImageAsync(string imageId, CancellationToken cancellatio
}
catch (Exception ex)
{
throw new ImageStorageException(ex);
throw new FileStorageException(ex);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TFile> : IFilesStorage<TFile, string>
where TFile : FileModel, new()
{
protected GcpFilesStorageBase(IGcpStorageContext storageContext)
{
StorageClient = storageContext.StorageClient;
BucketName = storageContext.BucketName;
}

private protected StorageClient StorageClient { get; }

private protected string BucketName { get; }

/// <inheritdoc/>
public virtual async Task<TFile> 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);
}
}

/// <inheritdoc/>
public virtual async Task<string> 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);
}
}

/// <inheritdoc/>
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);
}
}

/// <summary>
/// This method generates a unique value that is used as file identifier.
/// </summary>
/// <returns>
/// The result contains a string value of the file if it's uploaded.
/// </returns>
protected virtual string GenerateFileId()
{
return Guid.NewGuid().ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ImageFileModel>, IImageFilesStorage
{
public GcpImagesStorage(IGcpStorageContext storageContext)
: base(storageContext)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TFile, TIdentifier>
where TFile : FileModel
{
/// <summary>
/// Asynchronously gets a file by its id.
/// </summary>
/// <param name="fileId">File id.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
/// The task result contains a file of type <see cref="TFile"/>.
/// </returns>
Task<TFile> GetByIdAsync(TIdentifier fileId, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously uploads a file into the storage.
/// </summary>
/// <param name="file">File.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
/// The task result contains an identifier of the file if it's uploaded.
/// </returns>
Task<TIdentifier> UploadAsync(TFile file, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously deletes a file from the storage.
/// </summary>
/// <param name="fileId">File id.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
Task DeleteAsync(TIdentifier fileId, CancellationToken cancellationToken = default);
}
}
Loading