- 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
136 lines
3.5 KiB
Go
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
|
|
}
|