215 lines
6.3 KiB
Go
215 lines
6.3 KiB
Go
package domain
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
archivalRules "git.nakama.town/fmartingr/hako/internal/archival/rules"
|
|
archivalStore "git.nakama.town/fmartingr/hako/internal/archival/store"
|
|
"git.nakama.town/fmartingr/hako/internal/dependencies"
|
|
"git.nakama.town/fmartingr/hako/internal/model"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// LinkDomain handles business logic for links
|
|
type LinkDomain struct {
|
|
deps model.Dependencies
|
|
}
|
|
|
|
// NewLinkDomain creates a new LinkDomain
|
|
func NewLinkDomain(deps model.Dependencies) model.LinkDomain {
|
|
return &LinkDomain{deps: deps}
|
|
}
|
|
|
|
// CreateLink creates a new link and enqueues an archive job
|
|
func (d *LinkDomain) CreateLink(ctx context.Context, url, userID string) (*model.Link, error) {
|
|
// Access dependencies
|
|
deps := d.deps.(*dependencies.Dependencies)
|
|
|
|
// Clean URL by removing tracking parameters before processing
|
|
url = archivalRules.CleanURL(url)
|
|
|
|
// Check if link already exists for this user
|
|
existing, err := deps.LinkStore.GetByURL(ctx, url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check existing link: %w", err)
|
|
}
|
|
|
|
if existing != nil && existing.UserID == userID {
|
|
return existing, nil
|
|
}
|
|
|
|
// Create new link
|
|
now := time.Now()
|
|
link := &model.Link{
|
|
ID: uuid.New().String(),
|
|
URL: url,
|
|
UserID: userID,
|
|
TotalSize: 0, // Initialize to 0, will be updated when archives complete
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if err := deps.LinkStore.Create(ctx, link); err != nil {
|
|
return nil, fmt.Errorf("failed to create link: %w", err)
|
|
}
|
|
|
|
// Analyze URL using HEAD request
|
|
metadata, err := archivalRules.AnalyzeURL(ctx, url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to analyze URL: %w", err)
|
|
}
|
|
|
|
// Evaluate rules to determine archivers
|
|
archivers, err := deps.RuleEngine.Evaluate(ctx, metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to evaluate rules: %w", err)
|
|
}
|
|
|
|
// Log which archivers were selected
|
|
deps.Logger().Info("Rule engine selected archivers", "url", url, "archiverCount", len(archivers), "archivers", archivers)
|
|
|
|
// If no archivers returned (empty array), skip archiving
|
|
if len(archivers) == 0 {
|
|
// Link created but no archive - return link without creating archive
|
|
deps.Logger().Info("No archivers selected, skipping archiving", "url", url)
|
|
return link, nil
|
|
}
|
|
|
|
// Create archive record with pending status
|
|
archive := &model.Archive{
|
|
ID: uuid.New().String(),
|
|
LinkID: link.ID,
|
|
UserID: userID,
|
|
Status: model.ArchiveStatusPending,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
// Store now accepts model types directly
|
|
if err := deps.ArchiveStore.Create(ctx, archive); err != nil {
|
|
return nil, fmt.Errorf("failed to create archive: %w", err)
|
|
}
|
|
|
|
// Enqueue archive job with archiver keys
|
|
archiverKeys := make([]string, len(archivers))
|
|
for i, arch := range archivers {
|
|
archiverKeys[i] = arch.Key
|
|
}
|
|
|
|
payload := model.ArchiveLinkPayload{
|
|
ArchiveID: archive.ID,
|
|
LinkID: link.ID,
|
|
ArchiverKeys: archiverKeys,
|
|
}
|
|
|
|
if err := deps.Queue.Enqueue(ctx, model.JobTypeArchiveLink, payload); err != nil {
|
|
return nil, fmt.Errorf("failed to enqueue archive job: %w", err)
|
|
}
|
|
|
|
return link, nil
|
|
}
|
|
|
|
// GetLink retrieves a link by ID
|
|
func (d *LinkDomain) GetLink(ctx context.Context, id string) (*model.Link, error) {
|
|
deps := d.deps.(*dependencies.Dependencies)
|
|
link, err := deps.LinkStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get link: %w", err)
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
// ListLinks retrieves links for a user with pagination
|
|
func (d *LinkDomain) ListLinks(ctx context.Context, req model.LinkListRequest) ([]model.LinkListItem, int, error) {
|
|
req.Defaults()
|
|
if err := req.IsValid(); err != nil {
|
|
return nil, 0, fmt.Errorf("invalid request: %w", err)
|
|
}
|
|
|
|
deps := d.deps.(*dependencies.Dependencies)
|
|
listOpts := convertLinkListRequestToOptions(req)
|
|
countOpts := convertLinkListRequestToCountOptions(req)
|
|
|
|
storeLinks, err := deps.LinkStore.List(ctx, listOpts)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to list links: %w", err)
|
|
}
|
|
|
|
count, err := deps.LinkStore.Count(ctx, countOpts)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to count links: %w", err)
|
|
}
|
|
|
|
// Convert to domain structs
|
|
items := make([]model.LinkListItem, 0, len(storeLinks))
|
|
for _, link := range storeLinks {
|
|
item := model.LinkListItem{
|
|
Link: model.Link{
|
|
ID: link.ID,
|
|
URL: link.URL,
|
|
UserID: link.UserID,
|
|
TotalSize: link.TotalSize,
|
|
CreatedAt: link.CreatedAt,
|
|
UpdatedAt: link.UpdatedAt,
|
|
},
|
|
}
|
|
|
|
// Get latest archive for this link
|
|
latestArchive, err := deps.ArchiveStore.GetLatestByLinkID(ctx, link.ID)
|
|
if err == nil && latestArchive != nil {
|
|
item.LatestArchiveTitle = latestArchive.Title
|
|
item.LatestArchiveStatus = string(latestArchive.Status)
|
|
|
|
// Get thumbnail (always check, but only include URL if requested)
|
|
thumbnailFile, err := deps.ArchiveFileStore.GetThumbnailByLinkID(ctx, link.ID)
|
|
if err == nil && thumbnailFile != nil {
|
|
item.HasThumbnail = true
|
|
// Only construct URL if requested
|
|
if req.IncludeThumbnail {
|
|
item.ThumbnailURL = fmt.Sprintf("/api/v1/archives/%s/files/%s/download", latestArchive.ID, thumbnailFile.ID)
|
|
}
|
|
} else {
|
|
item.HasThumbnail = false
|
|
}
|
|
}
|
|
|
|
items = append(items, item)
|
|
}
|
|
|
|
return items, count, nil
|
|
}
|
|
|
|
// DeleteLink deletes a link and all its associated data (cascade delete)
|
|
func (d *LinkDomain) DeleteLink(ctx context.Context, id string) error {
|
|
deps := d.deps.(*dependencies.Dependencies)
|
|
if err := deps.Storage.DeleteDirectory(ctx, id); err != nil {
|
|
// Log error but continue - directory might not exist
|
|
deps.Logger().Error("Failed to delete link directory", "linkID", id, "error", err)
|
|
}
|
|
|
|
if err := deps.LinkStore.Delete(ctx, id); err != nil {
|
|
return fmt.Errorf("failed to delete link: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions to convert LinkListRequest to store options
|
|
func convertLinkListRequestToOptions(req model.LinkListRequest) archivalStore.LinkListOptions {
|
|
return archivalStore.LinkListOptions{
|
|
UserID: req.UserID,
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
CategoryID: req.CategoryID,
|
|
SearchQuery: req.SearchQuery,
|
|
}
|
|
}
|
|
|
|
func convertLinkListRequestToCountOptions(req model.LinkListRequest) archivalStore.LinkCountOptions {
|
|
return archivalStore.LinkCountOptions{
|
|
UserID: req.UserID,
|
|
CategoryID: req.CategoryID,
|
|
SearchQuery: req.SearchQuery,
|
|
}
|
|
}
|