Refactored service to internalise the CDN #32

Merged
khwezi merged 1 commits from s3service into master 2026-05-19 11:34:52 +02:00
10 changed files with 188 additions and 46 deletions
+45 -18
View File
@@ -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<S3Settings>(optionsSection);
var options = optionsSection.Get<S3Settings>()
?? 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<IAmazonS3, AmazonS3Client>(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<IAmazonS3>(new AmazonS3Client(credentials, s3Config));
services.AddScoped<S3Service>();
services.AddKeyedScoped<IS3Service, BookstoreS3Service>(BookshopBucketName);
}
if (configuration.GetSection(BookshopInvoicesBucketName) is not null)
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(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<IS3Service, BookstoreInvoicesS3Service>(BookshopInvoicesBucketName);
}
if (configuration.GetSection(BookshopQuotesBucketName) is not null)
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(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<IS3Service, BookstoreQuotesS3Service>(BookshopQuotesBucketName);
}
return services;
}
@@ -137,6 +137,7 @@
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
@@ -0,0 +1,6 @@
namespace LiteCharms.Features.S3.Abstractions;
public interface IS3Service
{
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,8 @@
namespace LiteCharms.Features.S3.Abstractions;
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
{
protected readonly IAmazonS3 client = amazonS3;
public abstract Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
}
@@ -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<Result<string>> 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<string>("Bucket name is not configured.");
if (string.IsNullOrWhiteSpace(cdnBaseUrl))
return Result.Fail<string>("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<string>($"Failed to upload {fileName} to S3.")
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
}
@@ -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<Result<string>> 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<string>("Bucket name is not configured.");
if (string.IsNullOrWhiteSpace(cdnBaseUrl))
return Result.Fail<string>("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<string>($"Failed to upload {fileName} to S3.")
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
}
@@ -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<Result<string>> 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<string>("Bucket name is not configured.");
if (string.IsNullOrWhiteSpace(cdnBaseUrl))
return Result.Fail<string>("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<string>($"Failed to upload {fileName} to S3.")
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
}
@@ -11,4 +11,6 @@ public class S3Settings
public string? BucketName { get; set; }
public string? Region { get; set; }
public string? CdnBaseUrl { get; set; }
}
+12
View File
@@ -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";
}
-28
View File
@@ -1,28 +0,0 @@
namespace LiteCharms.Features.S3;
public class S3Service(IAmazonS3 amazonS3)
{
public async Task<Result<string>> 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<string>($"Failed to upload {fileName} to S3.")
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
}