Files
silo-server/internal/taskmanager/tasks/cleanup_artwork_revisions.go
1664c60425 fix(metadata): publish artwork revisions atomically (#399)
* fix(metadata): publish artwork revisions atomically

* fix(metadata): harden artwork revision cleanup

* fix(metadata): address artwork revision review findings

- restore image applies for all media_items types and reject unsupported
  target/image combinations with 400 before uploading; episodes coerce to
  stills and the web dialog no longer offers image tabs episodes can't use
- add WHEN clauses to displacement triggers and hoist to_jsonb so bulk
  catalog upserts that assign unchanged artwork columns skip the trigger
- make artworkkey the single variant-ladder owner: imagecache derives its
  widths from it and triggers store image_type instead of hardcoded
  variant arrays, expanded by the collector at deletion time
- sweep dormant registry rows periodically so references lost through
  untriggered surfaces degrade to slow cleanup instead of leaking
- park just-published revisions dormant, keep dormant rows dormant on
  re-cache, and batch the GC reference pre-check per run
- heal rows re-referencing a just-deleted revision via reconciler-style
  resets after the deletion commits
- share a per-URL image-loaded hook across DetailHero, ItemCard,
  SectionItemCard, GlobalSearch, and CollectionPosterCard
- deduplicate Cache/CacheBytes finalization and drop unused VariantPaths
  plumbing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(catalog): cast reused timestamp parameter in revision upsert

Postgres cannot deduce one type for $3 used both as a plain value and
inside a CASE arm; the dev deploy surfaced it as SQLSTATE 42P08 on every
publication. Cast both uses and cover the arm/park/track upserts with
database-backed tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(metadata): address artwork revision review comments

- keep a durable heal path: deletion marks deleted_at instead of removing
  the registry row, so a failed post-delete heal retries with backoff and
  broken references never park; trackers clear the marker on re-upload
- never treat bare existence as an immutable-content match; backends
  without content verification rewrite the object
- exercise revisioned cover keys in scanner/enrichment fakes, compare the
  tracked manifest exactly, and honor cancellation in the blocking test
  deleter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:33:21 -04:00

57 lines
1.9 KiB
Go

package tasks
import (
"context"
"fmt"
"time"
"github.com/Silo-Server/silo-server/internal/metadata"
"github.com/Silo-Server/silo-server/internal/taskmanager"
)
type ArtworkRevisionGCRunner interface {
Run(ctx context.Context) (metadata.ArtworkRevisionGCStats, error)
}
type CleanupArtworkRevisionsTask struct {
runner ArtworkRevisionGCRunner
}
func NewCleanupArtworkRevisionsTask(runner ArtworkRevisionGCRunner) *CleanupArtworkRevisionsTask {
return &CleanupArtworkRevisionsTask{runner: runner}
}
func (t *CleanupArtworkRevisionsTask) Key() string { return "cleanup_artwork_revisions" }
func (t *CleanupArtworkRevisionsTask) Name() string { return "Clean Artwork Revisions" }
func (t *CleanupArtworkRevisionsTask) Description() string {
return "Deletes unpublished or displaced immutable artwork revisions after a grace period when no catalog record references them."
}
func (t *CleanupArtworkRevisionsTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategoryMetadata
}
func (t *CleanupArtworkRevisionsTask) IsHidden() bool { return false }
func (t *CleanupArtworkRevisionsTask) DefaultTriggers() []taskmanager.TriggerConfig {
return []taskmanager.TriggerConfig{
{Type: taskmanager.TriggerTypeStartup},
{Type: taskmanager.TriggerTypeInterval, IntervalMs: int64(time.Hour / time.Millisecond)},
}
}
func (t *CleanupArtworkRevisionsTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
if t == nil || t.runner == nil {
progress.Report(100, "Artwork revision cleanup is not configured")
return nil
}
progress.Report(0, "Checking displaced artwork revisions")
stats, err := t.runner.Run(ctx)
progress.SetResultData(stats.JSON())
if err != nil {
return fmt.Errorf("cleaning artwork revisions: %w", err)
}
progress.Report(100, fmt.Sprintf(
"Processed %d revisions: deleted %d, retained %d referenced, scheduled %d retries",
stats.Claimed, stats.Deleted, stats.Referenced, stats.Retried,
))
return nil
}