Added S3 support and services

This commit is contained in:
2026-08-20 16:46:17 +02:00
parent f4c680d19d
commit 1931899d53
7 changed files with 151 additions and 0 deletions
@@ -0,0 +1,7 @@
namespace PostFundManagement.Domain.Abstractions;
public interface IS3Service
{
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,83 @@
using PostFundManagement.Domain.Services;
namespace PostFundManagement.Domain.Abstractions;
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
{
protected readonly IAmazonS3 Client = amazonS3;
protected abstract string BucketName { get; }
protected abstract string CdnBaseUrl { get; }
public virtual 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.");
using var stream = new MemoryStream();
await fileStream.CopyToAsync(stream, cancellationToken);
await fileStream.DisposeAsync();
stream.Seek(0, SeekOrigin.Begin);
var fileHash = HashService.StreamToSha256Hash(stream);
if(string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>("Failed to compute file hash.");
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}";
var putRequest = new PutObjectRequest
{
BucketName = BucketName,
Key = fileKey,
InputStream = stream,
ContentType = contentType,
UseChunkEncoding = false,
};
stream.Seek(0, SeekOrigin.Begin);
var response = await Client.PutObjectAsync(putRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
? Result.Fail<string>($"Failed to upload {fileName} to S3.")
: Result.Ok($"{CdnBaseUrl}/{fileKey}");
}
catch (Exception ex)
{
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default)
{
try
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail("Bucket name is not configured.");
var deleteRequest = new DeleteObjectRequest
{
BucketName = BucketName,
Key = fileKey
};
var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent
? Result.Fail($"Failed to delete {fileKey} from S3.")
: Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex));
}
}
}
@@ -0,0 +1,16 @@
namespace PostFundManagement.Domain.Configuration.S3;
public sealed 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; }
}
@@ -2,6 +2,18 @@ namespace PostFundManagement.Domain.Extensions;
public static class Constants public static class Constants
{ {
public const string GeneralS3SettingsSection = "PfmS3Settings";
public const string EvidenceS3SettingsSection = "EvidenceS3Settings";
public const string ContractS3SettingsSection = "ContractS3Settings";
public const string GeneralQuotesBucketName = "pfm.general";
public const string EvidenceQuotesBucketName = "pfm.evidence";
public const string ContractQuotesBucketName = "pfm.contract";
public const string DatabaseConfigName = "PfmDatabase"; public const string DatabaseConfigName = "PfmDatabase";
public const int LabelLength = 256; public const int LabelLength = 256;
@@ -0,0 +1,11 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Services;
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractQuotesBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? "";
}
@@ -0,0 +1,11 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Services;
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceQuotesBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? "";
}
@@ -0,0 +1,11 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Services;
public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralQuotesBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? "";
}