125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
#!/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()
|