file-system/internal/api/handlers/share_queries.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

136 lines
3.5 KiB
Go

package handlers
import (
"context"
"fmt"
"time"
"rag/file-system/internal/common"
"rag/file-system/internal/domain/model"
"rag/file-system/internal/domain/repository"
)
type GetShareInfoQuery struct {
Token string
}
type DownloadShareQuery struct {
Token string
Password string
}
type GetShareInfoHandler struct {
ShareRepo repository.ShareRepository
FileMetaRepo repository.FileMetaRepository
}
func NewGetShareInfoHandler(shareRepo repository.ShareRepository, fileMetaRepo repository.FileMetaRepository) *GetShareInfoHandler {
return &GetShareInfoHandler{ShareRepo: shareRepo, FileMetaRepo: fileMetaRepo}
}
func (h *GetShareInfoHandler) Handle(ctx context.Context, q GetShareInfoQuery) (*model.ShareInfo, error) {
share, err := h.ShareRepo.GetByToken(ctx, q.Token)
if err != nil {
return nil, err
}
if share == nil {
return nil, common.NewNotFoundError("分享链接不存在")
}
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
return nil, common.NewBusinessException("分享链接已过期")
}
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
return nil, common.NewBusinessException("分享链接下载次数已达上限")
}
info := &model.ShareInfo{
Token: share.Token,
ResourceType: share.ResourceType,
HasPassword: share.Password != nil,
ExpiresAt: share.ExpiresAt,
}
if share.ResourceType == "file" {
file, err := h.FileMetaRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return nil, err
}
if file != nil {
info.FileName = file.Name
info.FileSize = file.Size
}
}
return info, nil
}
type DownloadShareHandler struct {
ShareRepo repository.ShareRepository
FileMetaRepo repository.FileMetaRepository
S3Repo repository.FileRepository
}
func NewDownloadShareHandler(
shareRepo repository.ShareRepository,
fileMetaRepo repository.FileMetaRepository,
s3Repo repository.FileRepository,
) *DownloadShareHandler {
return &DownloadShareHandler{
ShareRepo: shareRepo,
FileMetaRepo: fileMetaRepo,
S3Repo: s3Repo,
}
}
func (h *DownloadShareHandler) Handle(ctx context.Context, q DownloadShareQuery) (string, error) {
share, err := h.ShareRepo.GetByToken(ctx, q.Token)
if err != nil {
return "", err
}
if share == nil {
return "", common.NewNotFoundError("分享链接不存在")
}
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
return "", common.NewBusinessException("分享链接已过期")
}
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
return "", common.NewBusinessException("下载次数已达上限")
}
if share.Password != nil && *share.Password != "" {
if q.Password == "" {
return "", common.NewBusinessException("此分享需要密码")
}
if q.Password != *share.Password {
return "", common.NewBusinessException("密码错误")
}
}
if share.ResourceType != "file" {
return "", common.NewBusinessException("仅支持文件分享下载")
}
file, err := h.FileMetaRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return "", err
}
if file == nil {
return "", common.NewNotFoundError("文件不存在")
}
presignedURL, err := h.S3Repo.GeneratePresignedURL(ctx, file.S3Bucket, file.S3Key, 5*time.Minute)
if err != nil {
return "", fmt.Errorf("failed to generate download URL: %w", err)
}
if err := h.ShareRepo.IncrementDownloadCount(ctx, q.Token); err != nil {
common.Logger.Error("failed to increment download count", "token", q.Token, "error", err)
}
return presignedURL, nil
}