Skip to content

Repository files navigation

diskscan

A macOS disk scanner that tells you what you can delete — and cannot delete it for you.

License Platform Rust

Every disk tool can tell you that ~/Library is 84 GB. That is not useful: you cannot delete ~/Library. diskscan walks your disk in parallel and produces one self-contained HTML file with a navigable treemap, the largest files, and — the point of the whole thing — a list of specific folders you can actually delete, each with the number of bytes you would really get back and a button that opens it in the Finder.

Then it stops. There is no delete key. That is deliberate.

diskscan report: treemap of /Users/berna next to the "what to delete" tab

The generated report: navigable treemap on the left, and the "what to delete" tab on the right with reclaimable candidates, each explaining why it is safe.

$ diskscan ~
diskscan - scanning /Users/berna (24 threads)
   1,284,331 files -   198,442 dirs -    248 GB -  11.4s
  report: diskscan-report.html  (4.7 MB)

  > live report: http://127.0.0.1:52341/   (Ctrl-C to quit)

Install

git clone https://github.com/bernatch22/diskscan
cd diskscan
cargo install --path .        # puts `diskscan` in ~/.cargo/bin

Use

diskscan                      # scan /, write the report, open the browser
diskscan ~ -o reports/home.html
diskscan / --no-serve         # just the static HTML (buttons copy paths)
diskscan --report old.html    # re-open a report without rescanning
sudo diskscan /               # also see what your user cannot read
All options
flag meaning
-o, --out <file> HTML to write (default ./diskscan-report.html)
-x, --exclude <path> do not descend into this directory (repeatable)
--all drop the default exclusions applied when scanning /
--top <n> how many largest files to list (default 400)
--port <n> port for the local server (default: random)
--no-serve only write the HTML, do not start the server
--no-open do not open the browser
--threads <n> scanning threads (default 2× CPUs, capped at 32)
--report <html> do not scan: serve a report written earlier

What makes it different

Disk analysers split into two families. TUI scanners (ncdu, dust, gdu, dua) are fast and live in the terminal, but what you learn dies with the session — and most of them delete on a keypress. GUI treemaps (DaisyDisk, GrandPerspective, OmniDiskSweeper) are beautiful and leave you staring at a rectangle labelled Library, still guessing.

diskscan sits in neither camp, on four axes at once:

diskscan
What you get one self-contained HTML file — mail it, keep it, open it in six months with nothing installed
What it measures allocated bytes by default: what the volume actually loses
What it tells you named, leaf-level folders that are safe to delete, not "your home folder is big"
What it can do to your files nothing. There is no delete path in the codebase

1. The numbers are the ones you'd actually get back

A size here is st_blocks * 512 — allocated blocks, not st_size. A 40 GB .dmg that is 90 % sparse counts as what it occupies. Most tools report apparent size by default and offer allocated size as an opt-in flag; here it is the only mode, because a number you have to remember to ask for is a number that will mislead you at some point.

One honest limit: APFS clones (cp -c, Finder duplicates, some Xcode and Time Machine copies) share blocks on disk but st_blocks reports the full allocation for each copy, so a pair of clones counts twice. Detecting that needs per-file extent walks and is not implemented.

2. Scanning / on macOS just works

APFS firmlinks make /Users and /System/Volumes/Data/Users the same directory. A naive walk counts your entire home folder twice — ncdu's own manual warns that scanning the root without --exclude-firmlinks may count directories multiple times and can loop until it runs out of memory.

diskscan deduplicates directories by (dev, ino), exactly as it deduplicates hard-linked files, so a firmlink is recognised as somewhere it has already been. The other APFS volumes in the container (Preboot, VM, Update), /Volumes and /dev are excluded by default, since none of them is space you can free. --all turns the exclusions off.

3. It is fast because of one syscall

The walk uses macOS's getattrlistbulk(2), which returns name, type, device, inode, link count and allocated size for hundreds of entries per call. The usual readdir + lstat shape costs one syscall and one path lookup per file; on a million-file home directory that difference is the runtime. Volumes that do not implement the bulk call (network mounts, FAT, some FUSE filesystems) fall back to readdir + lstat automatically, and a test asserts the two backends agree.

