向宁 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

147 lines
4.5 KiB
Go

package biz
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"rag/file-system/internal/data"
"github.com/go-kratos/kratos/v2/log"
)
// ShareRepo defines the interface for share link persistence operations.
type ShareRepo interface {
Create(ctx context.Context, share *data.ShareLinkPO) error
GetByToken(ctx context.Context, token string) (*data.ShareLinkPO, error)
GetByID(ctx context.Context, id string) (*data.ShareLinkPO, error)
Delete(ctx context.Context, id, createdBy string) error
IncrementDownloadCount(ctx context.Context, token string) error
ListByResource(ctx context.Context, resourceType, resourceID string) ([]data.ShareLinkPO, error)
}
// FileMetaRepoForShare is a minimal interface for share usecase to look up file metadata.
type FileMetaRepoForShare interface {
GetByID(ctx context.Context, id string) (*data.FileMetaPO, error)
}
// ShareUsecase handles share link business logic.
type ShareUsecase struct {
shareRepo ShareRepo
fileRepo FileMetaRepoForShare
s3Repo FileRepo
log *log.Helper
}
// NewShareUsecase creates a new ShareUsecase.
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, logger log.Logger) *ShareUsecase {
return &ShareUsecase{
shareRepo: shareRepo,
fileRepo: fileRepo,
s3Repo: s3Repo,
log: log.NewHelper(logger),
}
}
// CreateShare creates a new share link with a random token.
func (uc *ShareUsecase) CreateShare(ctx context.Context, resourceType, resourceID, password string, expiresAt *time.Time, maxDownloads *int, createdBy string) (*data.ShareLinkPO, error) {
token, err := generateToken(16)
if err != nil {
return nil, fmt.Errorf("failed to generate share token: %w", err)
}
share := &data.ShareLinkPO{
ResourceType: resourceType,
ResourceID: resourceID,
Token: token,
ExpiresAt: expiresAt,
MaxDownloads: maxDownloads,
CreatedBy: createdBy,
}
if password != "" {
pwd := password
share.Password = &pwd
}
if err := uc.shareRepo.Create(ctx, share); err != nil {
return nil, fmt.Errorf("failed to create share link: %w", err)
}
return share, nil
}
// DeleteShare removes a share link by ID and creator.
func (uc *ShareUsecase) DeleteShare(ctx context.Context, id, createdBy string) error {
return uc.shareRepo.Delete(ctx, id, createdBy)
}
// GetShareInfo retrieves share details and the associated file metadata.
func (uc *ShareUsecase) GetShareInfo(ctx context.Context, token string) (*data.ShareLinkPO, *data.FileMetaPO, error) {
share, err := uc.shareRepo.GetByToken(ctx, token)
if err != nil {
return nil, nil, fmt.Errorf("failed to get share link: %w", err)
}
if share == nil {
return nil, nil, nil
}
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return share, nil, fmt.Errorf("failed to get file metadata: %w", err)
}
return share, fileMeta, nil
}
// DownloadShare validates the share link, increments download count, and returns a presigned URL + filename.
func (uc *ShareUsecase) DownloadShare(ctx context.Context, token string) (string, string, error) {
share, err := uc.shareRepo.GetByToken(ctx, token)
if err != nil {
return "", "", fmt.Errorf("failed to get share link: %w", err)
}
if share == nil {
return "", "", fmt.Errorf("share link not found")
}
// Check expiry.
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now().UTC()) {
return "", "", fmt.Errorf("share link has expired")
}
// Check max downloads.
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
return "", "", fmt.Errorf("download limit reached")
}
// Get file metadata for bucket/key info.
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return "", "", fmt.Errorf("failed to get file metadata: %w", err)
}
if fileMeta == nil {
return "", "", fmt.Errorf("file not found")
}
// Generate presigned URL.
url, err := uc.s3Repo.GeneratePresignedURL(ctx, fileMeta.S3Bucket, fileMeta.S3Key, 15*time.Minute)
if err != nil {
return "", "", fmt.Errorf("failed to generate download URL: %w", err)
}
// Increment download count.
if err := uc.shareRepo.IncrementDownloadCount(ctx, token); err != nil {
uc.log.Errorf("failed to increment download count for token %s: %v", token, err)
}
return url, fileMeta.Name, nil
}
// generateToken creates a cryptographically secure random hex token.
func generateToken(byteLen int) (string, error) {
b := make([]byte, byteLen)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}