Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c9f5a846c | |||
| 89a343a85f | |||
| 41f7c05be3 | |||
| 52d204e286 | |||
| 1a03355e84 | |||
| f245bc94e1 | |||
| 7743c3178e | |||
| da141311ff |
@@ -40,7 +40,7 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper
|
|||||||
{
|
{
|
||||||
DateRange range = new()
|
DateRange range = new()
|
||||||
{
|
{
|
||||||
From = DateOnly.FromDateTime(new DateTime(2026, 05, 01, 0, 0, 0, DateTimeKind.Utc)),
|
From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)),
|
||||||
To = DateOnly.FromDateTime(DateTime.UtcNow),
|
To = DateOnly.FromDateTime(DateTime.UtcNow),
|
||||||
MaxRecords = 10
|
MaxRecords = 10
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using LiteCharms.Features.S3;
|
||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
|
public static class S3
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
if (configuration.GetSection(BookshopBucketName) is not null)
|
||||||
|
{
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,6 +129,17 @@
|
|||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Amazon S3 SDK -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.3.40" />
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="4.0.23.3" />
|
||||||
|
|
||||||
|
<!-- global Usings -->
|
||||||
|
<Using Include="Amazon.S3" />
|
||||||
|
<Using Include="Amazon.S3.Model" />
|
||||||
|
<Using Include="Amazon.Runtime" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
private readonly string bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value ?? "";
|
||||||
|
private readonly string cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||||
|
|
||||||
|
public override async Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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,16 @@
|
|||||||
|
namespace LiteCharms.Features.S3.Configuration;
|
||||||
|
|
||||||
|
public class S3Settings
|
||||||
|
{
|
||||||
|
public string? ServiceUrl { get; set; }
|
||||||
|
|
||||||
|
public string? AccessKey { get; set; }
|
||||||
|
|
||||||
|
public string? SecretKey { get; set; }
|
||||||
|
|
||||||
|
public string? BucketName { get; set; }
|
||||||
|
|
||||||
|
public string? Region { get; set; }
|
||||||
|
|
||||||
|
public string? CdnBaseUrl { get; set; }
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -97,12 +97,8 @@ public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory
|
|||||||
|
|
||||||
notification.Processed = request.Processed;
|
notification.Processed = request.Processed;
|
||||||
notification.UpdatedAt = DateTime.UtcNow;
|
notification.UpdatedAt = DateTime.UtcNow;
|
||||||
|
notification.HasError = request.HasError;
|
||||||
if (request.HasError)
|
notification.Errors = request.Errors;
|
||||||
{
|
|
||||||
notification.HasError = request.HasError;
|
|
||||||
notification.Errors = request.Errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
? Result.Ok()
|
? Result.Ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user