47f7a85510
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>
71 lines
2.6 KiB
Python
71 lines
2.6 KiB
Python
"""Zeitmaschine — Snapshots ansehen und per Klick zu einem Stand zurückkehren.
|
|
|
|
Die Maschinerie existiert komplett (deploy/backup.sh Timer 03:30, Off-Box-Kopie,
|
|
deploy/restore.sh mit Pre-Restore-Sicherung); hier kommt nur der letzte Meter dazu:
|
|
Liste + Inhalt in der UI und ein Restore-Start als EIGENE systemd-Unit — restore.sh
|
|
stoppt mission-control-2 selbst, als Kind des Backends stürbe der Lauf mittendrin."""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from services import backup as backup_svc
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
RESTORE_SH = _REPO_ROOT / "deploy" / "restore.sh"
|
|
_FILE_RX = re.compile(r"^mc2-state-[0-9T:_-]+\.tar\.gz$")
|
|
|
|
|
|
class RestoreIn(BaseModel):
|
|
file: str
|
|
|
|
|
|
@router.get("/zeitmaschine")
|
|
def list_snapshots() -> dict:
|
|
return {"available": os.name == "posix", "backups": backup_svc.list_backups()}
|
|
|
|
|
|
@router.get("/zeitmaschine/inhalt")
|
|
def snapshot_contents(file: str) -> dict:
|
|
"""Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …)."""
|
|
if not _FILE_RX.match(file):
|
|
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
|
p = backup_svc.BACKUP_DIR / file
|
|
if not p.is_file():
|
|
raise HTTPException(404, "Snapshot nicht gefunden.")
|
|
return {"file": file, "components": backup_svc.snapshot_components(p)}
|
|
|
|
|
|
@router.post("/zeitmaschine/restore")
|
|
def restore(body: RestoreIn) -> dict:
|
|
"""Wiederherstellung starten (detached). restore.sh macht VORHER selbst ein
|
|
Sicherheits-Backup des aktuellen Zustands — der Schritt ist also reversibel."""
|
|
if os.name != "posix":
|
|
raise HTTPException(400, "Wiederherstellen geht nur auf der Box.")
|
|
if not _FILE_RX.match(body.file):
|
|
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
|
if not (backup_svc.BACKUP_DIR / body.file).is_file():
|
|
raise HTTPException(404, "Snapshot nicht gefunden.")
|
|
|
|
try:
|
|
from services import announce
|
|
announce.add(f"Zeitmaschine: Wiederherstellung von {body.file} gestartet — "
|
|
"die Dienste starten gleich neu.", "[Zeitmaschine]", "zeitmaschine", "silent")
|
|
except Exception:
|
|
pass
|
|
|
|
unit = f"mc2-restore-{int(time.time())}"
|
|
r = subprocess.run(
|
|
["systemd-run", "--user", "--collect", f"--unit={unit}",
|
|
"/bin/bash", str(RESTORE_SH), "--yes", body.file],
|
|
capture_output=True, text=True, timeout=20)
|
|
if r.returncode != 0:
|
|
raise HTTPException(500, f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}")
|
|
return {"ok": True, "unit": unit}
|