Cleaner is the fourth app in Bonny's NAS workflow. It looks at what is left in the ingest lanes after Archive Assistant has moved media into the final libraries, and decides what is safe to clean up.
Intake Watcher Is the upload finished?
Archive Assistant What is it, and where should it go after approval?
Cleaner After approved moves, what leftovers are safe to clean?
BM Radio Plays the final Music and Audiobooks libraries.
Cleaner never deletes files. Its only real action today is removing empty folders that a person has reviewed, and only when every production gate is switched on. Everything else is reported for review.
| What | Local path | NAS path |
|---|---|---|
| Code | C:\Dev\NAS\cleaner |
container image |
| Data root | C:\NAS-Local\nas-data |
/mnt/rust-pool mounted at /app/data |
| Reports | C:\NAS-Local\nas-data\_REPORTS\cleaner |
/app/data/_REPORTS/cleaner |
| Dashboard | http://127.0.0.1:8092 | private LAN or Tailscale only |
| Setting | Default | Meaning |
|---|---|---|
MIN_AGE_DAYS |
30 |
A leftover must be untouched for 30 days before Cleaner will consider it. Age is measured from the newest modification time anywhere inside the item. |
CHECK_INTERVAL_SECONDS |
604800 (7 days) |
How often the dashboard writes a plan automatically when AUTO_RUN=true. |
TRASH_HOLD_DAYS |
14 |
How long an item moved to the trash hold stays recoverable before it may be purged. Must be at least 1. Empty-folder removal is not held (an empty folder has nothing to lose). |
| Category | Meaning | Action today |
|---|---|---|
safe_empty_folder |
Folder tree with no files, backed by a verified Archive Assistant move record | Removable when production gates are on |
known_harmless_trash |
Only junk files such as Thumbs.db, .DS_Store, .tmp, or CD-rip logs (.log, .cue, .accurip, .m3u, DR<n>.txt, auCDtect.txt) left after a verified move |
Moves to the trash hold when ALLOW_TRASH_HOLD and ALLOW_KNOWN_TRASH_DELETE are on; otherwise report only |
uncertain_leftover |
Anything else left behind (sidecars, logs, unmoved media) | Report only |
verified_duplicate |
A folder Archive Assistant has no record of, where every media file is a byte-for-byte copy (SHA-256) of a file already in a final library. Cover art, notes, rip logs and known junk may sit beside them | Moves to the trash hold when ALLOW_TRASH_HOLD is on; otherwise report only |
unrecorded_leftover |
A folder Archive Assistant has no record of that is not a provable duplicate (a media file not in any library, an unrecognised file type, or only artwork) | Report only until you tick it on the dashboard, then moves to the trash hold. Never picked up on its own |
quarantine_discard_approved |
A quarantined item you approved for discard in Archive Assistant, whose files still match the inventory recorded at that moment | Moves to the trash hold when ALLOW_TRASH_HOLD and ALLOW_QUARANTINE_DISCARD are on and the waiting period is over; otherwise report only |
quarantine_hold |
Anything else in _QUARANTINE, including items nobody decided about |
Never touched |
do_not_touch |
Final libraries, Photos, Documents, Projects, Backups, active upload lanes | Never touched |
blocked_too_new |
Younger than MIN_AGE_DAYS |
Waits |
blocked_by_missing_evidence |
No Archive Assistant move record matches | Waits for evidence |
blocked_by_destination_missing |
Move record exists but the moved files are missing | Flagged as critical |
Cleaner scans _INGEST/ready, _INGEST/leftover-review, _INGEST/failed, _STAGING and _QUARANTINE. It ignores _INGEST/incoming and _INGEST/intake-processing, which belong to Intake Watcher.
Three kinds of leftover are recognised and shown on their own dashboard lanes. They are report-only unless the trash hold is switched on (see below), and even then they are only moved aside, never deleted.
- Verified duplicates. Proven by content, not by name, so a
- Copyfolder is recognised even though Archive Assistant never recorded it. The folder must be at leastMIN_AGE_DAYSold. - Quarantine discards. Only items whose newest Archive Assistant decision is
discard_approvedand whose files still match the recorded inventory. Undecided, restored or revoked items are never picked up. A decision made less thanMIN_AGE_DAYSago is shown with the date it becomes eligible. - Unrecorded leftovers. Listed so nothing sits unnoticed, but never picked up automatically: it could be a download you have not scanned yet. They move to the trash hold only when you tick them.
Checking a folder against the libraries means hashing files. The dashboard spends at most 5 seconds per refresh on it and shows the rest as "not yet checked" (hashes are remembered, so it finishes over a few refreshes). A written plan (Run dry plan, the CLI, the weekly run) checks everything.
Default mode is report-only:
CLEANER_MODE=development
DRY_RUN=true
DESTRUCTIVE_ACTIONS_ENABLED=false
ALLOW_EMPTY_FOLDER_REMOVAL=falseEmpty-folder removal runs only when all four are set:
CLEANER_MODE=production
DRY_RUN=false
DESTRUCTIVE_ACTIONS_ENABLED=true
ALLOW_EMPTY_FOLDER_REMOVAL=trueEven then, a removal must pass every check at the moment it runs:
- the folder appears in a plan report you wrote and reviewed less than 24 hours ago;
- it is still classified
safe_empty_folderin a fresh plan; - it is inside
_INGEST/ready,_INGEST/failedor_STAGING, and is not the lane root itself; - it is not quarantine, leftover-review, or a link or junction;
- its whole tree still contains no files.
Removal deletes folders bottom-up with rmdir only. If a file appears mid-way, the removal stops. Each run writes execution-<run id>.json listing every removed folder so it can be recreated.
Nothing Cleaner picks up beyond empty folders is deleted. Instead it is renamed into _TRASH/<trash run>/<its original path> on the same disk (instant, and Cleaner never copies then deletes). A trash-manifest.json in each run folder records what the item was, why it was taken, the AA evidence, an inventory of every file, and when it may be purged (moved_at + TRASH_HOLD_DAYS). The intent is written to the manifest before the rename.
| Setting | What it allows (each also needs the production gates) |
|---|---|
ALLOW_TRASH_HOLD |
Moving verified duplicates, and strays you tick, into the hold. Required for everything below. |
ALLOW_QUARANTINE_DISCARD |
Also moving quarantine items you approved for discard in Archive Assistant, once their waiting period is over. |
ALLOW_KNOWN_TRASH_DELETE |
Also moving folders that hold only CD-rip logs and other known junk. |
ALLOW_TRASH_PURGE |
Permanently deleting items whose hold has expired. The only setting that deletes. |
An item moves only when all of these hold:
- it is in a plan report you wrote less than 24 hours ago, with the same category (a fresh plan re-checks every duplicate by content, so one whose library copy has gone is not moved);
- strays were also ticked by you; discards are past their waiting period;
- the folder still has the same file count, size and newest change as when it was classified;
- it is inside
_INGEST/ready,_INGEST/failedor_STAGING(or_QUARANTINE/<kind>/<item>for discards), is not a lane root, quarantine container, link or junction, and nothing already exists at its place in the hold; - the hold is on the same disk.
Restore puts an item back exactly where it was and is always available (no gates, it deletes nothing). It refuses if anything now exists at the original path.
Purge deletes an item only if you list it, ALLOW_TRASH_PURGE is on, its hold has expired, it is still in_trash, and its files still match the recorded inventory (an item that was altered while held is left alone). Each purge writes purge-<id>.json. Cleaner never purges on its own; a held item stays until you purge it.
Moves write trash-<run>.json next to the other reports. Only one cleanup action runs at a time; a second request gets a 409.
CLI equivalents: trash-move --run-id ... [--tick "<relative path>"], trash-list, restore <trash run> <relative path>, purge <trash run> <relative path>....
Cleaner reads Archive Assistant's JSON move manifests. It never imports Archive Assistant code or reads its database. See docs/ARCHIVE_ASSISTANT_EVIDENCE.md.
Set-Location C:\Dev\NAS\cleaner
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e .[dev]
Copy-Item .env.example .env # then set DATA_ROOT=C:/NAS-Local/nas-data.\.venv\Scripts\python.exe -m cleaner.server --host 127.0.0.1 --port 8092 # dashboard
.\.venv\Scripts\python.exe -m cleaner.cli dry-run # write a plan report
.\.venv\Scripts\python.exe -m cleaner.cli status # counts only
.\.venv\Scripts\python.exe -m cleaner.cli inspect # raw scan items
.\.venv\Scripts\python.exe -m cleaner.cli ensure-folders # create the shared folder layout
.\.venv\Scripts\python.exe -m cleaner.cli execute --run-id cleaner-YYYYMMDD-HHMMSS
.\.venv\Scripts\python.exe -m cleaner.cli trash-list # what is held, and until when
.\.venv\Scripts\python.exe -m pytest -q # testsOn the dashboard, Run dry plan (or Write plan to review when execution is enabled) writes a plan report. Remove N empty folders appears only when all production gates are on, and acts only on the plan you just wrote. With ALLOW_TRASH_HOLD on, Move N to trash hold does the same for the trash candidates (strays need their tick box), and the Trash Hold panel lists held items with Restore and, when ALLOW_TRASH_PURGE is on, Purge expired items.
- When one download produced several move records (for example a discography split into albums), Cleaner keeps only one record per source folder when checking destinations.
- Cleaner does not yet ask Archive Assistant whether any batch from a folder is still pending review. A folder that still holds unmoved media is reported as
uncertain_leftover, never removed, because it is not empty. - Cleaner reads
discard_approveddisposition records but ignoresrejectedones, so a rejected batch's files are reported, not cleaned. - The trash hold is a folder on the same disk, not a separate backup. Held items still take space until purged. Moving a hold to another disk is deliberately unsupported.
_TRASHsits beside_INGESTin the data root. Archive Assistant scans only_INGESTand BM Radio only its configured library folders, so neither reads it; keep it that way if you add scan roots.
- Read all move records per source folder and Archive Assistant disposition records.
- Ask Archive Assistant for pending batches before acting on a folder.
- Add a Cleaner SQLite ledger for first-seen dates, plan identity, and the action log.
- Ask Archive Assistant for pending batches before moving anything, and add the SQLite ledger (items 2 and 3).