10. ScanDiff and the session trash
Two helper features, both reached from the Resume screen (see §06 Resume).
10.1. ScanDiff — comparing two scans
Analyzes what changed between two scans of the same root. Open it with D from Resume: it compares the selected session (the newer one) against the next entry in the list (the older one).
┌─ Scan comparison ──────────────────────────────────────────────────────┐
│ Scans #142 ↔ #141 · root /tank │
│ │
│ Unchanged: 1234567 · Moved: inode 87 / hash 12 │
│ Modified: 145 · Deleted: 67 · New: 312 │
│ New duplicates (a duplicate arrived): 234 │
├─────────────────────────────────────────────────────────────────────────┤
│ ▸ New duplicates (234) — Tab next category │
│ │
│ ▶ /tank/photos/2024/IMG_0010.HEIC ← dup: /tank/backup/IMG_0010.HEIC │
│ /tank/photos/2024/IMG_0011.HEIC ← dup: /tank/backup/IMG_0011.HEIC │
│ ... │
├─────────────────────────────────────────────────────────────────────────┤
│ ↑↓ select · Tab/Shift+Tab category · Esc/F10 back │
└─────────────────────────────────────────────────────────────────────────┘
The six categories (cycled with Tab)
| Category | What it means |
|---|---|
| New duplicates | Files that became duplicates in the new scan (they gained a match) |
| Moved (inode) | A file relocated — detected by a matching inode (within one dataset) |
| Moved (hash) | A file relocated — detected by a matching hash (across datasets) |
| Modified | Same path, different hash (the content changed) |
| Deleted | The path was present in the old scan, absent in the new one |
| New | The path appeared in the new scan, was not in the old one |
"Unchanged" is only a counter in the header; it is never listed (it could be millions of lines).
ScanDiff keys
| Key | Action |
|---|---|
| Tab | Next category |
| Shift+Tab | Previous category |
| ↑ / ↓ or k / j | Move the cursor through the category's entries |
| Esc or q or F10 | Return to Resume |
Where this is useful
- Watching a regular scan. Run
dedcom --scan /tankonce a week and look at the diff: what is new, what moved. If Modified is unexpectedly large, something is changing files — possibly unintentionally. - A verify safety net after apply. After applying deduplication you can run a fresh scan and diff it: Deleted = everything you removed or turned into a hardlink; Unchanged = the rest; if anything shows up under Modified, that is a red flag.
- Moves as cleanup information. If files were relocated en masse (the 2024
photos moved from
/tank/inboxinto/tank/photos/2024), the Moved (inode) category shows it — you can confirm that hardlink groups were not broken.
Detection:
- Moved (inode) — an exact match (
device+inodepreserved) = anmvwithin the dataset. Independent of the hash. - Moved (hash) — a different
inode(for example,cp -a) but a matchingblake3= the same content in another place. Less reliable than the inode match, but it catches copies. - New duplicates — the file was already in the old scan as a single copy (not a "duplicate"), and in the new scan it gained at least one peer with the same hash (now a "duplicate"). A hint that more duplicate content was added.
10.2. Session trash — Trash
This is not the file quarantine (.dedcom-quarantine is about physical
files — see §03 Data Safety). The session trash is a soft
delete of scan records in dedcom.db.
Open it with T from Resume. On large pools, deleting a session means
deleting millions of rows from file — it takes minutes, so it is implemented
as a two-step flow:
Resume — list of active sessions
│
│ Del on a session → ConfirmAction::TrashScan(id)
│
▼ modal dialog: "Y confirm · N/Esc cancel"
│
▼ Y → fast UPDATE: trashed=1 (instant)
│
│ T from Resume → open the trash
│
▼ Trash — list of "deleted" sessions
│
▼ R/Enter — restore (instant: trashed=0)
│
▼ Del + Y → ConfirmAction::PurgeScan(id)
heavy DELETE in the background; final removal
The Trash screen
┌─ Trash — deleted scans ────────────────────────────────────────────────┐
│ ▸ #138 /tank/media 2026-02-01 (deleted 2026-04-10 13:42) │
│ #135 /rpool 2026-01-15 (deleted 2026-03-22 09:15) │
│ #130 /tank 2025-12-08 (deleted 2026-02-14 11:00) │
├─────────────────────────────────────────────────────────────────────────┤
│ R/Enter restore · Del purge permanently · Esc back │
└─────────────────────────────────────────────────────────────────────────┘
Trash keys
| Key | Action |
|---|---|
| ↑ / ↓ or k / j | Move the cursor through the sessions |
| R / Enter | Restore the selected session (it returns to the active list) |
| Del | Purge permanently (modal Y/N) |
| Esc or q / Q | Return to Resume |
The purge confirmation
┌─ Purge from trash? ─────────────────────────────────────────────────────┐
│ The session will be deleted PERMANENTLY — this is irreversible. │
│ │
│ The related rows in the DB (file, file_mark, file_group) number in the │
│ millions. The deletion runs in the background; the UI is not blocked. │
│ │
│ [Y] yes · [N] no │
└──────────────────────────────────────────────────────────────────────────┘
The matching modal when you first send a session to the trash from Resume is
titled Move to trash? with the body
The session will be moved to the trash — it can be restored (t).
After Y, the status reads "Purging from trash in the background…" and work continues.
What "purge permanently" does NOT do
- It does not touch real files. This deletes the scan's records: the
file manifest, the marks, the groups. ZFS snapshots (
@dedcom-<ts>) and the file quarantine (.dedcom-quarantine/<ts>/) stay in place — clearing them is separate (zfs destroy,dedcom --purge-quarantine --yes; see §03 Data Safety). - It does not free space immediately. SQLite does not return space to the
dedcom.dbfile after aDELETE; that requires aVACUUM(dedcom --compact-db; see §12 Maintenance).
When to use the session trash
- Dozens of old scans have piled up and the DB has bloated. Delete the old ones,
keep the recent ones, then run
--compact-db. - You ran a test scan against the wrong root — delete it so it does not clutter Resume.
- You compared two scans (ScanDiff) and confirmed the older session is no longer needed — delete it.
Retention (automatic cleanup)
Retention is on by default and runs without asking. On every scan
completion, completed scans of the same roots beyond the newest history_keep
(default: 2) are moved to the trash, along with stale unfinished ones. Nothing
is deleted — the sessions stay in the trash and remain recoverable until you
purge them yourself.
Two exceptions keep an empty root — usually the mount point of a dataset that is
not mounted — from eating the history. When a scan finds no files under one of
its roots, where an earlier scan of the same roots found files, the scans that
hold files under it stay out of the trash (an unfinished one too), and the scan
says so (History kept: …, §11); the rest is trimmed as usual.
If the root is empty on purpose, move those scans to the trash yourself. And a
finished scan that found no file at all takes no place in history_keep: the next
trim moves it to the trash with the stale unfinished ones. With several roots, a
scan that found only one of them empty still takes its place — after the dataset
is back, it can push an older full scan to the trash; restore that one from here
if you need it.
The limit is history_keep in config.json; it is not yet settable from the
UI. Values and what they mean: §12 Maintenance.
What's next
- §11 Headless —
--statsfor per-session statistics,--compact-dbfor VACUUM. - §12 Maintenance — the structure of
dedcom.db, its size, and cleanup.
Published from docs/manual/10-diff-trash.md at v0.9.2 · last changed 2026-09-25