This commit is contained in:
2026-01-28 09:51:46 +01:00
parent e8c322145a
commit 7ebe32edc0
22 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import gzip
import shutil
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
MYSQLDUMP = PROJECT_ROOT / "bin" / "mysqldump.exe"
def load_dotenv(dotenv_path: Path) -> None:
if not dotenv_path.exists():
return
for line in dotenv_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip()
v = v.strip().strip('"').strip("'")
os.environ.setdefault(k, v)
def require_env(name: str) -> str:
v = os.getenv(name)
if not v:
raise SystemExit(f"Chybí proměnná {name} (dej ji do .env nebo env).")
return v
def run(cmd: list[str], *, cwd: Path | None = None) -> None:
# pro ladění si můžeš odkomentovat print(cmd)
p = subprocess.run(cmd, cwd=str(cwd) if cwd else None)
if p.returncode != 0:
raise SystemExit(f"Příkaz selhal (exit={p.returncode}): {' '.join(cmd)}")
def main() -> None:
here = Path(__file__).resolve().parent
load_dotenv(here / ".env")
host = require_env("KB_MYSQL_HOST")
port = require_env("KB_MYSQL_PORT")
user = require_env("KB_MYSQL_USER")
password = require_env("KB_MYSQL_PASSWORD")
db = require_env("KB_MYSQL_DB")
backup_dir = Path(os.getenv("KB_BACKUP_DIR", "./backups")).resolve()
keep_days = int(os.getenv("KB_KEEP_DAYS", "30"))
backup_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
sql_path = backup_dir / f"kanboard_{db}_{ts}.sql"
gz_path = backup_dir / f"{sql_path.name}.gz"
# Bezpečně: vytvoříme dočasný mysql config soubor, aby heslo nebylo v "ps"
cnf_path = backup_dir / f".mysql_{db}_{ts}.cnf"
cnf_path.write_text(
"[client]\n"
f"host={host}\n"
f"port={port}\n"
f"user={user}\n"
f"password={password}\n",
encoding="utf-8"
)
try:
# omezit práva (na Linuxu/Unraid ok; na Windows to ignoruj)
try:
os.chmod(cnf_path, 0o600)
except Exception:
pass
# Mysqldump kompletní pro obnovu
dump_cmd = [
str(MYSQLDUMP),
f"--defaults-extra-file={str(cnf_path)}",
"--single-transaction",
"--routines",
"--triggers",
"--events",
"--hex-blob",
"--default-character-set=utf8mb4",
"--set-gtid-purged=OFF",
"--databases", db,
]
print(f"[OK] Dělám dump DB '{db}' -> {gz_path}")
# Dump do .sql a rovnou gzip
with subprocess.Popen(dump_cmd, stdout=subprocess.PIPE) as proc:
if proc.stdout is None:
raise SystemExit("Nepodařilo se získat stdout z mysqldump.")
with gzip.open(gz_path, "wb", compresslevel=6) as gz:
shutil.copyfileobj(proc.stdout, gz)
rc = proc.wait()
if rc != 0:
raise SystemExit(f"mysqldump selhal (exit={rc}).")
# Malá kontrola velikosti
size = gz_path.stat().st_size
if size < 200: # hodně hrubé
raise SystemExit(f"Dump je podezřele malý ({size} B) něco je špatně.")
# Retence
cutoff = datetime.now() - timedelta(days=keep_days)
for f in backup_dir.glob("kanboard_*_*.sql.gz"):
try:
if datetime.fromtimestamp(f.stat().st_mtime) < cutoff:
f.unlink(missing_ok=True)
except Exception:
pass
print(f"[OK] Hotovo. Velikost: {size/1024/1024:.2f} MB")
finally:
try:
cnf_path.unlink(missing_ok=True)
except Exception:
pass
if __name__ == "__main__":
main()