Record each file's size on disk and print it in trees (closes #147)
check / check (push) Waiting to run

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. When a rescan finds a file with its size
and mtime unchanged but a different disk_size, it updates disk_size
alone, without reading the file. trees adds disk_size 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.

Judgement calls: each row's disk_size is the dupe copy's, not the
first's. A file whose record gets only a new disk_size counts as
unchanged.

Model: opus-5-5
This commit was merged in pull request #149.
This commit is contained in:
2026-10-27 19:56:49 +01:00
parent 98193b2896
commit 4ed2e370b1
10 changed files with 452 additions and 145 deletions
+13 -6
View File
@@ -22,6 +22,10 @@ type treeNode struct {
digest [sha256.Size]byte
fileCount int64
totalSize int64
// diskSize is the bytes the directory's files take on disk. Two
// copies of one tree have the same totalSize, but their diskSize
// can differ.
diskSize int64
}
// treesPhase names one phase of trees with its place among all of them,
@@ -139,15 +143,16 @@ func readTree(ctx context.Context, db *sql.DB,
// writeTreeGroups writes dupes to stdout as TSV, a header line and then
// a row for every member of each group but its first, counting each
// group on prog once its rows are written. stdout may be the terminal
// prog is drawn on, so the lines go through prog's writer. It returns
// the number of rows (dupe trees) and the bytes they could reclaim.
// group on prog once its rows are written. The disk_size column is the
// dupe's own. stdout may be the terminal prog is drawn on, so the lines
// go through prog's writer. It returns the number of rows (dupe trees)
// and the bytes they could reclaim.
func writeTreeGroups(stdout io.Writer, dupes [][]*treeNode,
prog *progress,
) (int, int64, error) {
out := prog.writer(stdout)
_, err := fmt.Fprintln(out, "first\tdupe\tfiles\tsize")
_, err := fmt.Fprintln(out, "first\tdupe\tfiles\tsize\tdisk_size")
if err != nil {
return 0, 0, fmt.Errorf("write stdout: %w", err)
}
@@ -159,9 +164,9 @@ func writeTreeGroups(stdout io.Writer, dupes [][]*treeNode,
for _, g := range dupes {
first := g[0]
for _, n := range g[1:] {
_, err = fmt.Fprintf(out, "%s\t%s\t%d\t%d\n",
_, err = fmt.Fprintf(out, "%s\t%s\t%d\t%d\t%d\n",
escapePath(first.path), escapePath(n.path),
first.fileCount, first.totalSize)
first.fileCount, first.totalSize, n.diskSize)
if err != nil {
return 0, 0, fmt.Errorf("write stdout: %w", err)
}
@@ -232,6 +237,7 @@ func (b *treeBuilder) add(r scanRec) {
dir.entries = append(dir.entries, fileEntry(name, r))
dir.fileCount++
dir.totalSize += r.size
dir.diskSize += r.diskSize
}
// openDir starts the directory called name inside the innermost open
@@ -270,6 +276,7 @@ func (b *treeBuilder) closeTo(n int) {
"d\x00"+name+"\x00"+string(dir.digest[:]))
dir.parent.fileCount += dir.fileCount
dir.parent.totalSize += dir.totalSize
dir.parent.diskSize += dir.diskSize
b.dirs = append(b.dirs, dir)
}
}