refactor(artwork): unexport artwork.HashImage function

This commit is contained in:
Deluan 2026-07-27 10:33:13 -04:00
parent fa879e1576
commit 3a0190dff3
6 changed files with 35 additions and 35 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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 })

View File

@ -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

View File

@ -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())