ruff was never wired into this repository: no target, no CI step, no pin. It reported 45 findings across sources and tests, so nothing enforced what the codebase already mostly followed. Adds `make ruff` (check + format --check), `make ruff-fix`, and `make lint` as its alias, and makes `make test` run lint as a fourth parallel spur. The CI workflow calls `make test`, so it is covered there too. The linter is pinned in a `lint` extra: a ruff minor bump changes which rules fire, and with the suite gating on a clean run an unpinned linter would fail it on an unrelated day. The 45 findings are fixed rather than configured away. Three needed a decision instead of the mechanical fix: - The generation timestamp keeps its local wall clock (DTZ005 waived). Generations sort by that name, and UTC would order new ones before the existing ones wherever the offset is positive - "newest generation" is what every restore path selects on. - The per-volume `copy` closure now binds volume_name and vol_dir as default arguments (B023). It only worked because it is called inside the same iteration. - The two CLI top-level handlers keep their blind except (BLE001 waived): turning any failure into exit 1 is what a CLI boundary is for. The two in run.py did not need it and were narrowed to what they actually catch. Also drops the comments that restate the code: the section banners in restore/__main__.py, the filename repeated as line 1 of nine test files, step narration above the statement it narrates, and a block in app.py documenting parameters that had moved to another module. What names a trip-wire stays - the snapshot destination rule, the mysql-binary absence in MariaDB 11 images, the session-scoped FOREIGN_KEY_CHECKS, the spooled temp file for multi-GB dumps, and the negative control that loses its discriminating power if it ever passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
2.5 KiB
Python
91 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
from ..run import docker_exec, docker_exec_sh
|
|
|
|
|
|
def _pick_client(container: str) -> str:
|
|
"""
|
|
Prefer 'mariadb', fallback to 'mysql'.
|
|
Some MariaDB images no longer ship a 'mysql' binary, so we must not assume it exists.
|
|
"""
|
|
script = r"""
|
|
set -eu
|
|
if command -v mariadb >/dev/null 2>&1; then echo mariadb; exit 0; fi
|
|
if command -v mysql >/dev/null 2>&1; then echo mysql; exit 0; fi
|
|
exit 42
|
|
"""
|
|
try:
|
|
out = docker_exec_sh(container, script, capture=True).stdout.decode().strip()
|
|
if not out:
|
|
raise RuntimeError("empty client detection output")
|
|
return out
|
|
except Exception:
|
|
print(
|
|
"ERROR: neither 'mariadb' nor 'mysql' found in container.", file=sys.stderr
|
|
)
|
|
raise
|
|
|
|
|
|
def restore_mariadb_sql(
|
|
*,
|
|
container: str,
|
|
db_name: str,
|
|
user: str,
|
|
password: str,
|
|
sql_path: str,
|
|
empty: bool,
|
|
) -> None:
|
|
client = _pick_client(container)
|
|
|
|
if not os.path.isfile(sql_path):
|
|
raise FileNotFoundError(sql_path)
|
|
|
|
if empty:
|
|
# Do not hardcode 'mysql': MariaDB 11 images may not ship that binary.
|
|
result = docker_exec(
|
|
container,
|
|
[
|
|
client,
|
|
"-u",
|
|
user,
|
|
f"--password={password}",
|
|
"-N",
|
|
"-e",
|
|
f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{db_name}';",
|
|
],
|
|
capture=True,
|
|
)
|
|
tables = result.stdout.decode().split()
|
|
|
|
if tables:
|
|
# SET FOREIGN_KEY_CHECKS is session-scoped, so it must share one
|
|
# client session with the DROPs or FK constraints still fire.
|
|
drop_sql = (
|
|
"SET FOREIGN_KEY_CHECKS=0; "
|
|
+ " ".join(
|
|
f"DROP TABLE IF EXISTS `{db_name}`.`{tbl}`;" for tbl in tables
|
|
)
|
|
+ " SET FOREIGN_KEY_CHECKS=1;"
|
|
)
|
|
docker_exec(
|
|
container,
|
|
[
|
|
client,
|
|
"-u",
|
|
user,
|
|
f"--password={password}",
|
|
"-e",
|
|
drop_sql,
|
|
],
|
|
)
|
|
|
|
with open(sql_path, "rb") as f:
|
|
docker_exec(
|
|
container, [client, "-u", user, f"--password={password}", db_name], stdin=f
|
|
)
|
|
|
|
print(f"MariaDB/MySQL restore complete for db '{db_name}'.")
|