file-system/internal/api/handlers/file_meta_commands.go
向宁 3a18ca0579 feat: add directory structure and file sharing support
- PostgreSQL metadata overlay layer on top of existing S3 storage
- 3 new tables: folders, files, share_links
- Folder CRUD: create, get with children, tree, rename, delete (cascade)
- File operations: upload to folder, move between folders
- Share links: create with optional password/expiry/download limit, public access
- S3 compensation on PG write failure
- Existing 14 endpoints untouched
2026-05-20 20:26:19 +08:00

101 lines
2.5 KiB
Go

package handlers
import (
"context"
"fmt"
"io"
"time"
"rag/file-system/internal/common"
"rag/file-system/internal/domain/model"
"rag/file-system/internal/domain/repository"
"github.com/google/uuid"
)
type UploadToFolderCommand struct {
FolderID string
FileName string
Data io.Reader
S3Bucket string
OwnerID string
}
type MoveFileCommand struct {
FileID string
TargetFolderID string
OwnerID string
}
type UploadToFolderHandler struct {
FileMetaRepo repository.FileMetaRepository
FolderRepo repository.FolderRepository
S3Repo repository.FileRepository
}
func NewUploadToFolderHandler(
fileMetaRepo repository.FileMetaRepository,
folderRepo repository.FolderRepository,
s3Repo repository.FileRepository,
) *UploadToFolderHandler {
return &UploadToFolderHandler{
FileMetaRepo: fileMetaRepo,
FolderRepo: folderRepo,
S3Repo: s3Repo,
}
}
func (h *UploadToFolderHandler) Handle(ctx context.Context, cmd UploadToFolderCommand) (*model.FileMeta, error) {
folder, err := h.FolderRepo.GetByID(ctx, cmd.FolderID)
if err != nil {
return nil, err
}
if folder == nil || folder.OwnerID != cmd.OwnerID {
return nil, common.NewNotFoundError("目录不存在")
}
s3Key := uuid.New().String()
if err := h.S3Repo.UploadFile(ctx, cmd.S3Bucket, s3Key, cmd.Data); err != nil {
return nil, fmt.Errorf("S3 upload failed: %w", err)
}
now := time.Now()
fileMeta := &model.FileMeta{
ID: uuid.New().String(),
FolderID: cmd.FolderID,
Name: cmd.FileName,
S3Key: s3Key,
S3Bucket: cmd.S3Bucket,
ContentType: "application/octet-stream",
OwnerID: cmd.OwnerID,
CreatedAt: now,
UpdatedAt: now,
}
if err := h.FileMetaRepo.Create(ctx, fileMeta); err != nil {
if delErr := h.S3Repo.DeleteFile(ctx, cmd.S3Bucket, s3Key); delErr != nil {
common.Logger.Error("compensation failed: could not delete S3 object after PG write failure",
"s3_key", s3Key, "error", delErr)
}
return nil, fmt.Errorf("failed to save file metadata: %w", err)
}
return fileMeta, nil
}
type MoveFileHandler struct {
FileMetaRepo repository.FileMetaRepository
}
func NewMoveFileHandler(fileMetaRepo repository.FileMetaRepository) *MoveFileHandler {
return &MoveFileHandler{FileMetaRepo: fileMetaRepo}
}
func (h *MoveFileHandler) Handle(ctx context.Context, cmd MoveFileCommand) (string, error) {
if err := h.FileMetaRepo.Move(ctx, cmd.FileID, cmd.TargetFolderID, cmd.OwnerID); err != nil {
return "", err
}
return "文件移动成功", nil
}