- Add EventPublisher interface in biz layer for domain event publishing - Wire EventBusPublisher (Watermill EventBus adapter) into FileUsecase, FolderUsecase, ShareUsecase - Publish events after UploadFile, DeleteFile, CreateFolder, DeleteFolder, CreateShare - Implement CQRSHandler with logging event handlers for all 6 event types - Register event handlers via CQRSBus.RegisterHandlers using Watermill EventProcessor - Store subscriber and wmLogger in CQRSBus for EventProcessor wiring - Expose SqlDB() on Data struct for Watermill SQL pub/sub - Start Watermill router in goroutine alongside Kratos app with graceful close - Use appContext wrapper struct to pass CQRSBus through Wire DI graph
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
package biz
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"rag/file-system/internal/data"
|
|
"rag/file-system/internal/watermark"
|
|
|
|
"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
|
|
eventPub EventPublisher
|
|
log *log.Helper
|
|
}
|
|
|
|
// NewShareUsecase creates a new ShareUsecase.
|
|
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *ShareUsecase {
|
|
return &ShareUsecase{
|
|
shareRepo: shareRepo,
|
|
fileRepo: fileRepo,
|
|
s3Repo: s3Repo,
|
|
eventPub: eventPub,
|
|
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)
|
|
}
|
|
|
|
// Publish domain event on success.
|
|
if err := uc.eventPub.Publish(ctx, &watermark.ShareCreatedEvent{
|
|
ShareID: share.ID,
|
|
ResourceType: share.ResourceType,
|
|
ResourceID: share.ResourceID,
|
|
Token: share.Token,
|
|
CreatedBy: share.CreatedBy,
|
|
}); err != nil {
|
|
uc.log.Errorf("failed to publish ShareCreatedEvent: %v", 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
|
|
}
|