using Amazon.S3; using Amazon.S3.Model; using Configuration; using Microsoft.Extensions.Configuration; namespace Features.ImageUploads.Services; /// /// 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; private readonly IAmazonS3 _presignClient; private readonly Protocol _presignProtocol; /// /// Initializes a new instance of the class, reading storage /// configuration (SEAWEEDFS_SERVICE_URL, SEAWEEDFS_ACCESS_KEY_ID, /// SEAWEEDFS_SECRET_ACCESS_KEY, SEAWEEDFS_BUCKET, and the optional /// SEAWEEDFS_PUBLIC_URL) 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); // A presigned URL is signed over its host, so one signed against the container-internal // endpoint is only usable from inside the network. Sign downloads against the public // endpoint instead, so a browser can fetch the object. string? configuredPublicUrl = configuration[ConfigurationKeys.SeaweedFsPublicUrl]; string publicUrl = string.IsNullOrWhiteSpace(configuredPublicUrl) ? serviceUrl : configuredPublicUrl; _presignClient = publicUrl == serviceUrl ? _client : new AmazonS3Client( accessKeyId, secretAccessKey, new AmazonS3Config { ServiceURL = publicUrl, ForcePathStyle = true } ); // GetPreSignedUrlRequest.Protocol defaults to HTTPS and ignores the ServiceURL scheme, so // take the scheme from the endpoint the URL is signed against. _presignProtocol = Uri.TryCreate(publicUrl, UriKind.Absolute, out Uri? publicUri) && publicUri.Scheme == Uri.UriSchemeHttp ? Protocol.HTTP : Protocol.HTTPS; } /// 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), Protocol = _presignProtocol, }; return _presignClient.GetPreSignedURL(request); } /// public void Dispose() { if (!ReferenceEquals(_presignClient, _client)) _presignClient.Dispose(); _client.Dispose(); GC.SuppressFinalize(this); } }