refactor(artwork): reuse auth.WithAdminUser and dedupe image cap guards

This commit is contained in:
Deluan 2026-07-22 18:02:54 -04:00
parent bba0eab3a5
commit b172ce4296
4 changed files with 38 additions and 39 deletions

View File

@ -9,9 +9,9 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/slice"
)
@ -35,20 +35,10 @@ func Fingerprint() string {
return hex.EncodeToString(sum[:])
}
// withAdminUser wraps ctx with the first admin so repos that apply a per-user visibility
// filter (playlists) expose private rows during this headless work; no admin yet -> unchanged.
func withAdminUser(ctx context.Context, ds model.DataStore) context.Context {
admin, err := ds.User(ctx).FindFirstAdmin()
if err != nil || admin == nil || admin.ID == "" {
return ctx
}
return request.WithUser(ctx, *admin)
}
// Backfill enqueues artwork resolution for every entity when the config fingerprint changed
// (or was never stored), artists first so those pages resolve before the larger backlog.
func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
ctx = withAdminUser(ctx, ds)
ctx = auth.WithAdminUser(ctx, ds)
current := Fingerprint()
props := ds.Property(ctx)
stored, err := props.DefaultGet(FingerprintPropertyKey, "")

View File

@ -71,13 +71,9 @@ func processItem(ctx context.Context, deps *workerDeps, item model.ArtworkQueueI
}
defer res.reader.Close()
data, err := io.ReadAll(io.LimitReader(res.reader, maxImageBytes+1))
data, err := readCapped(res.reader)
if err != nil {
log.Warn(ctx, "artwork: failed to read resolved image", "kind", item.ItemKind, "id", item.ItemID, err)
return outcomeFailed
}
if len(data) > maxImageBytes {
log.Warn(ctx, "artwork: resolved image exceeds size cap", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, "cap", maxImageBytes)
log.Warn(ctx, "artwork: failed to read resolved image", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, err)
return outcomeFailed
}
log.Debug(ctx, "artwork: read resolved image", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, "bytes", len(data))
@ -147,19 +143,41 @@ func writeAbsent(ctx context.Context, repo model.ArtworkRepository, item model.A
return outcomeAbsent
}
// decodeArtwork builds a new Artwork row from raw bytes: dimensions, mime and a
// blurhash computed from a downscaled thumbnail.
func decodeArtwork(ctx context.Context, hash string, data []byte) (*model.Artwork, error) {
// readCapped reads r, rejecting anything over maxImageBytes.
func readCapped(r io.Reader) ([]byte, error) {
data, err := io.ReadAll(io.LimitReader(r, maxImageBytes+1))
if err != nil {
return nil, err
}
if len(data) > maxImageBytes {
return nil, fmt.Errorf("image exceeds size cap %d", maxImageBytes)
}
return data, nil
}
// decodeCapped rejects declared dimensions over maxImagePixels BEFORE the
// full-decode allocation, then decodes.
func decodeCapped(data []byte) (image.Image, string, error) {
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("decode image config: %w", err)
return nil, "", fmt.Errorf("decode image config: %w", err)
}
if int64(cfg.Width)*int64(cfg.Height) > maxImagePixels {
return nil, fmt.Errorf("image dimensions %dx%d exceed pixel cap %d", cfg.Width, cfg.Height, maxImagePixels)
return nil, "", fmt.Errorf("image dimensions %dx%d exceed pixel cap %d", cfg.Width, cfg.Height, maxImagePixels)
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("decode image: %w", err)
return nil, "", fmt.Errorf("decode image: %w", err)
}
return img, format, nil
}
// decodeArtwork builds a new Artwork row from raw bytes: dimensions, mime and a
// blurhash computed from a downscaled thumbnail.
func decodeArtwork(ctx context.Context, hash string, data []byte) (*model.Artwork, error) {
img, format, err := decodeCapped(data)
if err != nil {
return nil, err
}
thumb := makeThumbnail(img, thumbnailSize)
@ -173,8 +191,8 @@ func decodeArtwork(ctx context.Context, hash string, data []byte) (*model.Artwor
return &model.Artwork{
Hash: hash,
Mime: mimeForFormat(format),
Width: cfg.Width,
Height: cfg.Height,
Width: img.Bounds().Dx(),
Height: img.Bounds().Dy(),
BlurHash: bh,
}, nil
}

View File

@ -392,21 +392,11 @@ func mtimeViaFS(fsys fs.FS, name string) int64 {
// decodeTile runs on every sampled album's resolved bytes before processItem's
// own maxImageBytes/maxImagePixels guards apply, so it enforces them itself too.
func decodeTile(r io.ReadCloser) (image.Image, error) {
data, err := io.ReadAll(io.LimitReader(r, maxImageBytes+1))
data, err := readCapped(r)
if err != nil {
return nil, err
}
if len(data) > maxImageBytes {
return nil, fmt.Errorf("tile image exceeds size cap %d", maxImageBytes)
}
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
if int64(cfg.Width)*int64(cfg.Height) > maxImagePixels {
return nil, fmt.Errorf("tile dimensions %dx%d exceed pixel cap %d", cfg.Width, cfg.Height, maxImagePixels)
}
img, _, err := image.Decode(bytes.NewReader(data))
img, _, err := decodeCapped(data)
if err != nil {
return nil, err
}

View File

@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/external"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"golang.org/x/time/rate"
@ -116,7 +117,7 @@ func (w *Worker) RunPrune(ctx context.Context) error {
func (w *Worker) drain(ctx context.Context, concurrency int) (int, error) {
// Resolved per drain, not once in Run: the worker starts at boot, possibly before any
// admin exists, so a late-created admin is picked up on the next poll (private playlists).
ctx = withAdminUser(ctx, w.deps.ds)
ctx = auth.WithAdminUser(ctx, w.deps.ds)
batch, err := w.deps.ds.ArtworkQueue(ctx).DequeueBatch(2 * concurrency)
if err != nil {
return 0, err