Dev build 2026-06-01 21:45
This commit is contained in:
parent
3e2700b1bc
commit
ef05da92f8
@ -826,11 +826,24 @@ def _run_pg_restore(dump_bytes: bytes) -> None:
|
|||||||
Before the destructive load, a safety dump of the CURRENT database is taken.
|
Before the destructive load, a safety dump of the CURRENT database is taken.
|
||||||
If applying the requested dump fails, the database is rolled back to that
|
If applying the requested dump fails, the database is rolled back to that
|
||||||
safety snapshot so a failed restore never leaves an empty/broken database.
|
safety snapshot so a failed restore never leaves an empty/broken database.
|
||||||
|
|
||||||
|
The safety dump is also written to the local dump store, so every restore
|
||||||
|
leaves behind a token-free local snapshot of the pre-restore state. Regular
|
||||||
|
backups stay Dropbox-only (they can run hourly and would otherwise fill the
|
||||||
|
disk).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
safety_bytes, _ = _run_pg_dump()
|
safety_bytes, safety_name = _run_pg_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
safety_bytes = None
|
safety_bytes, safety_name = None, ""
|
||||||
|
|
||||||
|
if safety_bytes is not None:
|
||||||
|
try:
|
||||||
|
_save_local_dump(f"pre-restore-{safety_name}", safety_bytes)
|
||||||
|
_enforce_local_dump_retention(_load_dropbox_retention_count())
|
||||||
|
except Exception:
|
||||||
|
# A local-copy failure must not block the restore.
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_apply_pg_dump(dump_bytes)
|
_apply_pg_dump(dump_bytes)
|
||||||
@ -1136,16 +1149,6 @@ def _run_backup_internal(*, dry_run: bool, progress_key: int | None = None) -> t
|
|||||||
uploaded_size += len(dump_data)
|
uploaded_size += len(dump_data)
|
||||||
uploaded_count += 1
|
uploaded_count += 1
|
||||||
|
|
||||||
# Keep a local copy of the dump so the database can be restored without a
|
|
||||||
# Dropbox token. Only for real runs, not dry runs.
|
|
||||||
if not dry_run:
|
|
||||||
try:
|
|
||||||
_save_local_dump(dump_name, dump_data)
|
|
||||||
_enforce_local_dump_retention(retention_count)
|
|
||||||
except Exception:
|
|
||||||
# A local-copy failure must not fail the backup itself.
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
_save_manifest(new_manifest)
|
_save_manifest(new_manifest)
|
||||||
return total_files, uploaded_count, uploaded_size
|
return total_files, uploaded_count, uploaded_size
|
||||||
@ -1868,8 +1871,8 @@ async def restore_local_dump(request: Request):
|
|||||||
async def upload_restore_dump(file: UploadFile = File(...)):
|
async def upload_restore_dump(file: UploadFile = File(...)):
|
||||||
"""Restore the database from an uploaded .sql dump. No Dropbox token required.
|
"""Restore the database from an uploaded .sql dump. No Dropbox token required.
|
||||||
|
|
||||||
The uploaded dump is also saved to the local dump store so it is available
|
A local pre-restore safety snapshot is taken automatically (by _run_pg_restore)
|
||||||
for future restores.
|
before the load; the uploaded file itself is not persisted.
|
||||||
"""
|
"""
|
||||||
if not shutil.which("psql"):
|
if not shutil.which("psql"):
|
||||||
return {"ok": False, "error": "psql is not available in this container"}
|
return {"ok": False, "error": "psql is not available in this container"}
|
||||||
@ -1890,10 +1893,4 @@ async def upload_restore_dump(file: UploadFile = File(...)):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "error": str(e)}
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
# Persist a local copy so it shows up under Local Database Restore afterwards.
|
|
||||||
try:
|
|
||||||
await asyncio.to_thread(_save_local_dump, filename, data)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {"ok": True, "restored": filename, "size_bytes": len(data)}
|
return {"ok": True, "restored": filename, "size_bytes": len(data)}
|
||||||
|
|||||||
@ -302,9 +302,10 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card-head">Local Database Restore (no Dropbox needed)</div>
|
<div class="card-head">Local Database Restore (no Dropbox needed)</div>
|
||||||
<p class="muted" style="margin-top:0;margin-bottom:0.6rem;">
|
<p class="muted" style="margin-top:0;margin-bottom:0.6rem;">
|
||||||
Every backup also keeps a local copy of the database dump on the config volume,
|
Before every restore a safety snapshot of the current database is saved locally
|
||||||
so the database can be restored even without a Dropbox token. You can also upload
|
on the config volume (named <code>pre-restore-…</code>), so you can roll back or
|
||||||
a <code>.sql</code> dump you downloaded from Dropbox manually.
|
restore without a Dropbox token. You can also upload a <code>.sql</code> dump you
|
||||||
|
downloaded from Dropbox manually. Regular backups remain Dropbox-only.
|
||||||
</p>
|
</p>
|
||||||
<p class="muted" style="margin-top:0;margin-bottom:0.9rem;color:var(--err);">
|
<p class="muted" style="margin-top:0;margin-bottom:0.9rem;color:var(--err);">
|
||||||
⚠ Destructive: replaces the entire current database (with automatic rollback if the dump fails to load).
|
⚠ Destructive: replaces the entire current database (with automatic rollback if the dump fails to load).
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from changelog import CHANGELOG
|
from changelog import CHANGELOG
|
||||||
|
|
||||||
BUILD = 5
|
BUILD = 6
|
||||||
|
|
||||||
|
|
||||||
def _release_version() -> str:
|
def _release_version() -> str:
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
# Develop Changelog
|
# Develop Changelog
|
||||||
|
|
||||||
## 2026-06-01 — Local database backups + token-free / upload restore
|
## 2026-06-01 — Local pre-restore snapshots + token-free / upload restore
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Backups now keep a **local copy** of each PostgreSQL dump on the config volume (`CONFIG_DIR/postgres_dumps/`, same retention as snapshots), so the database can be restored **without a Dropbox token**. Previously dumps lived only in Dropbox, so losing the stored token meant being locked out of every backup.
|
- Every restore now writes its **pre-restore safety dump to a local store** on the config volume (`CONFIG_DIR/postgres_dumps/`, named `pre-restore-…`, with snapshot-equal retention), so the database can be restored or rolled back **without a Dropbox token**. Regular backups are intentionally left **Dropbox-only** — production backs up hourly, so writing every backup to disk would fill the volume.
|
||||||
- `routers/backup.py`: `LOCAL_DUMP_DIR`, helpers `_save_local_dump`, `_list_local_dumps`, `_enforce_local_dump_retention`, `_resolve_local_dump`; `_run_backup_internal` writes the dump locally (real runs only) after uploading to Dropbox (a local-copy failure never fails the backup).
|
- `routers/backup.py`: `LOCAL_DUMP_DIR`, helpers `_save_local_dump`, `_list_local_dumps`, `_enforce_local_dump_retention`, `_resolve_local_dump`; `_run_pg_restore` persists the safety dump locally (and prunes by retention) before the destructive load. `_run_backup_internal` is unchanged (no local copy).
|
||||||
- New **Local Database Restore** card on the Backup page with two token-free paths:
|
- New **Local Database Restore** card on the Backup page with two token-free paths:
|
||||||
- Restore from a locally stored dump — `GET /api/backup/local/dumps`, `POST /api/backup/local/restore`.
|
- Restore from a locally stored dump — `GET /api/backup/local/dumps`, `POST /api/backup/local/restore`.
|
||||||
- Upload a `.sql` dump (e.g. downloaded manually from dropbox.com) and restore it — `POST /api/backup/upload-restore`; the upload is also saved to the local dump store for future use.
|
- Upload a `.sql` dump (e.g. downloaded manually from dropbox.com) and restore it — `POST /api/backup/upload-restore`. The upload itself is not persisted; the automatic pre-restore safety snapshot covers the local copy.
|
||||||
- Both reuse the safe restore path (pre-restore safety dump + automatic rollback) and require `psql`.
|
- Both reuse the safe restore path (pre-restore safety dump + automatic rollback) and require `psql`.
|
||||||
- `templates/backup.html`: new card with a local-dump selector, an upload field, double-guarded restore buttons, and `fmtBytes` sizes.
|
- `templates/backup.html`: new card with a local-dump selector, an upload field, double-guarded restore buttons, and `fmtBytes` sizes.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user