diff --git a/LiteCharms.Features/Extensions/S3.cs b/LiteCharms.Features/Extensions/S3.cs index 0a99a49..5518b7d 100644 --- a/LiteCharms.Features/Extensions/S3.cs +++ b/LiteCharms.Features/Extensions/S3.cs @@ -1,6 +1,6 @@ -using Amazon.Runtime; -using LiteCharms.Features.S3; -using LiteCharms.Features.S3.Configuration; +using LiteCharms.Features.S3; +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.Extensions; @@ -8,23 +8,50 @@ public static class S3 { public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) { - var optionsSection = configuration.GetSection(nameof(S3Settings)); - services.Configure(optionsSection); - - var options = optionsSection.Get() - ?? throw new InvalidOperationException("S3 configuration section is missing."); - - var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey); - - var s3Config = new AmazonS3Config + if (configuration.GetSection(BookshopBucketName) is not null) { - ServiceURL = options.ServiceUrl, - AuthenticationRegion = options.Region, - ForcePathStyle = true, - }; + services.AddKeyedSingleton(BookshopBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value, + ForcePathStyle = true, + })); - services.AddSingleton(new AmazonS3Client(credentials, s3Config)); - services.AddScoped(); + services.AddKeyedScoped(BookshopBucketName); + } + + if (configuration.GetSection(BookshopInvoicesBucketName) is not null) + { + services.AddKeyedSingleton(BookshopInvoicesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesBucketName}:AccessKey").Value, + configuration.GetSection($"{BookshopInvoicesBucketName}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopInvoicesBucketName}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesBucketName}:Region").Value, + ForcePathStyle = true, + })); + + services.AddKeyedScoped(BookshopInvoicesBucketName); + } + + if (configuration.GetSection(BookshopQuotesBucketName) is not null) + { + services.AddKeyedSingleton(BookshopQuotesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesBucketName}:AccessKey").Value, + configuration.GetSection($"{BookshopQuotesBucketName}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopQuotesBucketName}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopQuotesBucketName}:Region").Value, + ForcePathStyle = true, + })); + + services.AddKeyedScoped(BookshopQuotesBucketName); + } return services; } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 1e65e11..ab20ae2 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -137,6 +137,7 @@ + diff --git a/LiteCharms.Features/S3/Abstractions/IS3Service.cs b/LiteCharms.Features/S3/Abstractions/IS3Service.cs new file mode 100644 index 0000000..8684b2d --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/IS3Service.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.S3.Abstractions; + +public interface IS3Service +{ + Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs new file mode 100644 index 0000000..7b9f147 --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.S3.Abstractions; + +public abstract class S3ServiceBase(IAmazonS3 amazonS3) +{ + protected readonly IAmazonS3 client = amazonS3; + + public abstract Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs b/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs new file mode 100644 index 0000000..9614d6f --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs b/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs new file mode 100644 index 0000000..79b1abc --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreQuotesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/BookstoreS3Service.cs b/LiteCharms.Features/S3/BookstoreS3Service.cs new file mode 100644 index 0000000..2edf506 --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs index dfcb0c0..6be7460 100644 --- a/LiteCharms.Features/S3/Configuration/S3Settings.cs +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -11,4 +11,6 @@ public class S3Settings public string? BucketName { get; set; } public string? Region { get; set; } + + public string? CdnBaseUrl { get; set; } } diff --git a/LiteCharms.Features/S3/Constants.cs b/LiteCharms.Features/S3/Constants.cs new file mode 100644 index 0000000..3da4501 --- /dev/null +++ b/LiteCharms.Features/S3/Constants.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.S3; + +public static class Constants +{ + public const string BookshopS3SettingsSection = "BookshopS3Settings"; + public const string BookshopInvoicesS3SettingsSection = "BookshopInvoicesS3Settings"; + public const string BookshopQuotesS3SettingsSection = "BookshopQuotesS3Settings"; + + public const string BookshopBucketName = "bookshop"; + public const string BookshopInvoicesBucketName = "bookshop.invoices"; + public const string BookshopQuotesBucketName = "bookshop.quotes"; +} diff --git a/LiteCharms.Features/S3/S3Service.cs b/LiteCharms.Features/S3/S3Service.cs deleted file mode 100644 index b55c267..0000000 --- a/LiteCharms.Features/S3/S3Service.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.S3; - -public class S3Service(IAmazonS3 amazonS3) -{ - public async Task> UploadFileAsync(string bucketName, string fileName, Stream fileStream, string contentType, string cdnBaseUrl, CancellationToken cancellationToken = default) - { - try - { - var putRequest = new PutObjectRequest - { - BucketName = bucketName, - Key = fileName, - InputStream = fileStream, - ContentType = contentType - }; - - var response = await amazonS3.PutObjectAsync(putRequest, cancellationToken); - - return response.HttpStatusCode != System.Net.HttpStatusCode.OK - ? Result.Fail($"Failed to upload {fileName} to S3.") - : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); - } - } -}