From 637ca51f4764d7249c8beb49e8ce12c43400bc98 Mon Sep 17 00:00:00 2001 From: namdh861 Date: Tue, 15 Sep 2026 22:23:54 +0700 Subject: [PATCH] fix: change upload name --- .../Documents/Commands/CreateDocument.cs | 3 +- .../Documents/Commands/DeleteDocument.cs | 24 +++++---- .../Documents/Commands/UpdateDocument.cs | 9 +++- .../Documents/IDocumentContentStore.cs | 8 ++- application/Documents/Queries/GetDocument.cs | 4 +- domain/Documents/Document.cs | 1 + .../20260912132858_InitialCreate.Designer.cs | 4 ++ .../20260912132858_InitialCreate.cs | 1 + .../Migrations/AppDbContextModelSnapshot.cs | 4 ++ .../Configuration/DocumentConfiguration.cs | 1 + .../Storage/S3DocumentContentStore.cs | 49 +++++++++---------- 11 files changed, 62 insertions(+), 46 deletions(-) diff --git a/application/Documents/Commands/CreateDocument.cs b/application/Documents/Commands/CreateDocument.cs index 58a6c06..e941563 100644 --- a/application/Documents/Commands/CreateDocument.cs +++ b/application/Documents/Commands/CreateDocument.cs @@ -56,11 +56,10 @@ public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentCon if (request.Type == DocumentType.Document && !string.IsNullOrEmpty(request.Content)) { - await contentStore.EnsureBucketAsync(doc.ProjectId, ct); var bytes = Encoding.UTF8.GetBytes(request.Content); using (var ms = new MemoryStream(bytes)) { - await contentStore.UploadAsync(doc.ProjectId, doc.Id, "text/html; charset=utf-8", ms, ct); + doc.StorageKey = await contentStore.UploadAsync(doc.ProjectId, doc.Title, "text/html; charset=utf-8", ms, ct); } doc.ContentSize = bytes.Length; diff --git a/application/Documents/Commands/DeleteDocument.cs b/application/Documents/Commands/DeleteDocument.cs index 626a01c..0ae73ba 100644 --- a/application/Documents/Commands/DeleteDocument.cs +++ b/application/Documents/Commands/DeleteDocument.cs @@ -14,29 +14,35 @@ public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore conten var doc = await DocumentAccess.GetDocumentForUserAsync(uow, command.UserId, command.DocumentId, ct); await DocumentAccess.EnsureDocumentPermissionAsync(uow, command.UserId, doc.ProjectId, PermissionAction.Delete, ct); - var allIds = new List<(Guid ProjectId, Guid DocumentId)>(); - await CollectDescendants(uow, command.DocumentId, doc.ProjectId, allIds, ct); - allIds.Add((doc.ProjectId, doc.Id)); + var keys = new List(); + await CollectDescendants(uow, command.DocumentId, keys, ct); + if (!string.IsNullOrEmpty(doc.StorageKey)) + { + keys.Add(doc.StorageKey); + } await DocumentAccess.DeleteDescendantsAsync(uow, command.DocumentId, ct); uow.Documents.Remove(doc); await uow.SaveChangesAsync(ct); - foreach (var (projectId, documentId) in allIds) + foreach (var key in keys) { - await contentStore.DeleteAsync(projectId, documentId, ct); + await contentStore.DeleteAsync(key, ct); } } private static async Task CollectDescendants( - IUnitOfWork uow, Guid parentId, Guid projectId, - List<(Guid ProjectId, Guid DocumentId)> ids, CancellationToken ct) + IUnitOfWork uow, Guid parentId, + List keys, CancellationToken ct) { var children = await uow.Documents.GetChildrenAsync(parentId, ct); foreach (var child in children) { - ids.Add((projectId, child.Id)); - await CollectDescendants(uow, child.Id, projectId, ids, ct); + if (!string.IsNullOrEmpty(child.StorageKey)) + { + keys.Add(child.StorageKey); + } + await CollectDescendants(uow, child.Id, keys, ct); } } } diff --git a/application/Documents/Commands/UpdateDocument.cs b/application/Documents/Commands/UpdateDocument.cs index 5f87e12..faa9076 100644 --- a/application/Documents/Commands/UpdateDocument.cs +++ b/application/Documents/Commands/UpdateDocument.cs @@ -31,11 +31,16 @@ public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentCon if (doc.Type == DocumentType.Document) { - await contentStore.EnsureBucketAsync(doc.ProjectId, ct); + var previousKey = doc.StorageKey; var bytes = Encoding.UTF8.GetBytes(request.Content ?? ""); using (var ms = new MemoryStream(bytes)) { - await contentStore.UploadAsync(doc.ProjectId, doc.Id, "text/html; charset=utf-8", ms, ct); + doc.StorageKey = await contentStore.UploadAsync(doc.ProjectId, doc.Title, "text/html; charset=utf-8", ms, ct); + } + + if (!string.IsNullOrEmpty(previousKey) && previousKey != doc.StorageKey) + { + await contentStore.DeleteAsync(previousKey, ct); } doc.ContentSize = bytes.Length; diff --git a/application/Documents/IDocumentContentStore.cs b/application/Documents/IDocumentContentStore.cs index 770d2fd..7cf67d2 100644 --- a/application/Documents/IDocumentContentStore.cs +++ b/application/Documents/IDocumentContentStore.cs @@ -2,9 +2,7 @@ namespace mws.backend.dotnet.application.Documents; public interface IDocumentContentStore { - Task EnsureBucketAsync(Guid projectId, CancellationToken ct); - Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct); - Task DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct); - Task ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct); - Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct); + Task UploadAsync(Guid projectId, string fileName, string contentType, Stream body, CancellationToken ct); + Task DownloadAsync(string storageKey, CancellationToken ct); + Task DeleteAsync(string storageKey, CancellationToken ct); } diff --git a/application/Documents/Queries/GetDocument.cs b/application/Documents/Queries/GetDocument.cs index dd0c5d7..f75532f 100644 --- a/application/Documents/Queries/GetDocument.cs +++ b/application/Documents/Queries/GetDocument.cs @@ -16,9 +16,9 @@ public class GetDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentConten var doc = await DocumentAccess.GetDocumentForUserAsync(uow, query.UserId, query.DocumentId, ct); var dto = mapper.Map(doc); - if (doc.Type == DocumentType.Document) + if (doc.Type == DocumentType.Document && !string.IsNullOrEmpty(doc.StorageKey)) { - await using var stream = await contentStore.DownloadAsync(doc.ProjectId, doc.Id, ct); + await using var stream = await contentStore.DownloadAsync(doc.StorageKey, ct); if (stream != Stream.Null) { using var reader = new StreamReader(stream, Encoding.UTF8); diff --git a/domain/Documents/Document.cs b/domain/Documents/Document.cs index 840de29..4eab083 100644 --- a/domain/Documents/Document.cs +++ b/domain/Documents/Document.cs @@ -14,6 +14,7 @@ public class Document public string Title { get; set; } = string.Empty; public DocumentType Type { get; set; } = DocumentType.Document; public string? ContentHash { get; set; } + public string? StorageKey { get; set; } public long? ContentSize { get; set; } public DateTime? UpdatedContentAt { get; set; } public Guid CreatedBy { get; set; } diff --git a/infrastructure/Migrations/20260912132858_InitialCreate.Designer.cs b/infrastructure/Migrations/20260912132858_InitialCreate.Designer.cs index d03443b..2c10f08 100644 --- a/infrastructure/Migrations/20260912132858_InitialCreate.Designer.cs +++ b/infrastructure/Migrations/20260912132858_InitialCreate.Designer.cs @@ -50,6 +50,10 @@ namespace mws.backend.dotnet.infrastructure.Migrations b.Property("ProjectId") .HasColumnType("uuid"); + b.Property("StorageKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + b.Property("Title") .IsRequired() .HasMaxLength(300) diff --git a/infrastructure/Migrations/20260912132858_InitialCreate.cs b/infrastructure/Migrations/20260912132858_InitialCreate.cs index b77192f..5f72286 100644 --- a/infrastructure/Migrations/20260912132858_InitialCreate.cs +++ b/infrastructure/Migrations/20260912132858_InitialCreate.cs @@ -147,6 +147,7 @@ namespace mws.backend.dotnet.infrastructure.Migrations { Id = table.Column(type: "uuid", nullable: false), ProjectId = table.Column(type: "uuid", nullable: false), + StorageKey = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), Title = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), Description = table.Column(type: "text", nullable: true), Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), diff --git a/infrastructure/Migrations/AppDbContextModelSnapshot.cs b/infrastructure/Migrations/AppDbContextModelSnapshot.cs index 197e9a6..7ee8112 100644 --- a/infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -47,6 +47,10 @@ namespace mws.backend.dotnet.infrastructure.Migrations b.Property("ProjectId") .HasColumnType("uuid"); + b.Property("StorageKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + b.Property("Title") .IsRequired() .HasMaxLength(300) diff --git a/infrastructure/Persistence/Configuration/DocumentConfiguration.cs b/infrastructure/Persistence/Configuration/DocumentConfiguration.cs index cc56581..457ba9a 100644 --- a/infrastructure/Persistence/Configuration/DocumentConfiguration.cs +++ b/infrastructure/Persistence/Configuration/DocumentConfiguration.cs @@ -13,6 +13,7 @@ public class DocumentConfiguration : IEntityTypeConfiguration e.Property(d => d.Title).HasMaxLength(300).IsRequired(); e.Property(d => d.Type).HasConversion().HasMaxLength(20); e.Property(d => d.ContentHash).HasMaxLength(64); + e.Property(d => d.StorageKey).HasMaxLength(512); e.Property(d => d.ContentSize); e.Property(d => d.UpdatedContentAt); diff --git a/infrastructure/Storage/S3DocumentContentStore.cs b/infrastructure/Storage/S3DocumentContentStore.cs index 8c8221a..b425b43 100644 --- a/infrastructure/Storage/S3DocumentContentStore.cs +++ b/infrastructure/Storage/S3DocumentContentStore.cs @@ -15,35 +15,45 @@ public class S3DocumentContentStore( { private string Bucket => opt.Value.Bucket; - private static string Key(Guid projectId, Guid documentId) => $"{projectId:N}/{documentId:N}.html"; - - public Task EnsureBucketAsync(Guid projectId, CancellationToken ct) => Task.CompletedTask; - - public async Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct) + 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(projectId, documentId), + Key = key, InputStream = body, ContentType = contentType, AutoCloseStream = false, }, ct); + return key; } catch (AmazonS3Exception ex) { - log.LogError(ex, "MinIO upload failed for document {DocumentId}", documentId); + log.LogError(ex, "MinIO upload failed for {Key}", key); throw new StorageUnavailableException("Document upload failed", ex); } } - public async Task DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct) + public async Task DownloadAsync(string storageKey, CancellationToken ct) { try { - var response = await s3.GetObjectAsync(Bucket, Key(projectId, documentId), ct); + var response = await s3.GetObjectAsync(Bucket, storageKey, ct); return response.ResponseStream; } catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) @@ -52,33 +62,20 @@ public class S3DocumentContentStore( } catch (AmazonS3Exception ex) { - log.LogError(ex, "MinIO download failed for document {DocumentId}", documentId); + log.LogError(ex, "MinIO download failed for {Key}", storageKey); throw new StorageUnavailableException("Document download failed", ex); } } - public async Task ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct) + public async Task DeleteAsync(string storageKey, CancellationToken ct) { try { - await s3.GetObjectMetadataAsync(Bucket, Key(projectId, documentId), ct); - return true; - } - catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - return false; - } - } - - public async Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct) - { - try - { - await s3.DeleteObjectAsync(Bucket, Key(projectId, documentId), ct); + await s3.DeleteObjectAsync(Bucket, storageKey, ct); } catch (AmazonS3Exception ex) { - log.LogWarning(ex, "MinIO delete failed for document {DocumentId}", documentId); + log.LogWarning(ex, "MinIO delete failed for {Key}", storageKey); } } }