Three audit items on the backup path.
The script matched volume names by pattern and took the first hit, so a stale
or restored volume could be backed up instead of the live one — and every
verification step would then faithfully confirm the wrong database. It now asks
the container what is mounted at /data and refuses ambiguity. Tested against a
decoy volume that the old pattern would have matched first.
check-images treated a photo as healthy if a file with the right name existed,
so a truncated or partially restored file passed. It compares each file against
the byte count its row records now; a one-byte stand-in for a 123KB photo is
reported as WRONG SIZE and exits non-zero. The backup runs the same check
against the stopped volume and exits 2 when the source was already damaged —
still writing the archive, because a faithful copy of imperfect data is worth
having, but saying so.
The restart trap was installed after the app had already been stopped, so an
interrupt in between could leave the service down with nothing to bring it
back. The trap goes in first now, covers INT and TERM as well as EXIT, and
records whether the container was running beforehand so a backup of an
already-stopped app leaves it stopped.
Verified on the live host: healthy source exits 0, damaged source exits 2 with
the archive still written and verified, decoy volume correctly ignored, service
answering immediately afterwards, and the test rows removed.
Checks go from 290 to 294.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running it against the live service exposed two gaps. It announced "App
restarted" the moment the container existed, so the very next request got a 502
from an app that wasn't listening yet; it now polls /healthz until the app is
actually serving. And it only checked that the archive contained the right
files — it now opens the archived database and runs SQLite's integrity_check,
because an archive that exists but won't restore is the worst kind of backup.
End to end on the server, with a throwaway part and photo inserted to exercise
the verification path: 2.6 seconds of downtime, archive verified, service
answering immediately on return, throwaway data removed afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four audit findings on the photo feature.
The upload route was async, so its blocking SQLite work ran on the event loop:
under contention it stalled every other request for SQLite's busy timeout, not
just its own. It also read the photo count without the write lock, so
overlapping uploads all observed the same total and stored past the ceiling
together. It is a synchronous endpoint now, running in the threadpool, taking
BEGIN IMMEDIATE before re-checking the part, the ceiling and the position, and
committing before it returns. Reverting either half makes the new test die with
the same TimeoutError the audit reported.
The 8MB cap protected nothing: Starlette parses and spools an entire multipart
body before a route's dependencies run — before the login check — so the bytes
were already on disk by the time anything rejected them, and an anonymous
caller could make us write them. A plain ASGI middleware outside routing now
refuses an over-large body first, and Caddy enforces the same ceiling at the
edge.
The documented backup captured the database and the photos at two different
moments while the app stayed writable, so a photo deleted in between left the
saved database pointing at a file the archive did not contain. tools/backup.sh
stops the app for the few seconds the copy takes and verifies afterwards that
every referenced photo is in the archive.
Cleanup could destroy data rather than merely litter: prune-images could delete
a file between an upload writing it and inserting its row, and deletions
unlinked before their transaction committed. Pruning now ignores anything under
an hour old unless forced, and deletes commit before unlinking — an orphaned
file is recoverable, a row without its photo is not. check-images reports drift
in both directions and fails only on the direction that loses data.
Checks go from 271 to 291.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>