向宁 4927de90cc feat: add biz layer with usecases for file, bucket, folder, share
Defines repo interfaces in biz (dependency inversion) implemented by data layer.
Removes old domain layer replaced by data layer in previous commit.
2026-05-25 13:00:48 +08:00

156 lines
5.5 KiB
Go

package biz
import (
"bytes"
"context"
"fmt"
"time"
"rag/file-system/internal/data"
"rag/file-system/internal/pkg/sanitize"
"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
log *log.Helper
}
// NewFolderUsecase creates a new FolderUsecase.
func NewFolderUsecase(folderRepo FolderRepo, fileRepo FileMetaRepo, s3Repo FileRepo, logger log.Logger) *FolderUsecase {
return &FolderUsecase{
folderRepo: folderRepo,
fileRepo: fileRepo,
s3Repo: s3Repo,
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)
}
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)
}
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)
}