feat: integration cloudfile

This commit is contained in:
2026-09-06 12:33:38 +07:00
parent c2088f42dc
commit 888dd41dcd
24 changed files with 1148 additions and 28 deletions
@@ -1,3 +1,4 @@
using System.Text;
using AutoMapper;
using MediatR;
using mws.backend.dotnet.application.Common;
@@ -8,7 +9,8 @@ namespace mws.backend.dotnet.application.Documents;
public record CreateDocumentCommand(Guid UserId, Guid ProjectId, CreateDocumentRequest Request) : IRequest<DocumentDto>;
public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateDocumentCommand, DocumentDto>
public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
: IRequestHandler<CreateDocumentCommand, DocumentDto>
{
public async Task<DocumentDto> Handle(CreateDocumentCommand command, CancellationToken ct)
{
@@ -43,7 +45,6 @@ public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHa
ParentId = request.ParentId,
Title = request.Title.Trim(),
Type = request.Type,
Content = request.Type == DocumentType.Document ? request.Content : null,
CreatedBy = command.UserId,
CreatedAt = now,
UpdatedBy = command.UserId,
@@ -52,6 +53,21 @@ public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHa
uow.Documents.Add(doc);
await uow.SaveChangesAsync(ct);
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.ContentSize = bytes.Length;
doc.UpdatedContentAt = now;
await uow.SaveChangesAsync(ct);
}
return mapper.Map<DocumentDto>(doc);
}
}
@@ -6,15 +6,37 @@ namespace mws.backend.dotnet.application.Documents;
public record DeleteDocumentCommand(Guid UserId, Guid DocumentId) : IRequest;
public class DeleteDocumentHandler(IUnitOfWork uow) : IRequestHandler<DeleteDocumentCommand>
public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore contentStore)
: IRequestHandler<DeleteDocumentCommand>
{
public async Task Handle(DeleteDocumentCommand command, CancellationToken ct)
{
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));
await DocumentAccess.DeleteDescendantsAsync(uow, command.DocumentId, ct);
uow.Documents.Remove(doc);
await uow.SaveChangesAsync(ct);
foreach (var (projectId, documentId) in allIds)
{
await contentStore.DeleteAsync(projectId, documentId, ct);
}
}
private static async Task CollectDescendants(
IUnitOfWork uow, Guid parentId, Guid projectId,
List<(Guid ProjectId, Guid DocumentId)> ids, 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);
}
}
}
@@ -1,3 +1,4 @@
using System.Text;
using AutoMapper;
using MediatR;
using mws.backend.dotnet.application.Common;
@@ -8,7 +9,8 @@ namespace mws.backend.dotnet.application.Documents;
public record UpdateDocumentCommand(Guid UserId, Guid DocumentId, UpdateDocumentRequest Request) : IRequest<DocumentDto>;
public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateDocumentCommand, DocumentDto>
public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
: IRequestHandler<UpdateDocumentCommand, DocumentDto>
{
public async Task<DocumentDto> Handle(UpdateDocumentCommand command, CancellationToken ct)
{
@@ -22,19 +24,31 @@ public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHa
}
doc.Title = request.Title.Trim();
if (doc.Type == DocumentType.Document)
{
doc.Content = request.Content;
doc.UpdatedAt = DateTime.UtcNow;
doc.UpdatedBy = command.UserId;
}
else
{
doc.UpdatedAt = DateTime.UtcNow;
doc.UpdatedBy = command.UserId;
}
doc.UpdatedAt = DateTime.UtcNow;
doc.UpdatedBy = command.UserId;
await uow.SaveChangesAsync(ct);
return mapper.Map<DocumentDto>(doc);
if (doc.Type == DocumentType.Document)
{
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.ContentSize = bytes.Length;
doc.UpdatedContentAt = DateTime.UtcNow;
doc.ContentHash = null;
await uow.SaveChangesAsync(ct);
}
var dto = mapper.Map<DocumentDto>(doc);
if (doc.Type == DocumentType.Document)
{
dto.Content = request.Content;
}
return dto;
}
}
@@ -0,0 +1,10 @@
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<Stream> DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct);
Task<bool> ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct);
Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct);
}
+22 -2
View File
@@ -1,16 +1,36 @@
using System.Text;
using AutoMapper;
using MediatR;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.domain.Documents;
namespace mws.backend.dotnet.application.Documents;
public record GetDocumentQuery(Guid UserId, Guid DocumentId) : IRequest<DocumentDto>;
public class GetDocumentHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetDocumentQuery, DocumentDto>
public class GetDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
: IRequestHandler<GetDocumentQuery, DocumentDto>
{
public async Task<DocumentDto> Handle(GetDocumentQuery query, CancellationToken ct)
{
var doc = await DocumentAccess.GetDocumentForUserAsync(uow, query.UserId, query.DocumentId, ct);
return mapper.Map<DocumentDto>(doc);
var dto = mapper.Map<DocumentDto>(doc);
if (doc.Type == DocumentType.Document)
{
await using var stream = await contentStore.DownloadAsync(doc.ProjectId, doc.Id, ct);
if (stream != Stream.Null)
{
using var reader = new StreamReader(stream, Encoding.UTF8);
var content = await reader.ReadToEndAsync(ct);
dto.Content = string.IsNullOrEmpty(content) ? null : content;
}
else
{
dto.Content = null;
}
}
return dto;
}
}