mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
129 lines
4.4 KiB
C#
129 lines
4.4 KiB
C#
using Amazon.S3;
|
|
using Amazon.S3.Model;
|
|
using Configuration;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace Features.ImageUploads.Services;
|
|
|
|
/// <summary>
|
|
/// File storage service backed by an S3-compatible object store (e.g. SeaweedFS, AWS S3).
|
|
/// Configured via application configuration (backed by environment variables).
|
|
/// </summary>
|
|
public class S3FileStorageProvider : IFileStorageProvider, IDisposable
|
|
{
|
|
private readonly string _bucket;
|
|
private readonly IAmazonS3 _client;
|
|
private readonly IAmazonS3 _presignClient;
|
|
private readonly Protocol _presignProtocol;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="S3FileStorageProvider" /> class, reading storage
|
|
/// configuration (<c>SEAWEEDFS_SERVICE_URL</c>, <c>SEAWEEDFS_ACCESS_KEY_ID</c>,
|
|
/// <c>SEAWEEDFS_SECRET_ACCESS_KEY</c>, <c>SEAWEEDFS_BUCKET</c>, and the optional
|
|
/// <c>SEAWEEDFS_PUBLIC_URL</c>) from <paramref name="configuration" />.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// Thrown when any of the required configuration values is not set.
|
|
/// </exception>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task DeleteAsync(string key, CancellationToken cancellationToken = default)
|
|
{
|
|
DeleteObjectRequest request = new() { BucketName = _bucket, Key = key };
|
|
|
|
await _client.DeleteObjectAsync(request, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
if (!ReferenceEquals(_presignClient, _client))
|
|
_presignClient.Dispose();
|
|
|
|
_client.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|