From 999c94eedc1a97ed5808b775e742d785f70817ac Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Fri, 28 Aug 2026 00:32:41 -0400 Subject: [PATCH] Add SeaweedFS-backed file storage infrastructure Introduces IFileStorageProvider and an S3-compatible implementation (S3FileStorageProvider) configured via new SEAWEEDFS_* configuration keys, for storing uploaded files against SeaweedFS. --- web/backend/Biergarten.API.slnx | 1 + .../ConfigurationKeys.cs | 5 + .../IFileStorageProvider.cs | 29 ++++++ .../Infrastructure.FileUpload.csproj | 20 ++++ .../S3FileStorageProvider.cs | 96 +++++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 web/backend/Infrastructure/Infrastructure.FileUpload/IFileStorageProvider.cs create mode 100644 web/backend/Infrastructure/Infrastructure.FileUpload/Infrastructure.FileUpload.csproj create mode 100644 web/backend/Infrastructure/Infrastructure.FileUpload/S3FileStorageProvider.cs diff --git a/web/backend/Biergarten.API.slnx b/web/backend/Biergarten.API.slnx index f4ce55b6..fea462df 100644 --- a/web/backend/Biergarten.API.slnx +++ b/web/backend/Biergarten.API.slnx @@ -16,6 +16,7 @@ + diff --git a/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs index 0238734e..400a9742 100644 --- a/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs +++ b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs @@ -22,6 +22,11 @@ public static class ConfigurationKeys public const string SmtpFromEmail = "SMTP_FROM_EMAIL"; public const string SmtpFromName = "SMTP_FROM_NAME"; + public const string SeaweedFsServiceUrl = "SEAWEEDFS_SERVICE_URL"; + public const string SeaweedFsAccessKeyId = "SEAWEEDFS_ACCESS_KEY_ID"; + public const string SeaweedFsSecretAccessKey = "SEAWEEDFS_SECRET_ACCESS_KEY"; + public const string SeaweedFsBucket = "SEAWEEDFS_BUCKET"; + public const string DbServer = "DB_SERVER"; public const string DbUser = "DB_USER"; public const string DbPassword = "DB_PASSWORD"; diff --git a/web/backend/Infrastructure/Infrastructure.FileUpload/IFileStorageProvider.cs b/web/backend/Infrastructure/Infrastructure.FileUpload/IFileStorageProvider.cs new file mode 100644 index 00000000..106e3a60 --- /dev/null +++ b/web/backend/Infrastructure/Infrastructure.FileUpload/IFileStorageProvider.cs @@ -0,0 +1,29 @@ +namespace Infrastructure.FileUpload; + +/// +/// Service for storing and retrieving uploaded files in S3-compatible object storage. +/// +public interface IFileStorageProvider +{ + /// Uploads a file to the configured bucket. + /// The object key (path) to store the file under. + /// The file content. + /// The MIME type of the file. + /// A token to cancel the upload. + Task UploadAsync( + string key, + Stream content, + string contentType, + CancellationToken cancellationToken = default + ); + + /// Deletes a file from the configured bucket. + /// The object key of the file to delete. + /// A token to cancel the deletion. + Task DeleteAsync(string key, CancellationToken cancellationToken = default); + + /// Generates a time-limited URL for retrieving a file directly from storage. + /// The object key of the file. + /// How long the URL remains valid. + string GetPresignedUrl(string key, TimeSpan expiresIn); +} diff --git a/web/backend/Infrastructure/Infrastructure.FileUpload/Infrastructure.FileUpload.csproj b/web/backend/Infrastructure/Infrastructure.FileUpload/Infrastructure.FileUpload.csproj new file mode 100644 index 00000000..9308d863 --- /dev/null +++ b/web/backend/Infrastructure/Infrastructure.FileUpload/Infrastructure.FileUpload.csproj @@ -0,0 +1,20 @@ + + + net10.0 + enable + enable + Infrastructure.FileUpload + + + + + + + + + + + diff --git a/web/backend/Infrastructure/Infrastructure.FileUpload/S3FileStorageProvider.cs b/web/backend/Infrastructure/Infrastructure.FileUpload/S3FileStorageProvider.cs new file mode 100644 index 00000000..74d4a3ea --- /dev/null +++ b/web/backend/Infrastructure/Infrastructure.FileUpload/S3FileStorageProvider.cs @@ -0,0 +1,96 @@ +using Amazon.S3; +using Amazon.S3.Model; +using Infrastructure.Configuration; +using Microsoft.Extensions.Configuration; + +namespace Infrastructure.FileUpload; + +/// +/// File storage service backed by an S3-compatible object store (e.g. SeaweedFS, AWS S3). +/// Configured via application configuration (backed by environment variables). +/// +public class S3FileStorageProvider : IFileStorageProvider, IDisposable +{ + private readonly string _bucket; + private readonly IAmazonS3 _client; + + /// + /// Initializes a new instance of the class, reading storage + /// configuration (SEAWEEDFS_SERVICE_URL, SEAWEEDFS_ACCESS_KEY_ID, + /// SEAWEEDFS_SECRET_ACCESS_KEY, SEAWEEDFS_BUCKET) from . + /// + /// + /// Thrown when any of the required configuration values is not set. + /// + public S3FileStorageProvider(IConfiguration configuration) + { + string serviceUrl = ConfigurationHelpers.GetKeyOrThrow( + configuration, + ConfigurationKeys.SeaweedFsServiceUrl + ); + string accessKeyId = ConfigurationHelpers.GetKeyOrThrow( + configuration, + ConfigurationKeys.SeaweedFsAccessKeyId + ); + string secretAccessKey = ConfigurationHelpers.GetKeyOrThrow( + configuration, + ConfigurationKeys.SeaweedFsSecretAccessKey + ); + + _bucket = ConfigurationHelpers.GetKeyOrThrow( + configuration, + ConfigurationKeys.SeaweedFsBucket + ); + + AmazonS3Config config = new() { ServiceURL = serviceUrl, ForcePathStyle = true }; + + _client = new AmazonS3Client(accessKeyId, secretAccessKey, config); + } + + /// + public async Task UploadAsync( + string key, + Stream content, + string contentType, + CancellationToken cancellationToken = default + ) + { + PutObjectRequest request = new() + { + BucketName = _bucket, + Key = key, + InputStream = content, + ContentType = contentType, + }; + + await _client.PutObjectAsync(request, cancellationToken); + } + + /// + public async Task DeleteAsync(string key, CancellationToken cancellationToken = default) + { + DeleteObjectRequest request = new() { BucketName = _bucket, Key = key }; + + await _client.DeleteObjectAsync(request, cancellationToken); + } + + /// + public string GetPresignedUrl(string key, TimeSpan expiresIn) + { + GetPreSignedUrlRequest request = new() + { + BucketName = _bucket, + Key = key, + Expires = DateTime.UtcNow.Add(expiresIn), + }; + + return _client.GetPreSignedURL(request); + } + + /// + public void Dispose() + { + _client.Dispose(); + GC.SuppressFinalize(this); + } +}