#!/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()