Dev build 2026-06-01 21:32

This commit is contained in:
Ivo Oskamp 2026-06-01 21:32:16 +02:00
parent 53a1e30475
commit 9e9ba825e0
3 changed files with 40 additions and 9 deletions

View File

@ -778,17 +778,13 @@ def _real_restore_errors(stderr: str) -> list[str]:
return errors
def _run_pg_restore(dump_bytes: bytes) -> None:
"""Restore a full PostgreSQL dump.
Resets the public schema first so any plain pg_dump (with or without
--clean) restores cleanly into an empty schema. This is destructive: it
drops and recreates the entire public schema before applying the dump.
def _apply_pg_dump(dump_bytes: bytes) -> None:
"""Reset the public schema and load a dump into it.
The dump is applied WITHOUT ON_ERROR_STOP so that benign header SETs the
target server doesn't recognize (e.g. a newer pg_dump emitting
`transaction_timeout` against an older server) don't abort the restore.
stderr is then inspected: any non-benign `ERROR:` line fails the restore.
`transaction_timeout` against an older server) don't abort the load.
stderr is then inspected: any non-benign `ERROR:` line raises.
"""
env = os.environ.copy()
env["PGPASSWORD"] = os.environ.get("POSTGRES_PASSWORD", "")
@ -821,6 +817,35 @@ def _run_pg_restore(dump_bytes: bytes) -> None:
tmp_path.unlink(missing_ok=True)
def _run_pg_restore(dump_bytes: bytes) -> None:
"""Restore a full PostgreSQL dump, with automatic rollback on failure.
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
safety snapshot so a failed restore never leaves an empty/broken database.
"""
try:
safety_bytes, _ = _run_pg_dump()
except Exception:
safety_bytes = None
try:
_apply_pg_dump(dump_bytes)
except Exception as restore_err:
if safety_bytes is not None:
try:
_apply_pg_dump(safety_bytes)
except Exception as rollback_err:
raise RuntimeError(
f"restore failed: {restore_err}; AND rollback failed: {rollback_err}. "
"Database may be in an inconsistent state."
)
raise RuntimeError(
f"restore failed and was rolled back to the pre-restore state: {restore_err}"
)
raise
def _list_pg_dump_paths(client: dropbox.Dropbox, postgres_root: str) -> list[str]:
files = _dropbox_list_files_recursive(client, postgres_root)
return sorted([p for p in files if p.endswith(".sql")], reverse=True)

View File

@ -10,7 +10,7 @@ from __future__ import annotations
from changelog import CHANGELOG
BUILD = 3
BUILD = 4
def _release_version() -> str:

View File

@ -1,5 +1,11 @@
# Develop Changelog
## 2026-06-01 — Full Database Restore: safety dump + automatic rollback
### Fixed
- Full Database Restore could leave the database **empty** if the dump failed to load: the restore dropped and recreated the public schema first and only then applied the dump, so any failure during the load (e.g. a header `transaction_timeout` error with `ON_ERROR_STOP`) wiped all data with nothing to fall back to.
- `routers/backup.py`: split the load into `_apply_pg_dump`; `_run_pg_restore` now takes a safety `pg_dump` of the current database before the destructive load and, if applying the requested dump fails, automatically rolls back to that safety snapshot. A failed restore therefore no longer leaves an empty or broken database.
## 2026-06-01 — Full Database Restore: tolerate PostgreSQL version mismatch
### Fixed