mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(artwork): make the artwork image size cap configurable (#5931)
* feat(artwork): make the artwork image size cap configurable Replace the hardcoded 20MB cap on resolved image reads with a new MaxImageSize config option. Load floors it at MaxImageUploadSize so an accepted upload can never be too large for the resolver to read back. * fix(conf): reject zero-valued byte-size options at startup ParseBytes accepts "0", but parseSize silently substitutes the default for it, so the accepted config would differ from the effective limit. * fix(conf): reject byte-size options that overflow int64 A raw value above math.MaxInt64 parses as a valid uint64 but wraps to a negative int64 in parseSize, giving readCapped a non-positive LimitReader bound so every artwork read comes back empty.
This commit is contained in:
parent
c4126fa674
commit
9e95b19a4f
8 changed files with 85 additions and 27 deletions
|
|
@ -13,6 +13,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/artwork/blurhash"
|
||||
"github.com/navidrome/navidrome/core/artwork/dominant"
|
||||
"github.com/navidrome/navidrome/core/artwork/thumbhash"
|
||||
|
|
@ -51,7 +53,9 @@ const thumbnailSize = 100
|
|||
|
||||
// maxImageBytes caps a resolved image read: a user-editable ExternalImageURL could point at
|
||||
// an arbitrarily large endpoint.
|
||||
const maxImageBytes = 20 << 20
|
||||
func maxImageBytes() int64 {
|
||||
return parseSize(conf.Server.MaxImageSize, consts.DefaultMaxImageSize)
|
||||
}
|
||||
|
||||
// maxImagePixels guards against decompression bombs: a tiny file can declare a canvas that
|
||||
// image.Decode would expand into gigabytes.
|
||||
|
|
@ -202,12 +206,13 @@ func writeAbsent(ctx context.Context, repo model.ArtworkRepository, item model.A
|
|||
}
|
||||
|
||||
func readCapped(r io.Reader) ([]byte, error) {
|
||||
data, err := io.ReadAll(io.LimitReader(r, maxImageBytes+1))
|
||||
limit := maxImageBytes()
|
||||
data, err := io.ReadAll(io.LimitReader(r, limit+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) > maxImageBytes {
|
||||
return nil, fmt.Errorf("image exceeds size cap %d", maxImageBytes)
|
||||
if int64(len(data)) > limit {
|
||||
return nil, fmt.Errorf("image exceeds size cap %d", limit)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
imgPath := filepath.Join(tmpDir, "artwork", "radio", "big_test.jpg")
|
||||
f, err := os.Create(imgPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(f.Truncate(maxImageBytes + 1)).To(Succeed())
|
||||
Expect(f.Truncate(maxImageBytes() + 1)).To(Succeed())
|
||||
Expect(f.Close()).To(Succeed())
|
||||
|
||||
radioRepo := tests.CreateMockedRadioRepo()
|
||||
|
|
@ -493,3 +493,14 @@ var _ = Describe("makeThumbnail", func() {
|
|||
Expect(thumb.Pix[3]).To(BeNumerically("==", 128))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("maxImageBytes", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
})
|
||||
|
||||
It("reads MaxImageSize from config", func() {
|
||||
conf.Server.MaxImageSize = "30MB"
|
||||
Expect(maxImageBytes()).To(Equal(int64(30_000_000)))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -625,7 +625,7 @@ var _ = Describe("decodeTile", func() {
|
|||
})
|
||||
|
||||
It("rejects a tile larger than the size cap", func() {
|
||||
data := bytes.Repeat([]byte{0}, maxImageBytes+1)
|
||||
data := bytes.Repeat([]byte{0}, int(maxImageBytes())+1)
|
||||
_, err := decodeTile(io.NopCloser(bytes.NewReader(data)))
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ import (
|
|||
|
||||
// MaxImageUploadSize returns the configured max upload size in bytes, or the built-in default.
|
||||
func MaxImageUploadSize() int64 {
|
||||
if size, err := humanize.ParseBytes(conf.Server.MaxImageUploadSize); err == nil && size > 0 {
|
||||
return parseSize(conf.Server.MaxImageUploadSize, consts.DefaultMaxImageUploadSize)
|
||||
}
|
||||
|
||||
func parseSize(value, fallback string) int64 {
|
||||
if size, err := humanize.ParseBytes(value); err == nil && size > 0 {
|
||||
return int64(size)
|
||||
}
|
||||
size, _ := humanize.ParseBytes(consts.DefaultMaxImageUploadSize)
|
||||
size, _ := humanize.ParseBytes(fallback)
|
||||
return int64(size)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue