Files of a campaign that no longer exists are removed, daily and on a purge (#1075) #1093

Merged
claude-bot merged 4 commits from fix/1075-orphan-media-sweep into main 2026-10-05 03:19:09 +00:00
Collaborator

Closes #1075.

Summary

  • Confirmed first: a purge left the files. purge_campaign is only db.delete(campaign); nothing removed the campaign's media. A test showed it.
  • On a purge: the campaign's folders are removed once the delete has committed. A purge that fails keeps its files.
  • Daily, sweep_orphan_campaign_media: removes a campaign folder under either root (private/campaigns/ and the public campaigns/ from before #894) only when all of these hold:
    • its name is a UUID and it is a real directory, never a symlink;
    • no campaign row has that id. A campaign in the trash has a row, so its files are kept;
    • nothing anywhere inside has changed for 24 hours;
    • its resolved path is inside the media root.
  • The map walk's state files for that id go too, only when none of the id's folders was kept.
  • With no campaign rows at all, nothing is removed and a warning is logged. An empty table is what a database mid-restore, or the wrong database, looks like.
  • One failing folder is logged and does not stop the rest.
  • No migration, bot, compose or frontend change.

What an operator should know

docs/OPERATIONS.md gains a section. The point that matters: after restoring a database backup older than the media volume, campaigns made since that backup have files and no row, and the sweep removes those files once they are 24 hours unchanged.

Changelog

  • changelog.d/1075-orphan-media-sweep.fixed.md

Test plan

  • New tests/test_orphan_media_sweep.py, 14 tests:
    • an old orphan goes, in both roots, with its walk state;
    • a fresh file deep inside keeps the folder and its state;
    • a live and a trashed campaign are untouched;
    • non-UUID names, a UUID-named file, the lock and gate files are ignored;
    • a symlink is neither followed nor entered;
    • one failure does not stop the rest;
    • no campaign rows at all: nothing removed;
    • the beat entry exists once, daily;
    • a purge removes the files after the commit, and a campaign still in its grace keeps them.
  • At the head commit, with the soft-delete and session-hygiene tests: 44 passed. Lint clean.
  • An earlier run with tests/export and the watchdog tests: 514 passed, 1 failed. The failure was the sweep's own fresh-file test, which exposed a walk-state bug; fixed, and that test passes above.
  • Version and changelog checks OK.
  • Not run: the full backend suite (CI). tests/export was not re-run after the last two commits, which touch only the sweep.
  • Not run against a real media volume. The first run on dev will be the beat's own, a day after the deploy.

🤖 Generated with Claude Code

Closes #1075. ## Summary - **Confirmed first: a purge left the files.** `purge_campaign` is only `db.delete(campaign)`; nothing removed the campaign's media. A test showed it. - **On a purge:** the campaign's folders are removed once the delete has committed. A purge that fails keeps its files. - **Daily, `sweep_orphan_campaign_media`:** removes a campaign folder under either root (`private/campaigns/` and the public `campaigns/` from before #894) only when all of these hold: - its name is a UUID and it is a real directory, never a symlink; - no campaign row has that id. A campaign in the trash has a row, so its files are kept; - nothing anywhere inside has changed for 24 hours; - its resolved path is inside the media root. - **The map walk's state files** for that id go too, only when none of the id's folders was kept. - **With no campaign rows at all, nothing is removed** and a warning is logged. An empty table is what a database mid-restore, or the wrong database, looks like. - One failing folder is logged and does not stop the rest. - No migration, bot, compose or frontend change. ## What an operator should know `docs/OPERATIONS.md` gains a section. The point that matters: after restoring a database backup older than the media volume, campaigns made since that backup have files and no row, and the sweep removes those files once they are 24 hours unchanged. ## Changelog - [x] `changelog.d/1075-orphan-media-sweep.fixed.md` ## Test plan - [x] New `tests/test_orphan_media_sweep.py`, 14 tests: - an old orphan goes, in both roots, with its walk state; - a fresh file deep inside keeps the folder and its state; - a live and a trashed campaign are untouched; - non-UUID names, a UUID-named file, the lock and gate files are ignored; - a symlink is neither followed nor entered; - one failure does not stop the rest; - no campaign rows at all: nothing removed; - the beat entry exists once, daily; - a purge removes the files after the commit, and a campaign still in its grace keeps them. - [x] At the head commit, with the soft-delete and session-hygiene tests: `44 passed`. Lint clean. - [x] An earlier run with `tests/export` and the watchdog tests: `514 passed, 1 failed`. The failure was the sweep's own fresh-file test, which exposed a walk-state bug; fixed, and that test passes above. - [x] Version and changelog checks OK. - [ ] **Not run:** the full backend suite (CI). `tests/export` was not re-run after the last two commits, which touch only the sweep. - [ ] **Not run against a real media volume.** The first run on dev will be the beat's own, a day after the deploy. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The orphan sweep removes nothing when there are no campaign rows at all (#1075)
All checks were successful
CI / Docker image build (pull_request) Successful in 22s
CI / Does this change touch the recording path? (pull_request) Successful in 30s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend lint (ruff) (pull_request) Successful in 37s
CI / Bot/backend version sync (pull_request) Successful in 39s
CI / Feedback site tests (pull_request) Successful in 55s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m5s
CI / End-to-end journeys (desktop) (pull_request) Successful in 11m14s
CI / End-to-end journeys (phone) (pull_request) Successful in 13m48s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 14m41s
CI / Frontend (shard 2/2) (pull_request) Successful in 16m19s
CI / Frontend (shard 1/2) (pull_request) Successful in 21m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 1h9m34s
ee67194155
An empty campaigns table is what a database mid-restore, or the wrong
database, looks like, and from there every campaign's files read as
nobody's. The sweep now warns and removes nothing. OPERATIONS says what a
restore to an older backup means for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch fix/1075-orphan-media-sweep 2026-10-05 03:19:11 +00:00
rbrooks referenced this pull request from a commit 2026-10-05 07:42:48 +00:00
Sign in to join this conversation.
No description provided.