diff --git a/backend/internal/bookfile/cache.go b/backend/internal/bookfile/cache.go new file mode 100644 index 0000000..7667a4c --- /dev/null +++ b/backend/internal/bookfile/cache.go @@ -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 +} diff --git a/backend/internal/bookfile/cache_test.go b/backend/internal/bookfile/cache_test.go new file mode 100644 index 0000000..72b98bf --- /dev/null +++ b/backend/internal/bookfile/cache_test.go @@ -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") + } +} diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 054ba7b..4b98dbe 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -262,6 +262,24 @@ func (s *Store) ListBooks(ctx context.Context, libID int64, q, prefix string, us 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 ---------- func (s *Store) UpsertProgress(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64) error { diff --git a/backend/internal/store/store_test.go b/backend/internal/store/store_test.go index c5633e3..b91dda5 100644 --- a/backend/internal/store/store_test.go +++ b/backend/internal/store/store_test.go @@ -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) { s := setup(t) ctx := context.Background()