"""Backup/restore endpoints for PersistentLearningStore and state data.""" from __future__ import annotations import json import shutil from datetime import datetime, timezone from pathlib import Path from typing import Any from fastapi import APIRouter from fastapi.responses import FileResponse from fusionagi._logger import logger router = APIRouter() BACKUP_DIR = Path("backups") @router.post("/backup") def create_backup(body: dict[str, Any] | None = None) -> dict[str, Any]: """Create a backup of learning data and state.""" BACKUP_DIR.mkdir(parents=True, exist_ok=True) timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") backup_id = f"backup_{timestamp}" backup_path = BACKUP_DIR / backup_id backup_path.mkdir(parents=True, exist_ok=True) # Backup PersistentLearningStore learning_store_path = Path("data/learning_store.json") if learning_store_path.exists(): shutil.copy2(learning_store_path, backup_path / "learning_store.json") # Backup state files state_path = Path("data/state.json") if state_path.exists(): shutil.copy2(state_path, backup_path / "state.json") # Write manifest manifest = { "backup_id": backup_id, "timestamp": datetime.now(timezone.utc).isoformat(), "files": [f.name for f in backup_path.iterdir() if f.is_file()], } (backup_path / "manifest.json").write_text(json.dumps(manifest, indent=2)) logger.info("Backup created", extra={"backup_id": backup_id, "path": str(backup_path)}) return manifest @router.get("/backups") def list_backups() -> dict[str, Any]: """List available backups.""" if not BACKUP_DIR.exists(): return {"backups": []} backups = [] for d in sorted(BACKUP_DIR.iterdir(), reverse=True): if d.is_dir(): manifest_path = d / "manifest.json" if manifest_path.exists(): manifest = json.loads(manifest_path.read_text()) backups.append(manifest) else: backups.append({"backup_id": d.name, "files": []}) return {"backups": backups} @router.post("/restore/{backup_id}") def restore_backup(backup_id: str) -> dict[str, Any]: """Restore data from a backup.""" backup_path = BACKUP_DIR / backup_id if not backup_path.exists(): return {"error": f"Backup not found: {backup_id}"} data_dir = Path("data") data_dir.mkdir(parents=True, exist_ok=True) restored = [] for f in backup_path.iterdir(): if f.is_file() and f.name != "manifest.json": shutil.copy2(f, data_dir / f.name) restored.append(f.name) logger.info("Backup restored", extra={"backup_id": backup_id, "files": restored}) return {"backup_id": backup_id, "restored_files": restored, "status": "ok"} @router.get("/backup/{backup_id}/download") def download_backup(backup_id: str) -> Any: """Download a backup as a zip archive.""" backup_path = BACKUP_DIR / backup_id if not backup_path.exists(): return {"error": f"Backup not found: {backup_id}"} zip_path = BACKUP_DIR / f"{backup_id}.zip" shutil.make_archive(str(zip_path.with_suffix("")), "zip", str(backup_path)) return FileResponse(str(zip_path), media_type="application/zip", filename=f"{backup_id}.zip")