From b16ef725c90bb40bdf69a8dc7229fc1c47314450 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 21 Jul 2026 23:09:31 -0400 Subject: [PATCH] refactor(artwork): fold originals package into core/artwork as ImageStore --- .../originals.go => image_store.go} | 24 +++++++++---------- .../originals_test.go => image_store_test.go} | 23 +++++++++--------- .../artwork/originals/originals_suite_test.go | 17 ------------- core/artwork/prune.go | 3 +-- core/artwork/prune_test.go | 13 +++++----- 5 files changed, 30 insertions(+), 50 deletions(-) rename core/artwork/{originals/originals.go => image_store.go} (74%) rename core/artwork/{originals/originals_test.go => image_store_test.go} (76%) delete mode 100644 core/artwork/originals/originals_suite_test.go diff --git a/core/artwork/originals/originals.go b/core/artwork/image_store.go similarity index 74% rename from core/artwork/originals/originals.go rename to core/artwork/image_store.go index 12cfe56cc..c98064d13 100644 --- a/core/artwork/originals/originals.go +++ b/core/artwork/image_store.go @@ -1,6 +1,4 @@ -// Package originals is the content-addressed store for artwork images that have -// no library file backing them (external downloads, embedded extractions, generated). -package originals +package artwork import ( "errors" @@ -14,7 +12,7 @@ import ( "github.com/zeebo/xxh3" ) -func Hash(r io.Reader) (string, error) { +func HashImage(r io.Reader) (string, error) { d := xxh3.New() if _, err := io.Copy(d, r); err != nil { return "", err @@ -22,12 +20,14 @@ func Hash(r io.Reader) (string, error) { return fmt.Sprintf("%016x", d.Sum64()), nil } -type Store struct { +// ImageStore is the content-addressed store for artwork images that have no +// library file backing them (external downloads, embedded extractions, generated). +type ImageStore struct { root string } -func New(rootDir string) *Store { - return &Store{root: rootDir} +func NewImageStore(rootDir string) *ImageStore { + return &ImageStore{root: rootDir} } // extForMime is deliberately NOT mime.ExtensionsByType: extensions are baked into @@ -46,11 +46,11 @@ func extForMime(m string) string { return ".img" } -func (s *Store) path(hash, mimeType string) string { +func (s *ImageStore) path(hash, mimeType string) string { return filepath.Join(s.root, hash[0:2], hash[2:4], hash+extForMime(mimeType)) } -func (s *Store) Write(hash, mimeType string, r io.Reader) error { +func (s *ImageStore) Write(hash, mimeType string, r io.Reader) error { dst := s.path(hash, mimeType) if _, err := os.Stat(dst); err == nil { return nil @@ -73,11 +73,11 @@ func (s *Store) Write(hash, mimeType string, r io.Reader) error { return os.Rename(tmp.Name(), dst) } -func (s *Store) Open(hash, mimeType string) (io.ReadCloser, error) { +func (s *ImageStore) Open(hash, mimeType string) (io.ReadCloser, error) { return os.Open(s.path(hash, mimeType)) } -func (s *Store) Remove(hash, mimeType string) error { +func (s *ImageStore) Remove(hash, mimeType string) error { err := os.Remove(s.path(hash, mimeType)) if errors.Is(err, fs.ErrNotExist) { return nil @@ -85,7 +85,7 @@ func (s *Store) Remove(hash, mimeType string) error { return err } -func (s *Store) Sweep(keep func(hash string) bool) (int, error) { +func (s *ImageStore) Sweep(keep func(hash string) bool) (int, error) { removed := 0 err := filepath.WalkDir(s.root, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { diff --git a/core/artwork/originals/originals_test.go b/core/artwork/image_store_test.go similarity index 76% rename from core/artwork/originals/originals_test.go rename to core/artwork/image_store_test.go index caa861a6f..c3ca69a58 100644 --- a/core/artwork/originals/originals_test.go +++ b/core/artwork/image_store_test.go @@ -1,4 +1,4 @@ -package originals_test +package artwork import ( "bytes" @@ -6,33 +6,32 @@ import ( "os" "path/filepath" - "github.com/navidrome/navidrome/core/artwork/originals" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var _ = Describe("Originals Store", func() { - var store *originals.Store +var _ = Describe("ImageStore", func() { + var store *ImageStore var root string BeforeEach(func() { root = GinkgoT().TempDir() - store = originals.New(root) + store = NewImageStore(root) }) It("hashes deterministically", func() { - h1, err := originals.Hash(bytes.NewReader([]byte("some image bytes"))) + h1, err := HashImage(bytes.NewReader([]byte("some image bytes"))) Expect(err).ToNot(HaveOccurred()) - h2, _ := originals.Hash(bytes.NewReader([]byte("some image bytes"))) + h2, _ := HashImage(bytes.NewReader([]byte("some image bytes"))) Expect(h1).To(Equal(h2)) Expect(h1).To(HaveLen(16)) - h3, _ := originals.Hash(bytes.NewReader([]byte("other bytes"))) + h3, _ := HashImage(bytes.NewReader([]byte("other bytes"))) Expect(h3).ToNot(Equal(h1)) }) It("writes sharded and reads back", func() { data := []byte("jpeg-bytes") - h, _ := originals.Hash(bytes.NewReader(data)) + h, _ := HashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) Expect(filepath.Join(root, h[0:2], h[2:4], h+".jpg")).To(BeAnExistingFile()) @@ -46,7 +45,7 @@ var _ = Describe("Originals Store", func() { It("is idempotent on duplicate writes", func() { data := []byte("dup") - h, _ := originals.Hash(bytes.NewReader(data)) + h, _ := HashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) }) @@ -62,10 +61,10 @@ var _ = Describe("Originals Store", func() { It("sweeps unknown files, keeps known ones", func() { d1 := []byte("keep-me") - h1, _ := originals.Hash(bytes.NewReader(d1)) + h1, _ := HashImage(bytes.NewReader(d1)) Expect(store.Write(h1, "image/jpeg", bytes.NewReader(d1))).To(Succeed()) d2 := []byte("orphan") - h2, _ := originals.Hash(bytes.NewReader(d2)) + h2, _ := HashImage(bytes.NewReader(d2)) Expect(store.Write(h2, "image/jpeg", bytes.NewReader(d2))).To(Succeed()) removed, err := store.Sweep(func(h string) bool { return h == h1 }) diff --git a/core/artwork/originals/originals_suite_test.go b/core/artwork/originals/originals_suite_test.go deleted file mode 100644 index 2686f9ec9..000000000 --- a/core/artwork/originals/originals_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package originals_test - -import ( - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestOriginals(t *testing.T) { - tests.Init(t, false) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "Originals Store Suite") -} diff --git a/core/artwork/prune.go b/core/artwork/prune.go index 931990056..9c88611a3 100644 --- a/core/artwork/prune.go +++ b/core/artwork/prune.go @@ -5,7 +5,6 @@ import ( "errors" "time" - "github.com/navidrome/navidrome/core/artwork/originals" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" ) @@ -13,7 +12,7 @@ import ( // pruneMinAge guards the window between artwork insert and item_artwork upsert. const pruneMinAge = time.Hour -func Prune(ctx context.Context, ds model.DataStore, store *originals.Store) error { +func Prune(ctx context.Context, ds model.DataStore, store *ImageStore) error { repo := ds.Artwork(ctx) orphans, err := repo.GetOrphanHashes(time.Now().Add(-pruneMinAge)) if err != nil { diff --git a/core/artwork/prune_test.go b/core/artwork/prune_test.go index 54564a170..925e46fd1 100644 --- a/core/artwork/prune_test.go +++ b/core/artwork/prune_test.go @@ -7,7 +7,6 @@ import ( "os" "time" - "github.com/navidrome/navidrome/core/artwork/originals" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" @@ -24,25 +23,25 @@ func (f *flakyGetArtworkRepo) Get(string) (*model.Artwork, error) { var _ = Describe("Prune", func() { var ds *tests.MockDataStore - var store *originals.Store + var store *ImageStore var awRepo *tests.MockArtworkRepo BeforeEach(func() { ds = &tests.MockDataStore{} awRepo = ds.Artwork(context.Background()).(*tests.MockArtworkRepo) - store = originals.New(GinkgoT().TempDir()) + store = NewImageStore(GinkgoT().TempDir()) }) It("deletes orphan rows and their store files, keeps referenced ones", func() { data := []byte("orphan-bytes") - h, _ := originals.Hash(bytes.NewReader(data)) + h, _ := HashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) Expect(awRepo.Put(&model.Artwork{Hash: h, Mime: "image/jpeg", CreatedAt: time.Now().Add(-2 * time.Hour)})).To(Succeed()) awRepo.OrphanHashes = []string{h} kept := []byte("kept-bytes") - hk, _ := originals.Hash(bytes.NewReader(kept)) + hk, _ := HashImage(bytes.NewReader(kept)) Expect(store.Write(hk, "image/jpeg", bytes.NewReader(kept))).To(Succeed()) Expect(awRepo.Put(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed()) @@ -59,7 +58,7 @@ var _ = Describe("Prune", func() { It("sweeps store files that have no artwork row", func() { stray := []byte("no-row-bytes") - h, _ := originals.Hash(bytes.NewReader(stray)) + h, _ := HashImage(bytes.NewReader(stray)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(stray))).To(Succeed()) Expect(Prune(context.Background(), ds, store)).To(Succeed()) @@ -72,7 +71,7 @@ var _ = Describe("Prune", func() { ds.MockedArtwork = &flakyGetArtworkRepo{MockArtworkRepo: tests.CreateMockArtworkRepo()} data := []byte("live-bytes") - h, _ := originals.Hash(bytes.NewReader(data)) + h, _ := HashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) Expect(Prune(context.Background(), ds, store)).To(Succeed())