mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Queue rows for deleted entities failed forever (Get -> ErrNotFound -> failed -> capped retries, unbounded). Add ArtworkQueueRepository.PurgeDangling, called from Prune next to the item_artwork purge. Separately, the found/absent path unconditionally deleted the dequeued row, erasing a concurrent scan re-enqueue; switch to DeleteIfUnchanged, which deletes only while retry_at still matches the dequeued value (verified retry_at is the column an Enqueue upsert resets).
87 lines
2.4 KiB
Go
87 lines
2.4 KiB
Go
package artwork
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
// pruneMinAge guards the window between artwork insert and item_artwork upsert.
|
|
const pruneMinAge = time.Hour
|
|
|
|
func Prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
|
|
repo := ds.Artwork(ctx)
|
|
|
|
purged, err := repo.PurgeDanglingItemArtwork()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if purged > 0 {
|
|
log.Info(ctx, "Prune: purged dangling item artwork state", "count", purged)
|
|
}
|
|
|
|
// Queue rows for deleted entities would otherwise retry forever (Get -> not found -> failed).
|
|
queuePurged, err := ds.ArtworkQueue(ctx).PurgeDangling()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if queuePurged > 0 {
|
|
log.Info(ctx, "Prune: purged dangling artwork queue rows", "count", queuePurged)
|
|
}
|
|
|
|
// One grace cutoff for both the DB orphan check and the file sweep: files younger
|
|
// than the window may belong to acquisitions whose rows aren't committed yet.
|
|
cutoff := time.Now().Add(-pruneMinAge)
|
|
candidates, err := repo.GetOrphanHashes(cutoff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(candidates) > 0 {
|
|
arts, err := repo.GetImages(candidates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := repo.DeleteOrphans(cutoff, candidates); err != nil {
|
|
return err
|
|
}
|
|
// DeleteOrphans may spare candidates reacquired since the snapshot; only remove files
|
|
// for rows actually gone (absent from the post-delete re-read).
|
|
survivors, err := repo.GetImages(candidates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
removed := 0
|
|
for _, h := range candidates {
|
|
if _, ok := survivors[h]; ok {
|
|
continue
|
|
}
|
|
// A spared fresh file is at worst a stray a later sweep reclaims;
|
|
// Worker.RunPrune serializes prune against in-flight acquisitions.
|
|
if err := store.Remove(h, arts[h].Mime, cutoff); err != nil {
|
|
log.Warn(ctx, "Prune: could not remove artwork file", "hash", h, err)
|
|
}
|
|
removed++
|
|
}
|
|
log.Info(ctx, "Prune: removed orphan artwork", "count", removed)
|
|
}
|
|
|
|
mimes, err := repo.GetAllMimes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
removed, err := store.Sweep(cutoff, func(hash, ext string) bool {
|
|
// A known hash under a stale extension is a superseded mime variant — reclaim it.
|
|
m, ok := mimes[hash]
|
|
return ok && ext == extForMime(m)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if removed > 0 {
|
|
log.Info(ctx, "Prune: swept stray artwork files", "count", removed)
|
|
}
|
|
return nil
|
|
}
|