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 }