Space keeps its scans and refreshes them: zfs diff, folder mtimes, resume after cancel
A scan is saved beside the config and read back after a restart. The next scan starts from it: on a pool with snapshots zfs diff names what changed and only those folders are read; elsewhere a folder whose mtime is unchanged keeps its list and only its kept files are stat'ed again. A cancelled scan keeps what it walked and Resume reads the rest. Forget drops the saved result. The treemap is every file a tile, grouped by folder, with no nested boxes: the folder chains of a Linux system made a staircase of empty headers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c96d6facad
commit
53d1da08a5
@@ -0,0 +1,87 @@
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func mk(t *testing.T, dir string, files map[string]int) {
|
||||
for p, n := range files {
|
||||
full := filepath.Join(dir, p)
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(full, make([]byte, n), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshAndResume(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mk(t, dir, map[string]int{"a/big1": 300 << 10, "a/small": 100, "b/c/big2": 500 << 10, "b/d/big3": 200 << 10})
|
||||
res, err := Walk(context.Background(), []string{dir}, Options{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := Find(res.Root, dir)
|
||||
if root == nil || root.Files != 4 {
|
||||
t.Fatalf("full walk: %+v", root)
|
||||
}
|
||||
want := root.Size
|
||||
// grow a file in place (its directory's mtime does not change) and add a file elsewhere
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if err := os.WriteFile(filepath.Join(dir, "a/big1"), make([]byte, 600<<10), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mk(t, dir, map[string]int{"b/d/big4": 400 << 10})
|
||||
res2, err := Walk(context.Background(), []string{dir}, Options{Prev: res.Root, Modes: map[string]Mode{dir: ModeMTime}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r2 := Find(res2.Root, dir)
|
||||
if r2.Files != 5 || r2.Size != want+(300<<10)+(400<<10) {
|
||||
t.Fatalf("mtime refresh: files %d size %d want %d (reused %d read %d)", r2.Files, r2.Size, want+(300<<10)+(400<<10), res2.Reused, res2.Read)
|
||||
}
|
||||
if res2.Reused == 0 || res2.Read == 0 {
|
||||
t.Fatalf("mtime refresh should reuse some and read some: reused %d read %d", res2.Reused, res2.Read)
|
||||
}
|
||||
// diff mode: only b/d named dirty; a's growth must NOT be seen (that is the contract), b/d's new file must
|
||||
dirty := map[string]bool{filepath.Join(dir, "b/d"): true}
|
||||
touched := map[string]bool{dir: true, filepath.Join(dir, "b"): true}
|
||||
res3, err := Walk(context.Background(), []string{dir}, Options{Prev: res.Root, Modes: map[string]Mode{dir: ModeDiff}, Dirty: dirty, Touched: touched}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r3 := Find(res3.Root, dir)
|
||||
if r3.Files != 5 || r3.Size != want+(400<<10) || res3.Read != 1 {
|
||||
t.Fatalf("diff refresh: files %d size %d want %d read %d", r3.Files, r3.Size, want+(400<<10), res3.Read)
|
||||
}
|
||||
// a cancelled walk marks what it did not read; resuming reads those
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
res4, err := Walk(ctx, []string{dir}, Options{}, nil)
|
||||
if err == nil || res4.Complete {
|
||||
t.Fatal("cancelled walk should say so")
|
||||
}
|
||||
if len(partialPaths(res4.Root, "/")) == 0 {
|
||||
t.Fatal("cancelled walk should leave partial directories")
|
||||
}
|
||||
res5, err := Walk(context.Background(), []string{dir}, Options{Prev: res4.Root, Modes: map[string]Mode{dir: ModeMTime}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r5 := Find(res5.Root, dir); r5.Files != 5 || len(partialPaths(res5.Root, "/")) != 0 {
|
||||
t.Fatalf("resume: files %d partial %v", r5.Files, partialPaths(res5.Root, "/"))
|
||||
}
|
||||
// the codec round-trips the partial flag
|
||||
pr, pw, _ := os.Pipe()
|
||||
go func() { _ = Encode(pw, res4.Root); pw.Close() }()
|
||||
back, err := Decode(pr)
|
||||
if err != nil || len(partialPaths(back, "/")) != len(partialPaths(res4.Root, "/")) {
|
||||
t.Fatalf("codec: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user