1f4c987652
Backup: services/backup.py (Memory-SQLite inkl. WAL/SHM + Configs, retain 7) + deploy/backup.sh + POST /api/system/backup + GET /api/system/backups. Services-Health: GET /api/system/services (aggregierte Reachability + Observability-Links). Frontend: SystemView mit Dienste-Health + Backup- Button + Links; Theme-Toggle (Hell/Dunkel, persistent). Lokal verifiziert: Backup-Snapshot + services-Aggregat (TestClient), Frontend-Build + Browser (Light-Mode bestaetigt via computed styles). Damit Phasen 0-5 lokal fertig. Docs aktualisiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
"""
|
|
Backup der „Verfassung" (Shared-Memory-SQLite) + aller Configs.
|
|
Snapshot in einen Zeitstempel-Ordner; die letzten N bleiben erhalten.
|
|
"""
|
|
|
|
import shutil
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from config import CONFIG_PATH, GATEWAY_CONFIG_PATH, MEMORY_DB, MODELS_DIR
|
|
|
|
BACKUP_DIR = Path(MODELS_DIR) / "mc2-backups"
|
|
RETAIN = 7
|
|
|
|
|
|
def _safe_copy(src: Path, dst_dir: Path) -> str | None:
|
|
try:
|
|
if src and src.exists():
|
|
shutil.copy2(src, dst_dir / src.name)
|
|
return src.name
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def backup_now() -> dict:
|
|
"""Erstellt einen Snapshot (Memory-DB inkl. WAL/SHM + Configs). Alte Snapshots
|
|
über RETAIN hinaus werden entfernt."""
|
|
ts = time.strftime("%Y%m%d-%H%M%S")
|
|
dst = BACKUP_DIR / ts
|
|
dst.mkdir(parents=True, exist_ok=True)
|
|
saved = []
|
|
for src in (MEMORY_DB, Path(str(MEMORY_DB) + "-wal"), Path(str(MEMORY_DB) + "-shm"),
|
|
CONFIG_PATH, GATEWAY_CONFIG_PATH):
|
|
if (name := _safe_copy(src, dst)):
|
|
saved.append(name)
|
|
# Aufräumen: nur die letzten RETAIN Snapshots behalten.
|
|
snaps = sorted([p for p in BACKUP_DIR.iterdir() if p.is_dir()], reverse=True)
|
|
for old in snaps[RETAIN:]:
|
|
shutil.rmtree(old, ignore_errors=True)
|
|
return {"ok": bool(saved), "snapshot": ts, "files": saved, "dir": str(dst)}
|
|
|
|
|
|
def list_backups() -> list[dict]:
|
|
if not BACKUP_DIR.exists():
|
|
return []
|
|
out = []
|
|
for p in sorted([d for d in BACKUP_DIR.iterdir() if d.is_dir()], reverse=True):
|
|
files = [f.name for f in p.iterdir() if f.is_file()]
|
|
out.append({"snapshot": p.name, "files": files})
|
|
return out
|