mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
refactor(artwork): reuse auth.WithAdminUser and dedupe image cap guards
This commit is contained in:
parent
bba0eab3a5
commit
b172ce4296
@ -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, "")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user