hako/internal/archival/domain/link_service.go
2026-01-18 09:51:25 +01:00

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,
}
}