Media directories with no campaign are never swept #1075

Closed
opened 2026-10-04 08:56:18 +00:00 by claude-bot · 2 comments
Collaborator

Found by the independent review of #991 part 2 (PR B).

What

Nothing removes a media/private/campaigns/<id>/ directory whose campaign row does not exist. Two ways one comes to exist:

  • A backend that dies mid-import (an OOM kill, a deploy, a restart). The import's own cleanup runs in a finally, which a killed process never reaches. The files it had written stay: attachments, page pictures, and each map's imported copy.
  • A purged campaign. The review saw no removal of that directory when a campaign is deleted for good. Read only, not confirmed by running; check this first.

The files are under private/, which nginx never serves, so nothing is exposed. The cost is disk: an interrupted import can leave several hundred megabytes.

Intended approach

  • A periodic task (Celery beat, daily) that lists media/private/campaigns/* and removes a directory when:
    • no campaign row has that id, and
    • the directory's newest file is older than a bound (24 hours), so an import still in flight is never touched.
  • It logs what it removed and how much.
  • If a purge really does leave the directory, remove it at purge time as well.
  • Tests: an orphan older than the bound is removed; a fresh orphan is kept; a live campaign's directory is never touched; a name that is not a UUID is ignored.

#991 (PR B), #1074.

Found by the independent review of #991 part 2 (PR B). ## What Nothing removes a `media/private/campaigns/<id>/` directory whose campaign row does not exist. Two ways one comes to exist: - **A backend that dies mid-import** (an OOM kill, a deploy, a restart). The import's own cleanup runs in a `finally`, which a killed process never reaches. The files it had written stay: attachments, page pictures, and each map's `imported` copy. - **A purged campaign.** The review saw no removal of that directory when a campaign is deleted for good. Read only, not confirmed by running; check this first. The files are under `private/`, which nginx never serves, so nothing is exposed. The cost is disk: an interrupted import can leave several hundred megabytes. ## Intended approach - A periodic task (Celery beat, daily) that lists `media/private/campaigns/*` and removes a directory when: - no campaign row has that id, and - the directory's newest file is older than a bound (24 hours), so an import still in flight is never touched. - It logs what it removed and how much. - If a purge really does leave the directory, remove it at purge time as well. - Tests: an orphan older than the bound is removed; a fresh orphan is kept; a live campaign's directory is never touched; a name that is not a UUID is ignored. ## Related #991 (PR B), #1074.
Author
Collaborator

Picking this up, on fix/1075-orphan-media-sweep.

Planned

  • First, check whether purging a campaign really leaves its media directory (read from the code so far, not confirmed).
  • A daily task that removes a per-campaign media directory only when:
    • its name is a UUID;
    • no campaign row has that id (a trashed campaign has a row, so its files are kept);
    • nothing inside it is newer than 24 hours;
    • it is a real directory inside the media root, never a symlink.
  • If a purge does leave the directory, remove it at purge time as well, after the delete commits.
**Picking this up**, on `fix/1075-orphan-media-sweep`. **Planned** - First, check whether purging a campaign really leaves its media directory (read from the code so far, not confirmed). - A daily task that removes a per-campaign media directory only when: - its name is a UUID; - no campaign row has that id (a trashed campaign has a row, so its files are kept); - nothing inside it is newer than 24 hours; - it is a real directory inside the media root, never a symlink. - If a purge does leave the directory, remove it at purge time as well, after the delete commits.
Author
Collaborator

Fixed in PR #1093, merged to main with the owner's go-ahead. CI was green on the PR's head.

  • Confirmed first: a purge left the campaign's files. Nothing removed them.
  • On a purge: the files go once the delete has committed.
  • Daily sweep: a campaign folder is removed only when no campaign row has its id (the trash counts as having one), nothing inside has changed for 24 hours, and it is a real directory inside the media root.
  • Guard: with no campaign rows at all, nothing is removed and a warning is logged.

To know when restoring: after a database backup older than the media volume is restored, files of campaigns made since that backup are swept a day later. docs/OPERATIONS.md says so.

Not yet seen on a real media volume. Its first run on dev will be the beat's own, a day after the next prerelease is deployed. I will read the worker's log for it then.

Reaches dev with the next prerelease.

**Fixed in PR #1093, merged to main with the owner's go-ahead. CI was green on the PR's head.** - **Confirmed first:** a purge left the campaign's files. Nothing removed them. - **On a purge:** the files go once the delete has committed. - **Daily sweep:** a campaign folder is removed only when no campaign row has its id (the trash counts as having one), nothing inside has changed for 24 hours, and it is a real directory inside the media root. - **Guard:** with no campaign rows at all, nothing is removed and a warning is logged. **To know when restoring:** after a database backup older than the media volume is restored, files of campaigns made since that backup are swept a day later. `docs/OPERATIONS.md` says so. **Not yet seen on a real media volume.** Its first run on dev will be the beat's own, a day after the next prerelease is deployed. I will read the worker's log for it then. Reaches dev with the next prerelease.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#1075
No description provided.