Settings → Maintenance → Backups gains "Restore from the bucket": pick one of
the bucket's dated dumps, type its confirmation, and FabledCurator saves the
current database to the bucket, replaces it with the dump, migrates it to this
version, then fetches every image and attachment back to the path its row
records. Each file is checked against its sha256 on arrival and moved into
place only then; a file already on disk is left alone, so "Fetch missing
files" repairs a library that lost files without touching the database.
- services/backup_restore.py: list/find/fetch dumps, migrate, keep this
install's destination (the dump's sealed secret can't be opened here),
library_files work list, restore_file (one fetch per sha, copies for
duplicate paths, refuses paths outside /images).
- restore_offsite_task: the database phase, then 45-minute file chunks with a
heartbeat; a failed load leaves an empty migrated database and says why.
The dump's own run row is marked ok, other runs then going "Interrupted".
- backup_s3.get_file: sha/size check on arrival, ObjectMissing for a 404,
download pacing.
- backup_service: every dump leaves the credential table's rows out;
restore_db reports pg_restore's last line, stops at the first error, ends
other connections and waits at most 60 s for locks.
- Backups and orphan repair wait while a restore runs; the stall sweep judges
restores by heartbeat; restore runs are pruned to 20.
- CLI: python -m backend.app.scripts.restore list|database|files.
- README: backup and restore section; logins are in no backup.
- Tests: a round trip through an in-process S3 bucket (tests/fake_s3.py) with
real pg_dump/pg_restore, plus file, run, API and get_file cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>