using System.Net; using Amazon.S3; using Amazon.S3.Model; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using mws.backend.dotnet.application.Common; using mws.backend.dotnet.application.Documents; namespace mws.backend.dotnet.infrastructure.Storage; public class S3DocumentContentStore( IAmazonS3 s3, IOptions opt, ILogger log) : IDocumentContentStore { private string Bucket => opt.Value.Bucket; public static string FileNameNow(string name) { var slug = new string(name.Trim().ToLowerInvariant() .Select(c => char.IsLetterOrDigit(c) ? c : '-') .ToArray()).Trim('-'); if (slug.Length == 0) { slug = "document"; } return $"{slug}_{DateTime.UtcNow:yyyyMMddHHmmss}"; } public async Task UploadAsync(Guid projectId, string fileName, string contentType, Stream body, CancellationToken ct) { var key = $"{projectId:N}/{FileNameNow(fileName)}.html"; try { await s3.PutObjectAsync(new PutObjectRequest { BucketName = Bucket, Key = key, InputStream = body, ContentType = contentType, AutoCloseStream = false, }, ct); return key; } catch (AmazonS3Exception ex) { log.LogError(ex, "MinIO upload failed for {Key}", key); throw new StorageUnavailableException("Document upload failed", ex); } } public async Task DownloadAsync(string storageKey, CancellationToken ct) { try { var response = await s3.GetObjectAsync(Bucket, storageKey, ct); return response.ResponseStream; } catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return Stream.Null; } catch (AmazonS3Exception ex) { log.LogError(ex, "MinIO download failed for {Key}", storageKey); throw new StorageUnavailableException("Document download failed", ex); } } public async Task DeleteAsync(string storageKey, CancellationToken ct) { try { await s3.DeleteObjectAsync(Bucket, storageKey, ct); } catch (AmazonS3Exception ex) { log.LogWarning(ex, "MinIO delete failed for {Key}", storageKey); } } }