mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-14 05:17:13 +02:00
feat(artwork): add artwork prune (orphan cleanup)
This commit is contained in:
parent
b72597821a
commit
db16b3de9a
3 changed files with 113 additions and 3 deletions
48
core/artwork/prune.go
Normal file
48
core/artwork/prune.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package artwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core/artwork/originals"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
repo := ds.Artwork(ctx)
|
||||
orphans, err := repo.GetOrphanHashes(time.Now().Add(-pruneMinAge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(orphans) > 0 {
|
||||
arts, err := repo.GetBatch(orphans)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repo.Delete(orphans...); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range arts {
|
||||
if err := store.Remove(a.Hash, a.Mime); err != nil {
|
||||
log.Warn(ctx, "Prune: could not remove artwork file", "hash", a.Hash, err)
|
||||
}
|
||||
}
|
||||
log.Info(ctx, "Prune: removed orphan artwork", "count", len(orphans))
|
||||
}
|
||||
|
||||
removed, err := store.Sweep(func(hash string) bool {
|
||||
_, err := repo.Get(hash)
|
||||
return err == nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if removed > 0 {
|
||||
log.Info(ctx, "Prune: swept stray artwork files", "count", removed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
61
core/artwork/prune_test.go
Normal file
61
core/artwork/prune_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package artwork
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core/artwork/originals"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Prune", func() {
|
||||
var ds *tests.MockDataStore
|
||||
var store *originals.Store
|
||||
var awRepo *tests.MockArtworkRepo
|
||||
|
||||
BeforeEach(func() {
|
||||
ds = &tests.MockDataStore{}
|
||||
awRepo = ds.Artwork(context.Background()).(*tests.MockArtworkRepo)
|
||||
store = originals.New(GinkgoT().TempDir())
|
||||
})
|
||||
|
||||
It("deletes orphan rows and their store files, keeps referenced ones", func() {
|
||||
data := []byte("orphan-bytes")
|
||||
h, _ := originals.Hash(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))
|
||||
Expect(store.Write(hk, "image/jpeg", bytes.NewReader(kept))).To(Succeed())
|
||||
Expect(awRepo.Put(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed())
|
||||
|
||||
Expect(Prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.Get(h)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = store.Open(h, "image/jpeg")
|
||||
Expect(os.IsNotExist(err)).To(BeTrue())
|
||||
rc, err := store.Open(hk, "image/jpeg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
rc.Close()
|
||||
})
|
||||
|
||||
It("sweeps store files that have no artwork row", func() {
|
||||
stray := []byte("no-row-bytes")
|
||||
h, _ := originals.Hash(bytes.NewReader(stray))
|
||||
Expect(store.Write(h, "image/jpeg", bytes.NewReader(stray))).To(Succeed())
|
||||
|
||||
Expect(Prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := store.Open(h, "image/jpeg")
|
||||
Expect(os.IsNotExist(err)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
|
@ -8,8 +8,9 @@ import (
|
|||
|
||||
type MockArtworkRepo struct {
|
||||
model.ArtworkRepository
|
||||
Data map[string]model.Artwork
|
||||
Err error
|
||||
Data map[string]model.Artwork
|
||||
OrphanHashes []string
|
||||
Err error
|
||||
}
|
||||
|
||||
func CreateMockArtworkRepo() *MockArtworkRepo {
|
||||
|
|
@ -51,7 +52,7 @@ func (m *MockArtworkRepo) GetBatch(hashes []string) (map[string]model.Artwork, e
|
|||
}
|
||||
|
||||
func (m *MockArtworkRepo) GetOrphanHashes(createdBefore time.Time) ([]string, error) {
|
||||
return nil, m.Err
|
||||
return m.OrphanHashes, m.Err
|
||||
}
|
||||
|
||||
func (m *MockArtworkRepo) Delete(hashes ...string) error {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue