Record each file's size on disk and print it in trees (closes #147)
check / check (push) Canceled after 0s

The files table gets a disk_size column: the file's allocated blocks,
st_blocks × 512, from the lstat the walk already does. scan writes it
with every record it writes. trees adds it up per directory and prints
it as a column after size. On a compressed or sparse file it is less
than size, so two copies of one tree can differ; each row shows the
dupe copy's.

Judgement call: a row's disk_size is the dupe copy's, not the first's.
Unchanged files keep the disk_size from their last hash: a change in
allocation that leaves size and mtime alone is not picked up.

Model: opus-5-5
This commit is contained in:
2026-10-27 18:08:27 +00:00
parent 98193b2896
commit dfbfdd0601
10 changed files with 230 additions and 88 deletions
+55
View File
@@ -42,6 +42,21 @@ func writeFile(t *testing.T, dir, name string, data []byte) string {
return p
}
// diskSizeOf returns the bytes the file at path takes on disk, from its
// own lstat: st_blocks counts 512-byte units.
func diskSizeOf(t *testing.T, path string) int64 {
t.Helper()
var st syscall.Stat_t
err := syscall.Lstat(path, &st)
if err != nil {
t.Fatal(err)
}
return st.Blocks * 512
}
// pattern returns n bytes of deterministic content seeded by tag.
func pattern(tag byte, n int) []byte {
data := make([]byte, n)
@@ -1416,6 +1431,46 @@ func TestScanStoresRawHashes(t *testing.T) {
}
}
// TestScanStoresDiskSize scans a sparse file, which takes less space on
// disk than its length, and checks the disk_size stored for it against
// the file's own lstat.
func TestScanStoresDiskSize(t *testing.T) {
t.Parallel()
dir := t.TempDir()
db := openTestDB(t)
p := writeFile(t, dir, "sparse", []byte("x"))
// Truncate makes the file longer without giving it blocks for the
// new bytes.
const length = 1024 * 1024
err := os.Truncate(p, length)
if err != nil {
t.Fatal(err)
}
syncTree(t, db, dir)
want := diskSizeOf(t, p)
if want >= length {
t.Fatalf("the file takes %d bytes on disk, want fewer than its "+
"length %d", want, length)
}
var got int64
err = db.QueryRowContext(t.Context(),
"SELECT disk_size FROM files WHERE path = ?", []byte(p)).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("disk_size = %d, want %d", got, want)
}
}
// TestSyncScanSameSecondRewrite rewrites a file in place at the same
// size with an mtime later in the same second as the recorded one: the
// next scan must notice the change and re-hash the file. The operand is