- 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
176 lines
6.1 KiB
Go
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)
|
|
}
|
|
|