向宁 11315fd00b feat: wire Watermill CQRS — EventBus in usecases, event handlers, router lifecycle
- Add EventPublisher interface in biz layer for domain event publishing
- Wire EventBusPublisher (Watermill EventBus adapter) into FileUsecase, FolderUsecase, ShareUsecase
- Publish events after UploadFile, DeleteFile, CreateFolder, DeleteFolder, CreateShare
- Implement CQRSHandler with logging event handlers for all 6 event types
- Register event handlers via CQRSBus.RegisterHandlers using Watermill EventProcessor
- Store subscriber and wmLogger in CQRSBus for EventProcessor wiring
- Expose SqlDB() on Data struct for Watermill SQL pub/sub
- Start Watermill router in goroutine alongside Kratos app with graceful close
- Use appContext wrapper struct to pass CQRSBus through Wire DI graph
2026-05-25 13:52:05 +08:00

176 lines
6.1 KiB
Go

package biz
import (
"bytes"
"context"
"fmt"
"time"
"rag/file-system/internal/data"
"rag/file-system/internal/pkg/sanitize"
"rag/file-system/internal/watermark"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/uuid"
)
// FolderRepo defines the interface for folder persistence operations.
type FolderRepo interface {
Create(ctx context.Context, folder *data.FolderPO) error
GetByID(ctx context.Context, id string) (*data.FolderPO, error)
GetWithChildren(ctx context.Context, id, ownerID string) (*data.FolderWithChildren, error)
GetTree(ctx context.Context, ownerID string) ([]data.FolderPO, error)
Update(ctx context.Context, folder *data.FolderPO) error
Delete(ctx context.Context, id, ownerID string) error
GetDescendantFileS3Keys(ctx context.Context, id, ownerID string) ([]data.FileMetaPO, error)
}
// FileMetaRepo defines the interface for file metadata persistence operations.
type FileMetaRepo interface {
Create(ctx context.Context, file *data.FileMetaPO) error
GetByID(ctx context.Context, id string) (*data.FileMetaPO, error)
GetByFolder(ctx context.Context, folderID string) ([]data.FileMetaPO, error)
Move(ctx context.Context, fileID, targetFolderID, ownerID string) error
Delete(ctx context.Context, id, ownerID string) error
}
// FolderUsecase handles folder and file metadata business logic.
type FolderUsecase struct {
folderRepo FolderRepo
fileRepo FileMetaRepo
s3Repo FileRepo
eventPub EventPublisher
log *log.Helper
}
// NewFolderUsecase creates a new FolderUsecase.
func NewFolderUsecase(folderRepo FolderRepo, fileRepo FileMetaRepo, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *FolderUsecase {
return &FolderUsecase{
folderRepo: folderRepo,
fileRepo: fileRepo,
s3Repo: s3Repo,
eventPub: eventPub,
log: log.NewHelper(logger),
}
}
// CreateFolder creates a new folder under the given parent.
func (uc *FolderUsecase) CreateFolder(ctx context.Context, parentID *string, name, ownerID string) (*data.FolderPO, error) {
folder := &data.FolderPO{
ParentID: parentID,
Name: sanitize.Filename(name),
OwnerID: ownerID,
}
if err := uc.folderRepo.Create(ctx, folder); err != nil {
return nil, fmt.Errorf("failed to create folder: %w", err)
}
// Publish domain event on success.
if err := uc.eventPub.Publish(ctx, &watermark.FolderCreatedEvent{
FolderID: folder.ID,
Name: folder.Name,
OwnerID: folder.OwnerID,
}); err != nil {
uc.log.Errorf("failed to publish FolderCreatedEvent: %v", err)
}
return folder, nil
}
// GetFolder retrieves a folder with its children (sub-folders and files).
func (uc *FolderUsecase) GetFolder(ctx context.Context, id, ownerID string) (*data.FolderPO, []data.FolderPO, []data.FileMetaPO, error) {
result, err := uc.folderRepo.GetWithChildren(ctx, id, ownerID)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get folder: %w", err)
}
if result == nil {
return nil, nil, nil, nil
}
return &result.Folder, result.SubFolders, result.Files, nil
}
// GetFolderTree retrieves all folders owned by the given owner.
func (uc *FolderUsecase) GetFolderTree(ctx context.Context, ownerID string) ([]data.FolderPO, error) {
return uc.folderRepo.GetTree(ctx, ownerID)
}
// RenameFolder updates a folder's name.
func (uc *FolderUsecase) RenameFolder(ctx context.Context, id, name, ownerID string) (*data.FolderPO, error) {
folder, err := uc.folderRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get folder: %w", err)
}
if folder == nil || folder.OwnerID != ownerID {
return nil, fmt.Errorf("folder not found or not owned by user")
}
folder.Name = sanitize.Filename(name)
folder.UpdatedAt = time.Now().UTC()
if err := uc.folderRepo.Update(ctx, folder); err != nil {
return nil, fmt.Errorf("failed to rename folder: %w", err)
}
return folder, nil
}
// DeleteFolder deletes a folder and all descendant files from both S3 and the database.
func (uc *FolderUsecase) DeleteFolder(ctx context.Context, id, ownerID string) error {
// First, get all descendant file S3 keys so we can delete from S3.
files, err := uc.folderRepo.GetDescendantFileS3Keys(ctx, id, ownerID)
if err != nil {
return fmt.Errorf("failed to get descendant files: %w", err)
}
// Delete each file from S3.
for _, f := range files {
if delErr := uc.s3Repo.DeleteFile(ctx, f.S3Bucket, f.S3Key); delErr != nil {
uc.log.Errorf("failed to delete S3 object %s/%s: %v", f.S3Bucket, f.S3Key, delErr)
// Continue deleting other files even if one fails.
}
}
// Delete the folder record (cascade should handle child files in DB).
if err := uc.folderRepo.Delete(ctx, id, ownerID); err != nil {
return fmt.Errorf("failed to delete folder: %w", err)
}
// Publish domain event on success.
if err := uc.eventPub.Publish(ctx, &watermark.FolderDeletedEvent{
FolderID: id,
OwnerID: ownerID,
}); err != nil {
uc.log.Errorf("failed to publish FolderDeletedEvent: %v", err)
}
return nil
}
// UploadToFolder uploads a file to a folder: generates a UUID S3 key, uploads to S3, saves metadata.
func (uc *FolderUsecase) UploadToFolder(ctx context.Context, folderID, fileName string, fileData []byte, contentType, ownerID string) (*data.FileMetaPO, error) {
s3Key := uuid.New().String()
safeName := sanitize.Filename(fileName)
bucket := "files" // Default bucket for folder-based uploads.
if err := uc.s3Repo.UploadFile(ctx, bucket, s3Key, bytes.NewReader(fileData)); err != nil {
return nil, fmt.Errorf("failed to upload file to S3: %w", err)
}
meta := &data.FileMetaPO{
FolderID: folderID,
Name: safeName,
S3Key: s3Key,
S3Bucket: bucket,
Size: int64(len(fileData)),
ContentType: contentType,
OwnerID: ownerID,
}
if err := uc.fileRepo.Create(ctx, meta); err != nil {
// Attempt to clean up the S3 object on metadata save failure.
_ = uc.s3Repo.DeleteFile(ctx, bucket, s3Key)
return nil, fmt.Errorf("failed to save file metadata: %w", err)
}
return meta, nil
}
// MoveFile moves a file from one folder to another.
func (uc *FolderUsecase) MoveFile(ctx context.Context, fileID, targetFolderID, ownerID string) error {
return uc.fileRepo.Move(ctx, fileID, targetFolderID, ownerID)
}