mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-24 14:54:32 +00:00
A restore with --empty destroys before it replays: the pre-clean drops the schema in one session and the dump goes in the next, with no rollback across the two. A dump the engine cannot parse therefore does not fail harmlessly, it leaves an emptied database behind. The version each side is on decides that up front, so the refusal lands before the first session opens. Both engines state their origin in the dump's own header and spell it differently. Postgres names the source server; MariaDB opens with mariadb-dump's own version and names the server further down, so matching the first number would read the tool on one engine and the server on the other. A pg_dumpall stream carries no version line of its own at all - the first belongs to the first database's embedded pg_dump output, arbitrarily far down - hence the deep scan. Restoring forward across a major version stays allowed; that is the upgrade path. Only backward is refused, with --no-version-check as the way out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
93 lines
2.4 KiB
Python
93 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def run(
|
|
cmd: list[str],
|
|
*,
|
|
stdin=None,
|
|
capture: bool = False,
|
|
env: dict | None = None,
|
|
) -> subprocess.CompletedProcess:
|
|
try:
|
|
kwargs: dict = {
|
|
"check": True,
|
|
"capture_output": capture,
|
|
"env": env,
|
|
}
|
|
|
|
# If stdin is raw data (bytes/str), pass it via input=.
|
|
# IMPORTANT: when using input=..., do NOT pass stdin=... as well.
|
|
if isinstance(stdin, (bytes, str)):
|
|
kwargs["input"] = stdin
|
|
else:
|
|
kwargs["stdin"] = stdin
|
|
|
|
return subprocess.run(cmd, **kwargs) # noqa: PLW1510 - check lives in kwargs
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
msg = f"ERROR: command failed ({e.returncode}): {' '.join(cmd)}"
|
|
print(msg, file=sys.stderr)
|
|
for stream in (e.stdout, e.stderr):
|
|
if not stream:
|
|
continue
|
|
try:
|
|
print(stream.decode(), file=sys.stderr)
|
|
except (UnicodeDecodeError, AttributeError):
|
|
print(stream, file=sys.stderr)
|
|
raise
|
|
|
|
|
|
def stdout_of(completed: subprocess.CompletedProcess) -> str:
|
|
"""The captured stdout as stripped text, whether it came back bytes or str."""
|
|
raw = completed.stdout or b""
|
|
return (raw.decode() if isinstance(raw, bytes) else raw).strip()
|
|
|
|
|
|
def docker_exec(
|
|
container: str,
|
|
argv: list[str],
|
|
*,
|
|
stdin=None,
|
|
capture: bool = False,
|
|
env: dict | None = None,
|
|
docker_env: dict[str, str] | None = None,
|
|
) -> subprocess.CompletedProcess:
|
|
cmd: list[str] = ["docker", "exec", "-i"]
|
|
if docker_env:
|
|
for k, v in docker_env.items():
|
|
cmd.extend(["-e", f"{k}={v}"])
|
|
cmd.extend([container, *argv])
|
|
return run(cmd, stdin=stdin, capture=capture, env=env)
|
|
|
|
|
|
def docker_exec_sh(
|
|
container: str,
|
|
script: str,
|
|
*,
|
|
stdin=None,
|
|
capture: bool = False,
|
|
env: dict | None = None,
|
|
docker_env: dict[str, str] | None = None,
|
|
) -> subprocess.CompletedProcess:
|
|
return docker_exec(
|
|
container,
|
|
["sh", "-lc", script],
|
|
stdin=stdin,
|
|
capture=capture,
|
|
env=env,
|
|
docker_env=docker_env,
|
|
)
|
|
|
|
|
|
def docker_volume_exists(volume: str) -> bool:
|
|
p = subprocess.run(
|
|
["docker", "volume", "inspect", volume],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
check=False,
|
|
)
|
|
return p.returncode == 0
|