fix(server): clean up uploaded artist images during GC

When artists are purged during garbage collection, any custom uploaded
cover images were left orphaned on disk. Modified purgeEmpty() to query
for uploaded_image filenames before the bulk DELETE, then remove the
corresponding files from disk afterwards. Image cleanup is best-effort
to avoid failing the GC if a file is already missing or inaccessible.

Also populated album_artists entries in the persistence test suite setup
to reflect the actual album-artist relationships from test data, ensuring
purgeEmpty() doesn't inadvertently delete shared test artists.
This commit is contained in:
Deluan
2026-03-17 19:39:00 -04:00
parent ad92b752be
commit b013b71ba9
3 changed files with 137 additions and 1 deletions
+29 -1
View File
@@ -4,7 +4,9 @@ import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"slices"
"strings"
"time"
@@ -12,6 +14,7 @@ import (
. "github.com/Masterminds/squirrel"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
@@ -315,7 +318,19 @@ func (r *artistRepository) GetIndex(includeMissing bool, libraryIds []int, roles
}
func (r *artistRepository) purgeEmpty() error {
del := Delete(r.tableName).Where("id not in (select artist_id from album_artists)")
orphanFilter := "id not in (select artist_id from album_artists)"
// Collect uploaded image filenames before deleting
sel := Select("uploaded_image").From(r.tableName).
Where(orphanFilter).
Where("uploaded_image != ''")
var imageFiles []string
if err := r.queryAllSlice(sel, &imageFiles); err != nil && !errors.Is(err, model.ErrNotFound) {
return fmt.Errorf("collecting artist images for cleanup: %w", err)
}
// Delete orphan artists
del := Delete(r.tableName).Where(orphanFilter)
c, err := r.executeSQL(del)
if err != nil {
return fmt.Errorf("purging empty artists: %w", err)
@@ -323,6 +338,19 @@ func (r *artistRepository) purgeEmpty() error {
if c > 0 {
log.Debug(r.ctx, "Purged empty artists", "totalDeleted", c)
}
if len(imageFiles) == 0 {
return nil
}
// Best-effort cleanup of uploaded image files
log.Debug(r.ctx, "Cleaning up artist images", "totalImages", len(imageFiles))
for _, filename := range imageFiles {
path := model.UploadedImagePath(consts.EntityArtist, filename)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
log.Warn(r.ctx, "Failed to remove artist image during GC", "path", path, err)
}
}
return nil
}