mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
refactor(artwork): fold originals package into core/artwork as ImageStore
This commit is contained in:
parent
3e7685adc2
commit
b16ef725c9
@ -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() {
|
||||
@ -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 })
|
||||
@ -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")
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user