Files
mission-control-v2/backend/services/backup.py
T
Hitonabi 47f7a85510 Ruff-Cleanup: ganzes MC2-Repo lint-grün + projekt-passende ruff.toml
Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo
auf. Aufgeraeumt:
- ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except,
  S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI-
  Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports
  geschuetzt (F401).
- ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports,
  ueberfluessige noqa) auto-behoben.
- 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat
  geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string),
  UP035 (veraltete typing-Imports).
Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:53:36 +02:00

79 lines
2.6 KiB
Python

"""
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-<ts>.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 snapshot_components(tarball: Path) -> list[str]:
"""Öffentliche Sicht auf die Snapshot-Komponenten (Zeitmaschine-Detail in der UI)."""
return _components(tarball)
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:
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