fix: change upload name

This commit is contained in:
2026-09-15 22:23:54 +07:00
parent 4078bf01f4
commit 637ca51f47
11 changed files with 62 additions and 46 deletions
@@ -56,11 +56,10 @@ public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentCon
if (request.Type == DocumentType.Document && !string.IsNullOrEmpty(request.Content)) if (request.Type == DocumentType.Document && !string.IsNullOrEmpty(request.Content))
{ {
await contentStore.EnsureBucketAsync(doc.ProjectId, ct);
var bytes = Encoding.UTF8.GetBytes(request.Content); var bytes = Encoding.UTF8.GetBytes(request.Content);
using (var ms = new MemoryStream(bytes)) 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; doc.ContentSize = bytes.Length;
@@ -14,29 +14,35 @@ public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore conten
var doc = await DocumentAccess.GetDocumentForUserAsync(uow, command.UserId, command.DocumentId, ct); var doc = await DocumentAccess.GetDocumentForUserAsync(uow, command.UserId, command.DocumentId, ct);
await DocumentAccess.EnsureDocumentPermissionAsync(uow, command.UserId, doc.ProjectId, PermissionAction.Delete, ct); await DocumentAccess.EnsureDocumentPermissionAsync(uow, command.UserId, doc.ProjectId, PermissionAction.Delete, ct);
var allIds = new List<(Guid ProjectId, Guid DocumentId)>(); var keys = new List<string>();
await CollectDescendants(uow, command.DocumentId, doc.ProjectId, allIds, ct); await CollectDescendants(uow, command.DocumentId, keys, ct);
allIds.Add((doc.ProjectId, doc.Id)); if (!string.IsNullOrEmpty(doc.StorageKey))
{
keys.Add(doc.StorageKey);
}
await DocumentAccess.DeleteDescendantsAsync(uow, command.DocumentId, ct); await DocumentAccess.DeleteDescendantsAsync(uow, command.DocumentId, ct);
uow.Documents.Remove(doc); uow.Documents.Remove(doc);
await uow.SaveChangesAsync(ct); 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( private static async Task CollectDescendants(
IUnitOfWork uow, Guid parentId, Guid projectId, IUnitOfWork uow, Guid parentId,
List<(Guid ProjectId, Guid DocumentId)> ids, CancellationToken ct) List<string> keys, CancellationToken ct)
{ {
var children = await uow.Documents.GetChildrenAsync(parentId, ct); var children = await uow.Documents.GetChildrenAsync(parentId, ct);
foreach (var child in children) foreach (var child in children)
{ {
ids.Add((projectId, child.Id)); if (!string.IsNullOrEmpty(child.StorageKey))
await CollectDescendants(uow, child.Id, projectId, ids, ct); {
keys.Add(child.StorageKey);
}
await CollectDescendants(uow, child.Id, keys, ct);
} }
} }
} }
@@ -31,11 +31,16 @@ public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentCon
if (doc.Type == DocumentType.Document) if (doc.Type == DocumentType.Document)
{ {
await contentStore.EnsureBucketAsync(doc.ProjectId, ct); var previousKey = doc.StorageKey;
var bytes = Encoding.UTF8.GetBytes(request.Content ?? ""); var bytes = Encoding.UTF8.GetBytes(request.Content ?? "");
using (var ms = new MemoryStream(bytes)) 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; doc.ContentSize = bytes.Length;
@@ -2,9 +2,7 @@ namespace mws.backend.dotnet.application.Documents;
public interface IDocumentContentStore public interface IDocumentContentStore
{ {
Task EnsureBucketAsync(Guid projectId, CancellationToken ct); Task<string> UploadAsync(Guid projectId, string fileName, string contentType, Stream body, CancellationToken ct);
Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct); Task<Stream> DownloadAsync(string storageKey, CancellationToken ct);
Task<Stream> DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct); Task DeleteAsync(string storageKey, CancellationToken ct);
Task<bool> ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct);
Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct);
} }
+2 -2
View File
@@ -16,9 +16,9 @@ public class GetDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentConten
var doc = await DocumentAccess.GetDocumentForUserAsync(uow, query.UserId, query.DocumentId, ct); var doc = await DocumentAccess.GetDocumentForUserAsync(uow, query.UserId, query.DocumentId, ct);
var dto = mapper.Map<DocumentDto>(doc); var dto = mapper.Map<DocumentDto>(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) if (stream != Stream.Null)
{ {
using var reader = new StreamReader(stream, Encoding.UTF8); using var reader = new StreamReader(stream, Encoding.UTF8);
+1
View File
@@ -14,6 +14,7 @@ public class Document
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public DocumentType Type { get; set; } = DocumentType.Document; public DocumentType Type { get; set; } = DocumentType.Document;
public string? ContentHash { get; set; } public string? ContentHash { get; set; }
public string? StorageKey { get; set; }
public long? ContentSize { get; set; } public long? ContentSize { get; set; }
public DateTime? UpdatedContentAt { get; set; } public DateTime? UpdatedContentAt { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
@@ -50,6 +50,10 @@ namespace mws.backend.dotnet.infrastructure.Migrations
b.Property<Guid>("ProjectId") b.Property<Guid>("ProjectId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("StorageKey")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasMaxLength(300) .HasMaxLength(300)
@@ -147,6 +147,7 @@ namespace mws.backend.dotnet.infrastructure.Migrations
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
ProjectId = table.Column<Guid>(type: "uuid", nullable: false), ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
StorageKey = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false), Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Description = table.Column<string>(type: "text", nullable: true), Description = table.Column<string>(type: "text", nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false), Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
@@ -47,6 +47,10 @@ namespace mws.backend.dotnet.infrastructure.Migrations
b.Property<Guid>("ProjectId") b.Property<Guid>("ProjectId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("StorageKey")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasMaxLength(300) .HasMaxLength(300)
@@ -13,6 +13,7 @@ public class DocumentConfiguration : IEntityTypeConfiguration<Document>
e.Property(d => d.Title).HasMaxLength(300).IsRequired(); e.Property(d => d.Title).HasMaxLength(300).IsRequired();
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20); e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
e.Property(d => d.ContentHash).HasMaxLength(64); e.Property(d => d.ContentHash).HasMaxLength(64);
e.Property(d => d.StorageKey).HasMaxLength(512);
e.Property(d => d.ContentSize); e.Property(d => d.ContentSize);
e.Property(d => d.UpdatedContentAt); e.Property(d => d.UpdatedContentAt);
@@ -15,35 +15,45 @@ public class S3DocumentContentStore(
{ {
private string Bucket => opt.Value.Bucket; private string Bucket => opt.Value.Bucket;
private static string Key(Guid projectId, Guid documentId) => $"{projectId:N}/{documentId:N}.html"; public static string FileNameNow(string name)
public Task EnsureBucketAsync(Guid projectId, CancellationToken ct) => Task.CompletedTask;
public async Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct)
{ {
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<string> UploadAsync(Guid projectId, string fileName, string contentType, Stream body, CancellationToken ct)
{
var key = $"{projectId:N}/{FileNameNow(fileName)}.html";
try try
{ {
await s3.PutObjectAsync(new PutObjectRequest await s3.PutObjectAsync(new PutObjectRequest
{ {
BucketName = Bucket, BucketName = Bucket,
Key = Key(projectId, documentId), Key = key,
InputStream = body, InputStream = body,
ContentType = contentType, ContentType = contentType,
AutoCloseStream = false, AutoCloseStream = false,
}, ct); }, ct);
return key;
} }
catch (AmazonS3Exception ex) 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); throw new StorageUnavailableException("Document upload failed", ex);
} }
} }
public async Task<Stream> DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct) public async Task<Stream> DownloadAsync(string storageKey, CancellationToken ct)
{ {
try try
{ {
var response = await s3.GetObjectAsync(Bucket, Key(projectId, documentId), ct); var response = await s3.GetObjectAsync(Bucket, storageKey, ct);
return response.ResponseStream; return response.ResponseStream;
} }
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
@@ -52,33 +62,20 @@ public class S3DocumentContentStore(
} }
catch (AmazonS3Exception ex) 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); throw new StorageUnavailableException("Document download failed", ex);
} }
} }
public async Task<bool> ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct) public async Task DeleteAsync(string storageKey, CancellationToken ct)
{ {
try try
{ {
await s3.GetObjectMetadataAsync(Bucket, Key(projectId, documentId), ct); await s3.DeleteObjectAsync(Bucket, storageKey, 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);
} }
catch (AmazonS3Exception ex) catch (AmazonS3Exception ex)
{ {
log.LogWarning(ex, "MinIO delete failed for document {DocumentId}", documentId); log.LogWarning(ex, "MinIO delete failed for {Key}", storageKey);
} }
} }
} }