Recursion is rayon work-stealing — the right primitive for a tree whose branches differ in size by four orders of magnitude. Memory stays bounded: only the directory tree survives the walk, plus three filenames per directory and one global heap of the --top largest files.

4. It is structurally incapable of deleting anything

ncdu and dua delete on a keypress. diskscan will not, ever — an earlier version had a "move to Trash" button and it was removed, on purpose.

Deleting is a decision, and the Finder is a much better place to make it: it shows you Quick Look previews, and it has Undo. So the report's buttons call open and open -R, and that is the entire set of side effects available to it. When the page is opened as a plain file with no server, they degrade to "copy path".

The optional bridge that makes those buttons work is a ~200-line loopback server with five independent limits, all of which must hold:

  • binds 127.0.0.1 only;
  • every action carries a 128-bit token from /dev/urandom, generated per run and embedded only in the served copy of the page — so another site your browser has open, which can guess the port but not the token, gets a 403;
  • the Host header must name loopback, so a DNS-rebound page cannot read the token from its own origin;
  • paths must be absolute, free of .., inside the directory that was actually scanned — and still inside it once symlinks are resolved, because open follows them;
  • the only two verbs are open and open -R, invoked as execve arguments — never through a shell, so a file named ; rm -rf ~ is just a filename.

The HTML written to disk never contains a token. --no-serve skips the server entirely.


The report

One file, no CDN, no network access, works offline forever.

  • Treemap — squarified, canvas-drawn. Click to enter, double click to open in the Finder, right click to reveal, ⌫ to go up, / to filter.

  • Contents — children of the current folder by weight, with the three largest files at each level.

  • Largest files — the top N of the whole scan, filtered to wherever you are standing.

  • What to delete — the tab that does the actual work. Two lists:

    • Known caches and data: a curated catalogue matched against real paths — node_modules, Xcode DerivedData and simulator runtimes, .npm, .cache, .cargo/registry, Homebrew, Docker/colima/lima/OrbStack, Android AVDs, __pycache__, virtualenvs, build output — each tagged regenerates or check first, with the command that cleans it properly.
    • Folders heavy with their own files: directories where the weight is in the files right there rather than spread across subfolders. What you see is what you free.

    Candidates nested inside other candidates are dropped, so you are shown the node_modules, not the project and its node_modules.

A 500 GB scan produces roughly 5 MB of HTML. That is the pruning at work: every directory keeps its 12 largest children plus anything over 1/20000 of the total, and the rest is folded into one "N smaller folders" row that still carries its byte count — pruned space is summarised, never silently dropped.

If the footer says N folders could not be read, grant your terminal Full Disk Access in System Settings → Privacy & Security, or run under sudo.


Requirements

macOS, Rust 1.74+. The crate will not compile elsewhere — it depends on getattrlistbulk, APFS firmlink semantics and the Finder. A Linux backend would be a welcome contribution.

Two dependencies: rayon and libc. No serde, no clap, no HTTP crate. For a tool people are invited to run as sudo diskscan /, a dependency tree you can read in an afternoon is a feature.

Library

The binary is a thin shell over a library you can use directly:

use diskscan::scan::{self, Walk};
use std::path::Path;

let walk = Walk::new(Vec::new(), 400);
let tree = scan::scan_root(Path::new("/tmp"), &walk);
println!("{} bytes in {} files", tree.size, tree.files);
module responsibility
scan the parallel walk and its accounting rules
report pruning, JSON, and HTML assembly
serve the localhost Finder bridge
cli argument parsing

Development

cargo test                              # 58 tests, including a real fixture tree
cargo clippy --all-targets -- -D warnings
cargo fmt --check

The integration suite in tests/accounting.rs builds an actual directory tree — sparse file, hard link, symlink, unreadable directory — and asserts each accounting rule this README claims. Those claims are the product; if one of them silently breaks, the tool is confidently wrong, which is worse than broken.

See CONTRIBUTING.md.

License

MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.

About

macOS disk scanner that tells you what you can delete - and cannot delete it for you. Parallel getattrlistbulk walk, APFS-correct sizes, self-contained HTML treemap.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages