A practical, copy-pasteable path to running oans as a scheduled deduplication job on a NAS or home server. It leans on the self-describing hashfile: a run remembers its own options and paths, so everything after the first run needs no arguments.
Throughout, replace /srv/media with your data directory and media with a
short name for the job.
Deduplication only works on btrfs or xfs:
findmnt -no FSTYPE /srv/media # or: stat -f -c %T /srv/media- btrfs / xfs → you're good. (Most Synology volumes are btrfs.)
- zfs → stop. oans cannot dedupe ZFS; use ZFS's own deduplication instead. (This rules out a stock TrueNAS SCALE pool.)
- ext4 / other → oans will scan and report, but cannot deduplicate.
There is no binary package yet, so build from source. Install the build dependencies first:
# Fedora / RHEL
sudo dnf install gcc make pkgconf-pkg-config \
glib2-devel sqlite-devel xxhash-devel \
libuuid-devel libmount-devel libblkid-devel libbsd-devel# Debian / Ubuntu
sudo apt install build-essential pkg-config \
libglib2.0-dev libsqlite3-dev libxxhash-dev \
uuid-dev libmount-dev libblkid-dev libbsd-devThen build and install:
make
sudo make install # installs the oans binary (+ duperemove symlink)
sudo make install-systemd # installs the oans@ timer/service templatesGive the job a name — the hashfile the timer will later use is
/var/cache/oans/<name>.hash:
sudo install -d -m 0755 /var/cache/oans
sudo oans -dr --hashfile=/var/cache/oans/media.hash /srv/mediaThe
-dhere is load-bearing. Every later scheduled run replays this one, so if you seed the hashfile with a preview run (no-d), the timer inherits it and will hash forever without ever deduplicating — exiting 0 and reporting0 Breclaimed each time, which looks exactly like a healthy job on an already-deduped tree. A replay in that state warns and prints the command to fix it;oans --jsonreports"scan_configured_dedupe": false.
This is the expensive pass: it hashes everything and deduplicates. It
- records the options and paths, so later runs need no arguments,
- sizes its I/O threads from the detected backing storage (run with -v to
see what it picked; pass
--io-threads=Nto override), and - is safe to interrupt — the kernel does each dedupe atomically and byte-verified, so Ctrl+C can only waste work, never corrupt data.
Check the result:
oans --stats --hashfile=/var/cache/oans/media.hashsudo systemctl enable --now [email protected]The name after @ is the basename of your hashfile in /var/cache/oans/:
oans@media runs --hashfile=/var/cache/oans/media.hash. So match it to the
hashfile you created in Step 2 — if yours is
/var/cache/oans/data.hash, enable [email protected] instead. (The unit skips
cleanly until that hashfile exists, so set it up first.)
Done. Weekly from here, oans re-scans /srv/media, hashes only what changed,
and deduplicates — with no arguments, because the hashfile remembers everything.
To change the frequency, override OnCalendar=:
sudo systemctl edit [email protected] # e.g. OnCalendar=dailysystemctl list-timers 'oans@*' # when it next runs
journalctl -u [email protected] # what the last run did
oans --history --hashfile=/var/cache/oans/media.hash # reclaimed over time
oans --json --hashfile=/var/cache/oans/media.hash # metrics for a dashboardGet told when a run covers less than you asked for. oans exits 2 when it completed but could not use one of its stored roots — an unmounted drive, a renamed share, a deleted directory. The run still deduplicates everything else and otherwise looks perfectly healthy, so this is the case worth alarming on:
systemctl is-failed [email protected] # 'failed' after an exit-2 run
journalctl -u [email protected] | grep -i 'no longer exists'[email protected] deliberately does not whitelist 2 in SuccessExitStatus=, so
the unit fails and an OnFailure= of your own fires:
sudo systemctl edit [email protected] # add: [Unit] / OnFailure=...Exit 1 means it refused to run at all — usually because every stored path
is gone, which oans treats as "the drive is not mounted" rather than "delete
every hash". Skips you configured (--exclude, --min-filesize, snapshots)
never change the exit status; they are reported in the summary and in --json.
For live progress of a run in flight — to feed a dashboard or a health
check rather than watch the terminal — run it with --progress=json. oans then
streams one JSON object per phase (about once a second) to stderr, ending
with a {"event":"done", ...} line, and leaves stdout alone:
oans -qd --progress=json --hashfile=/var/cache/oans/media.hash \
/srv/media 2>>/var/log/oans-media.jsonlTo capture that from the scheduled job, add --progress=json to the unit's
command (sudo systemctl edit [email protected]); the JSON lines then land in the
journal, where journalctl -u [email protected] -o cat gives you a clean
stream to parse.
-
Run as root so oans can read and re-extent every file in the tree.
-
Multiple datasets: repeat Steps 2–3 with different names (
oans@photos,oans@backups, …). Each is an independent timer you can schedule separately. -
Report-only mode: set the job up with
-rinstead of-drand the scheduled runs will only refresh hashes and report, never change data. -
Skipping snapshots and NAS metadata.
--excludetakes.gitignore-style patterns, so a bare name matches at any depth — no path juggling:sudo oans -dr --hashfile=/var/cache/oans/media.hash \ --exclude '.snapshots' --exclude '@eaDir' /srv/mediaThe patterns are stored with the rest of the run, so later scheduled runs reuse them. A pattern that matches nothing is reported as a warning.
Upgrading from before 1.6.0?
--excludeused to match withfnmatch()against the whole path, and a pattern without a leading/was resolved against the current directory. Patterns already stored in a hashfile are replayed under the new rules, so a scheduled job could now exclude more (or less) than it did. Run the job once by hand and check the output before trusting the next timer firing. -
The hashfile is just a cache (under
/var/cache/oans). Deleting it only forces the next run to re-hash from scratch; it can live on your system SSD, separate from the data pool. -
Compressed btrfs: the reported "Reclaimed" figure is logical — the real disk space freed is smaller (roughly the compression ratio times that). Compare
compsizebefore and after for the true reclaimed amount. -
First run is slow, later runs are fast: only changed/new files are re-hashed, and files whose extents are already shared are skipped.
-
Querying while a run is in progress is safe. The report commands (
--stats,--history,--json,-L) open the hashfile read-only, so you can run them against a hashfile that a scheduled or manual run is actively using — they show a consistent point-in-time snapshot and can't disturb the run or the file. What you should not do is start a second writing run (anotheroans -dr, or-R) on the same hashfile at the same time; the single-writer design assumes one writer. Note the systemd timer won't start a secondoans@<name>.servicewhile one is active, but a manualoansrun is invisible to that — so avoid kicking off a manual dedupe when the timer might fire on the same hashfile.