""" Voll-Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config). Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer); Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md. """ import subprocess import tarfile from pathlib import Path from config import MODELS_DIR BACKUP_DIR = Path(MODELS_DIR) / "mc2-backups" SRC_ROOT = Path(__file__).resolve().parents[2] BACKUP_SH = SRC_ROOT / "deploy" / "backup.sh" def _ts(p: Path) -> str: """Zeitstempel aus 'mc2-state-.tar.gz' (Path.stem ließe '.tar' stehen).""" return p.name[len("mc2-state-"):-len(".tar.gz")] def _latest() -> Path | None: if not BACKUP_DIR.exists(): return None snaps = sorted(BACKUP_DIR.glob("mc2-state-*.tar.gz"), reverse=True) return snaps[0] if snaps else None def _components(tarball: Path) -> list[str]: """Top-Level-Einträge im Tarball (zur Anzeige im UI).""" try: with tarfile.open(tarball, "r:gz") as t: top = {m.name.split("/")[1] for m in t.getmembers() if m.name.startswith("./") and "/" in m.name[2:]} top |= {m.name[2:] for m in t.getmembers() if m.name.startswith("./") and "/" not in m.name[2:] and m.isfile()} return sorted(x for x in top if x) except Exception: return [] def backup_now() -> dict: """Erstellt einen Voll-Zustands-Snapshot via deploy/backup.sh.""" try: r = subprocess.run(["/bin/bash", str(BACKUP_SH)], capture_output=True, text=True, timeout=180) if r.returncode != 0: return {"ok": False, "snapshot": "", "files": [], "error": (r.stderr or r.stdout).strip()[-300:]} except Exception as exc: # noqa: BLE001 return {"ok": False, "snapshot": "", "files": [], "error": str(exc)} latest = _latest() if not latest: return {"ok": False, "snapshot": "", "files": [], "error": "Kein Backup erzeugt"} return { "ok": True, "snapshot": _ts(latest), "files": _components(latest), "size_mb": round(latest.stat().st_size / 1_000_000, 2), } def list_backups() -> list[dict]: if not BACKUP_DIR.exists(): return [] out = [] for p in sorted(BACKUP_DIR.glob("mc2-state-*.tar.gz"), reverse=True): out.append({ "snapshot": _ts(p), "file": p.name, "size_mb": round(p.stat().st_size / 1_000_000, 2), }) return out