mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-09-04 12:02:07 +00:00
The package carried no ruff configuration at all, so it ran on the defaults (E4+E7+E9+F) while infinito-nexus-core, its only consumer, holds itself to a far wider selection. Measured against that selection this tree had 224 findings. It now has none. The selector list is core's verbatim so both repositories answer to one bar. target-version stays py39 rather than core's py311, because requires-python still declares >=3.9 and pyupgrade would otherwise propose syntax the declared minimum cannot run. Every ignore carries its reason: S603/S607 in particular, since running docker and dump binaries from PATH in list form is this tool's whole job and is already injection-safe. Two conversions are judgement rather than mechanics. os.path.join(dir, '') was the rsync idiom for a trailing separator, which Path drops, so it becomes an explicit os.sep. os.path.abspath stays where Path.resolve() would follow symlinks and let a symlinked volume test as inside the snapshot subject. BREAKING CHANGE: VersionMismatch is renamed VersionMismatchError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
3.0 KiB
Python
104 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from baudolo.restore.run import docker_exec
|
|
|
|
from .version import guard
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable, Iterator
|
|
|
|
_SUPERUSER_ONLY_PREFIXES = (b"COMMENT ON EXTENSION", b"ALTER DEFAULT PRIVILEGES")
|
|
_EMPTY_PRECLEAN_SQL = Path(__file__).parent / "empty_preclean.sql"
|
|
|
|
|
|
def filter_superuser_only_lines(lines: Iterable[bytes]) -> Iterator[bytes]:
|
|
"""Drop superuser-only statements an app-level psql replay cannot run.
|
|
|
|
Args:
|
|
lines: dump lines including their trailing newlines.
|
|
|
|
Yields:
|
|
Every line except top-level statements starting with a superuser-only
|
|
prefix. Lines inside COPY ... FROM stdin data blocks are passed
|
|
through untouched: a data row may legally start with the same bytes,
|
|
and dropping it would silently corrupt the restored table.
|
|
"""
|
|
in_copy = False
|
|
for line in lines:
|
|
if in_copy:
|
|
yield line
|
|
if line.rstrip(b"\r\n") == b"\\.":
|
|
in_copy = False
|
|
continue
|
|
if line.startswith(b"COPY ") and line.rstrip(b"\r\n").endswith(b"FROM stdin;"):
|
|
in_copy = True
|
|
yield line
|
|
continue
|
|
if line.startswith(_SUPERUSER_ONLY_PREFIXES):
|
|
continue
|
|
yield line
|
|
|
|
|
|
def restore_postgres_sql(
|
|
*,
|
|
container: str,
|
|
db_name: str,
|
|
user: str,
|
|
password: str,
|
|
sql_path: str,
|
|
empty: bool,
|
|
check_version: bool = True,
|
|
) -> None:
|
|
if not Path(sql_path).is_file():
|
|
raise FileNotFoundError(sql_path)
|
|
|
|
if check_version:
|
|
guard(
|
|
sql_path=sql_path,
|
|
engine="postgres",
|
|
container=container,
|
|
user=user,
|
|
password=password,
|
|
)
|
|
|
|
docker_env = {"PGPASSWORD": password}
|
|
|
|
if empty:
|
|
with _EMPTY_PRECLEAN_SQL.open(encoding="utf-8") as preclean:
|
|
drop_sql = preclean.read()
|
|
docker_exec(
|
|
container,
|
|
["psql", "-v", "ON_ERROR_STOP=1", "-U", user, "-d", db_name],
|
|
stdin=drop_sql.encode(),
|
|
docker_env=docker_env,
|
|
)
|
|
|
|
# Filter into a spooled temp file instead of building the whole dump in
|
|
# memory: production dumps reach many GB and the previous read/splitlines/
|
|
# join needed roughly three times the dump size in RSS.
|
|
with Path(sql_path).open("rb") as src, tempfile.TemporaryFile() as filtered:
|
|
for line in filter_superuser_only_lines(src):
|
|
filtered.write(line)
|
|
filtered.seek(0)
|
|
docker_exec(
|
|
container,
|
|
[
|
|
"psql",
|
|
"--single-transaction",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
user,
|
|
"-d",
|
|
db_name,
|
|
],
|
|
stdin=filtered,
|
|
docker_env=docker_env,
|
|
)
|
|
|
|
print(f"PostgreSQL restore complete for db '{db_name}'.")
|