Exports and Reports
Scope: separating the scan from the browsing — exporting a tree to a file, browsing the file later (or elsewhere), the JSON format read closely, the 2.x binary and compressed formats, and which combinations of versions can actually read each other's files.
Why export at all
Scanning and browsing are different jobs happening at different times. The scan must run where the disk is; the browsing is better done where you are, at leisure, possibly twice. An export is the scan written down:
$ ncdu -0 -o before.json homedir
$ ncdu -f before.json
-0 means "no interface" — scan and write,
nothing else, which is what makes exports
scriptable. The second command opens the
file in the ordinary browser: same screens,
same keys, with one marker in the title
bar — [imported] — and one disability
(chapter 5): deleting is off. The tree
itself does not need to exist where you
browse; the captures for this chapter
include a browsing session with the lab
renamed away, and nothing noticed. An
export is a photograph of a disk: dated,
portable, and safe to pass around.
Real uses, in the order they come up:
- A server you can SSH into but would rather not sit in front of: scan there, browse here.
- A before/after pair around a cleanup: export, delete, export again, and the two files are the ledger.
- A record: what this disk looked like the day it filled, kept after the fix.
-o - writes the export to stdout, and
-f - reads one from stdin, so the two
halves pipe together across a network:
$ ncdu -o - /srv | ssh you@laptop ncdu -f -
(The export side was verified locally in 1.19, the stdin side in 2.9; both are the same stream format described below.)
The JSON format, read closely
The default export is JSON, and it is meant to be readable — by you, not only by ncdu. The lab's, from either version:
[1,2,{"progname":"ncdu","progver":"1.19","timestamp":1792610718},
[{"name":"/home/.../homedir","asize":190,"dev":86},
[{"name":"Documents","asize":62},
[{"name":"archive","asize":30},
{"name":"old-letters.txt","asize":800,"dsize":4096}],
{"name":"notes.txt","asize":25,"dsize":4096},
{"name":"report-2024.txt","asize":2400,"dsize":4096}],
…
The shape: a header [1, 2, {...}] —
format major 1, minor 2, the producing
program and version, and the scan time —
then the tree. A directory is an array
whose first element describes the
directory itself and whose rest are its
children; a file is a plain object. The
fields repay attention:
- Every entry has
asize(apparent size). Files also havedsize(disk usage). Directories have nodsize— their usage is the sum of what is beneath them, computed by the reader. (Theasizeon a directory is its own entry's size — 190 bytes for the lab's root — not a total. Misreading that is a classic.) - The sparse file has no
dsizeat all: zero is written by omission."name":"old-vm.img","asize":104857600. - Symlinks are marked
"notreg":true; a directory ncdu could not read is"read_error":true(chapter 9's specimen: the locked room appears in the export as{"name":"locked","asize":60, "read_error":true}, childless). - Hard links are where the format bites.
Both names appear at full size, each
carrying
"hlnkc":true,"nlink":2and the shared"ino". Summingdsizeover the lab's export gives 75,354,112 bytes — 3,145,728 more than the tree's true disk usage (chapter 3's table). The browser deduplicates by inode when it totals; a script of yours must do the same, or it will report disks fuller than they are.
The two versions' JSON for the same tree
is byte-identical except progver (and
timestamp). That buys real
interoperability, next section.
The 2.x formats — and the wall
2.x adds two export shapes, both aimed at very large trees (the binary format arrived in 2.6, per the upstream site):
-O FILE— a compact binary export (929 bytes for the lab, against JSON's 2,920), which 2.x can also browse without loading it all into memory.-cwith-o— the JSON (or binary) zstd-compressed (758 bytes here).
Both are 2.x-only in the hard sense. Verified matrix, every cell exercised:
| Export written by | Read by 1.19 | Read by 2.9 |
|---|---|---|
| 1.19, JSON | ✓ | ✓ [imported] |
| 2.9, JSON | ✓ [imported] |
✓ |
2.9, binary -O |
✗ error | ✓ [from file] |
2.9, zstd -c |
✗ error | ✓ [imported] |
The failures are honest failures. Pointing 1.19 at either 2.x-only file gives:
┌───Error!─────────────────────────────────────────┐
│ Error: could not open export-2.9.bin │
│ Zero-byte found in JSON stream │
│ press any key to continue...
└──────────────────────────────────────────────────┘
— 1.19 tries to read anything as JSON and says so when it cannot. The rule for mixed fleets is therefore simple: export JSON when a 1.x anywhere might read it; use binary or compressed only inside a 2.x world, for size or for trees too large to hold in memory.
Then and now
There is no built-in diff. Browsing a
before export and an after export side by
side — two terminals, same path — is the
practical method, and for the lab it shows
chapter 5's deletion exactly: Downloads
25.0 MiB in before, 20.0 MiB in after,
forgotten.zip present in one, gone in
the other. If you script the comparison
over the JSON instead, reread the hardlink
paragraph above first, and compare
directory totals computed the way the
browser computes them — deduplicated —
not raw field sums.
Check: make the pair yourself on the lab.
ncdu -0 -o before.json homedir-del,
delete something small in the browser
(chapter 5's flow), ncdu -0 -o after.json
homedir-del, then ncdu -f before.json
and ncdu -f after.json in two terminals.
The title bar's [imported] tells you
which kind of truth you are looking at:
the recorded kind. It never pretends to be
the disk.