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) }