From 3a0190dff35e512a4392e52a8b2fbd92eb7da95b Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 27 Jul 2026 10:33:13 -0400 Subject: [PATCH] refactor(artwork): unexport artwork.HashImage function --- core/artwork/artwork.go | 2 +- core/artwork/artwork_test.go | 4 ++-- core/artwork/image_store.go | 16 ++++++++-------- core/artwork/image_store_test.go | 26 +++++++++++++------------- core/artwork/processor.go | 2 +- core/artwork/prune_test.go | 20 ++++++++++---------- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 280a49619..6e3f54185 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -225,7 +225,7 @@ func (s *service) serveResolution(ctx context.Context, res resolution, size int, if err != nil { return nil, ErrUnavailable } - hash, err := HashImage(bytes.NewReader(data)) + hash, err := hashImage(bytes.NewReader(data)) if err != nil { return nil, ErrUnavailable } diff --git a/core/artwork/artwork_test.go b/core/artwork/artwork_test.go index 1807194a7..37c385aff 100644 --- a/core/artwork/artwork_test.go +++ b/core/artwork/artwork_test.go @@ -43,7 +43,7 @@ var _ = Describe("Artwork", func() { // seedFoundStore installs a store-backed found state (bytes in the content-addressed // store, no backing file) and returns the hash. seedFoundStore := func(kind, id string, imgBytes []byte) string { - hash, err := HashImage(bytes.NewReader(imgBytes)) + hash, err := hashImage(bytes.NewReader(imgBytes)) Expect(err).ToNot(HaveOccurred()) Expect(store.Write(hash, "image/jpeg", bytes.NewReader(imgBytes))).To(Succeed()) Expect(artRepo.PutImage(&model.Artwork{Hash: hash, Mime: "image/jpeg"})).To(Succeed()) @@ -116,7 +116,7 @@ var _ = Describe("Artwork", func() { // Delete the store file: a warm resize-cache entry must keep serving without // ever touching the original (the stale-serve self-heal). - hash, _ := HashImage(bytes.NewReader(coverBytes)) + hash, _ := hashImage(bytes.NewReader(coverBytes)) Expect(store.Remove(hash, "image/jpeg", time.Now().Add(time.Hour))).To(Succeed()) Eventually(func(g Gomega) { img2, err := svc.Get(ctx, model.MustParseArtworkID("al-al1"), 100, false) diff --git a/core/artwork/image_store.go b/core/artwork/image_store.go index 684426e18..bfbc5a9bc 100644 --- a/core/artwork/image_store.go +++ b/core/artwork/image_store.go @@ -16,14 +16,6 @@ import ( "github.com/zeebo/xxh3" ) -func HashImage(r io.Reader) (string, error) { - d := xxh3.New() - if _, err := io.Copy(d, r); err != nil { - return "", err - } - return fmt.Sprintf("%016x", d.Sum64()), nil -} - // ImageStore is the content-addressed store for artwork images that have no // library file backing them (external downloads, embedded extractions, generated). type ImageStore struct { @@ -56,6 +48,14 @@ func extForMime(m string) string { return ".img" } +func hashImage(r io.Reader) (string, error) { + d := xxh3.New() + if _, err := io.Copy(d, r); err != nil { + return "", err + } + return fmt.Sprintf("%016x", d.Sum64()), nil +} + // validHash rejects anything but 16 lowercase hex chars: known-absent states carry "", // and malformed persisted hashes must never reach path sharding (slice panics, separators). func validHash(hash string) bool { diff --git a/core/artwork/image_store_test.go b/core/artwork/image_store_test.go index 80c8a14f9..c2cca399d 100644 --- a/core/artwork/image_store_test.go +++ b/core/artwork/image_store_test.go @@ -24,18 +24,18 @@ var _ = Describe("ImageStore", func() { }) It("hashes deterministically", func() { - h1, err := HashImage(bytes.NewReader([]byte("some image bytes"))) + h1, err := hashImage(bytes.NewReader([]byte("some image bytes"))) Expect(err).ToNot(HaveOccurred()) - h2, _ := HashImage(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, _ := HashImage(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, _ := HashImage(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()) @@ -49,7 +49,7 @@ var _ = Describe("ImageStore", func() { It("is idempotent on duplicate writes and preserves the original content", func() { data := []byte("dup") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) // A duplicate write only touches mtime; passing different bytes under the same // hash proves the second reader is never consumed to overwrite the file. @@ -65,7 +65,7 @@ var _ = Describe("ImageStore", func() { It("refreshes the mtime on a duplicate write", func() { data := []byte("touch-me") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) Expect(os.Chtimes(store.path(h, "image/png"), old, old)).To(Succeed()) @@ -79,7 +79,7 @@ var _ = Describe("ImageStore", func() { It("rewrites the bytes when the existing file vanished before the liveness touch", func() { data := []byte("vanishing") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) for range 10 { Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) Expect(os.Remove(store.path(h, "image/png"))).To(Succeed()) @@ -113,11 +113,11 @@ var _ = Describe("ImageStore", func() { It("spares a file newer than the cutoff, removes an aged one", func() { fresh := []byte("fresh") - hf, _ := HashImage(bytes.NewReader(fresh)) + hf, _ := hashImage(bytes.NewReader(fresh)) Expect(store.Write(hf, "image/jpeg", bytes.NewReader(fresh))).To(Succeed()) aged := []byte("aged") - ha, _ := HashImage(bytes.NewReader(aged)) + ha, _ := hashImage(bytes.NewReader(aged)) Expect(store.Write(ha, "image/jpeg", bytes.NewReader(aged))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) Expect(os.Chtimes(store.path(ha, "image/jpeg"), old, old)).To(Succeed()) @@ -135,10 +135,10 @@ var _ = Describe("ImageStore", func() { It("sweeps unknown files, keeps known ones", func() { d1 := []byte("keep-me") - h1, _ := HashImage(bytes.NewReader(d1)) + h1, _ := hashImage(bytes.NewReader(d1)) Expect(store.Write(h1, "image/jpeg", bytes.NewReader(d1))).To(Succeed()) d2 := []byte("orphan") - h2, _ := HashImage(bytes.NewReader(d2)) + h2, _ := hashImage(bytes.NewReader(d2)) Expect(store.Write(h2, "image/jpeg", bytes.NewReader(d2))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) @@ -156,7 +156,7 @@ var _ = Describe("ImageStore", func() { It("sweeps a stale mime variant of a known hash, keeps the current one", func() { data := []byte("same-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) @@ -178,7 +178,7 @@ var _ = Describe("ImageStore", func() { It("keeps young unknown files inside the grace window", func() { d := []byte("fresh-orphan") - h, _ := HashImage(bytes.NewReader(d)) + h, _ := hashImage(bytes.NewReader(d)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(d))).To(Succeed()) removed, err := store.Sweep(ctx, time.Now().Add(-time.Hour), func(string, string) bool { return false }) diff --git a/core/artwork/processor.go b/core/artwork/processor.go index 33451ab90..784921ec2 100644 --- a/core/artwork/processor.go +++ b/core/artwork/processor.go @@ -86,7 +86,7 @@ func (p *processor) acquire(ctx context.Context, item model.ArtworkQueueItem) (o } log.Debug(ctx, "Artwork: Read resolved image", "kind", item.ItemKind, "id", item.ItemID, "source", res.source, "bytes", len(data)) - hash, err := HashImage(bytes.NewReader(data)) + hash, err := hashImage(bytes.NewReader(data)) if err != nil { log.Warn(ctx, "Artwork: Failed to hash image", "kind", item.ItemKind, "id", item.ItemID, err) return outcomeFailed, nil diff --git a/core/artwork/prune_test.go b/core/artwork/prune_test.go index 832f5456a..4afdc6ba9 100644 --- a/core/artwork/prune_test.go +++ b/core/artwork/prune_test.go @@ -76,7 +76,7 @@ var _ = Describe("Prune", func() { It("deletes orphan rows and their store files, keeps referenced ones", func() { data := []byte("orphan-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) Expect(os.Chtimes(store.path(h, "image/jpeg"), old, old)).To(Succeed()) @@ -85,7 +85,7 @@ var _ = Describe("Prune", func() { awRepo.OrphanHashes = []string{h} kept := []byte("kept-bytes") - hk, _ := HashImage(bytes.NewReader(kept)) + hk, _ := hashImage(bytes.NewReader(kept)) Expect(store.Write(hk, "image/jpeg", bytes.NewReader(kept))).To(Succeed()) Expect(awRepo.PutImage(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed()) @@ -102,7 +102,7 @@ var _ = Describe("Prune", func() { It("spares a candidate reacquired between snapshot and delete", func() { data := []byte("reacquired-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed()) ageArtwork(h, time.Now().Add(-2*time.Hour)) @@ -122,7 +122,7 @@ var _ = Describe("Prune", func() { It("spares a candidate whose row was freshly recreated (created_at inside the grace window)", func() { data := []byte("fresh-reacquired-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) // Reacquisition refreshed created_at after the snapshot; still unreferenced. Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed()) @@ -139,7 +139,7 @@ var _ = Describe("Prune", func() { It("spares an orphan file freshly touched by an overlapping acquisition", func() { data := []byte("racing-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed()) ageArtwork(h, time.Now().Add(-2*time.Hour)) @@ -156,7 +156,7 @@ var _ = Describe("Prune", func() { It("sweeps store files that have no artwork row", func() { stray := []byte("no-row-bytes") - h, _ := HashImage(bytes.NewReader(stray)) + h, _ := hashImage(bytes.NewReader(stray)) Expect(store.Write(h, "image/jpeg", bytes.NewReader(stray))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) Expect(os.Chtimes(store.path(h, "image/jpeg"), old, old)).To(Succeed()) @@ -169,7 +169,7 @@ var _ = Describe("Prune", func() { It("sweeps an obsolete mime variant of a reacquired hash", func() { data := []byte("variant-bytes") - h, _ := HashImage(bytes.NewReader(data)) + h, _ := hashImage(bytes.NewReader(data)) Expect(store.Write(h, "image/png", bytes.NewReader(data))).To(Succeed()) Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed()) old := time.Now().Add(-2 * time.Hour) @@ -195,14 +195,14 @@ var _ = Describe("Prune", func() { old := time.Now().Add(-2 * time.Hour) blocked := []byte("blocked-bytes") - hb, _ := HashImage(bytes.NewReader(blocked)) + hb, _ := hashImage(bytes.NewReader(blocked)) Expect(store.Write(hb, "image/jpeg", bytes.NewReader(blocked))).To(Succeed()) Expect(os.Chtimes(store.path(hb, "image/jpeg"), old, old)).To(Succeed()) Expect(awRepo.PutImage(&model.Artwork{Hash: hb, Mime: "image/jpeg"})).To(Succeed()) ageArtwork(hb, old) good := []byte("good-bytes") - hg, _ := HashImage(bytes.NewReader(good)) + hg, _ := hashImage(bytes.NewReader(good)) Expect(store.Write(hg, "image/jpeg", bytes.NewReader(good))).To(Succeed()) Expect(os.Chtimes(store.path(hg, "image/jpeg"), old, old)).To(Succeed()) Expect(awRepo.PutImage(&model.Artwork{Hash: hg, Mime: "image/jpeg"})).To(Succeed()) @@ -242,7 +242,7 @@ var _ = Describe("Prune", func() { ds.MockedArtwork = &flakyGetArtworkRepo{MockArtworkRepo: tests.CreateMockArtworkRepo()} data := []byte("live-bytes") - h, _ := HashImage(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)).ToNot(Succeed())