feat(backend): immutable cache dir scheme + stale sweep
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
package bookfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DirKey(id int64, hash string) string { return fmt.Sprintf("%d-%s", id, hash) }
|
||||||
|
|
||||||
|
func CoverDir(cacheDir, key string) string { return filepath.Join(cacheDir, "covers", key) }
|
||||||
|
func PagesDir(cacheDir, key string) string { return filepath.Join(cacheDir, "pages", key) }
|
||||||
|
|
||||||
|
func SweepStale(cacheDir string, live map[string]bool) (int, error) {
|
||||||
|
removed := 0
|
||||||
|
for _, kind := range []string{"covers", "pages"} {
|
||||||
|
entries, err := os.ReadDir(filepath.Join(cacheDir, kind))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
continue // 首次运行还没有该层目录,不算错
|
||||||
|
}
|
||||||
|
return removed, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() || live[e.Name()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(filepath.Join(cacheDir, kind, e.Name())); err != nil {
|
||||||
|
return removed, err
|
||||||
|
}
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package bookfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mk(t *testing.T, cacheDir, kind, key, file string) {
|
||||||
|
t.Helper()
|
||||||
|
d := filepath.Join(cacheDir, kind, key)
|
||||||
|
os.MkdirAll(d, 0o755)
|
||||||
|
os.WriteFile(filepath.Join(d, file), []byte("x"), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSweepStale(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
mk(t, dir, "covers", "1-aaa", "cover.jpg")
|
||||||
|
mk(t, dir, "covers", "2-bbb", "cover.png")
|
||||||
|
mk(t, dir, "pages", "1-aaa", "0.jpg")
|
||||||
|
mk(t, dir, "pages", "3-ccc", "0.jpg")
|
||||||
|
os.WriteFile(filepath.Join(dir, "covers", "stray.txt"), []byte("x"), 0o644) // 非目录,忽略
|
||||||
|
|
||||||
|
n, err := SweepStale(dir, map[string]bool{"1-aaa": true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 2 {
|
||||||
|
t.Fatalf("removed %d want 2", n)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "covers", "1-aaa")); err != nil {
|
||||||
|
t.Fatal("live dir removed")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "pages", "1-aaa")); err != nil {
|
||||||
|
t.Fatal("live pages removed")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "covers", "2-bbb")); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("stale cover remains")
|
||||||
|
}
|
||||||
|
// cacheDir 不存在 = 首次运行,不算错
|
||||||
|
if n, err := SweepStale(filepath.Join(dir, "nope"), nil); n != 0 || err != nil {
|
||||||
|
t.Fatalf("%d %v", n, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPaths(t *testing.T) {
|
||||||
|
if DirKey(12, "ab") != "12-ab" {
|
||||||
|
t.Fatal("DirKey")
|
||||||
|
}
|
||||||
|
if CoverDir("/c", "12-ab") != filepath.Join("/c", "covers", "12-ab") {
|
||||||
|
t.Fatal("CoverDir")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,6 +262,24 @@ func (s *Store) ListBooks(ctx context.Context, libID int64, q, prefix string, us
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) BookHashes(ctx context.Context) (map[int64][2]int64, error) {
|
||||||
|
rows, err := s.P.Query(ctx, "SELECT id, file_size, mod_ts FROM books")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := map[int64][2]int64{}
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
var v [2]int64
|
||||||
|
if err := rows.Scan(&id, &v[0], &v[1]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out[id] = v
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- progress ----------
|
// ---------- progress ----------
|
||||||
|
|
||||||
func (s *Store) UpsertProgress(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64) error {
|
func (s *Store) UpsertProgress(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64) error {
|
||||||
|
|||||||
@@ -77,6 +77,20 @@ func TestBooksDiffLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBookHashes(t *testing.T) {
|
||||||
|
s := setup(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
libID, _ := s.CreateLibrary(ctx, "l", "/data/books/l")
|
||||||
|
bid, _ := s.InsertBook(ctx, libID, "a.cbz", "a", "cbz", 100, 2000, 1)
|
||||||
|
m, err := s.BookHashes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v := m[bid]; v[0] != 100 || v[1] != 2000 {
|
||||||
|
t.Fatalf("bad hashes %+v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProgressUpsertAndJoin(t *testing.T) {
|
func TestProgressUpsertAndJoin(t *testing.T) {
|
||||||
s := setup(t)
|
s := setup(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
Reference in New Issue
Block a user