"""Backup routes — export/import user data (memories, presets, settings, skills, preferences).""" import json import logging from datetime import datetime from fastapi import APIRouter, HTTPException, Request, Response from core.middleware import require_admin from src.auth_helpers import get_current_user from src.settings import load_settings, save_settings, load_features, save_features logger = logging.getLogger(__name__) def setup_backup_routes(memory_manager, preset_manager, skills_manager) -> APIRouter: router = APIRouter(tags=["backup"]) @router.get("/api/export") async def export_data(request: Request): """Export all user data as a downloadable JSON file.""" require_admin(request) user = get_current_user(request) # Memories (filtered by owner when auth is enabled) memories = memory_manager.load(owner=user) # Presets (shared across users — export all) presets = preset_manager.get_all() # Skills (filtered by owner when auth is enabled) skills = skills_manager.load(owner=user) # Settings settings = load_settings() # Feature flags features = load_features() # User preferences from routes.prefs_routes import _load_for_user preferences = _load_for_user(user) export_data = { "version": 1, "exported_at": datetime.now().isoformat(), "exported_by": user, "memories": memories, "presets": presets, "skills": skills, "settings": settings, "features": features, "preferences": preferences, } filename = f"odysseus_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" return Response( content=json.dumps(export_data, indent=2, ensure_ascii=False), media_type="application/json", headers={"Content-Disposition": f"attachment; filename={filename}"}, ) @router.post("/api/import") async def import_data(request: Request): """Import user data from a previously exported JSON file. Merges with existing data.""" require_admin(request) user = get_current_user(request) try: body = await request.json() except Exception: raise HTTPException(400, "Invalid JSON") if not isinstance(body, dict): raise HTTPException(400, "Expected a JSON object") imported = [] # ── Memories ── if "memories" in body and isinstance(body["memories"], list): existing = memory_manager.load_all() existing_texts = {e.get("text", "").strip().lower() for e in existing} added = 0 for mem in body["memories"]: if not isinstance(mem, dict) or not mem.get("text"): continue if mem["text"].strip().lower() in existing_texts: continue # skip duplicates # Assign owner when auth is enabled if user and not mem.get("owner"): mem["owner"] = user existing.append(mem) existing_texts.add(mem["text"].strip().lower()) added += 1 memory_manager.save(existing) imported.append(f"{added} memories") # ── Skills ── if "skills" in body and isinstance(body["skills"], list): existing = skills_manager.load_all() existing_ids = {s.get("id") for s in existing} existing_titles = {s.get("title", "").strip().lower() for s in existing} added = 0 for skill in body["skills"]: if not isinstance(skill, dict) or not skill.get("title"): continue # Skip if same id or same title already exists if skill.get("id") in existing_ids: continue if skill["title"].strip().lower() in existing_titles: continue if user and not skill.get("owner"): skill["owner"] = user existing.append(skill) existing_ids.add(skill.get("id")) existing_titles.add(skill["title"].strip().lower()) added += 1 skills_manager.save(existing) imported.append(f"{added} skills") # ── Presets ── if "presets" in body and isinstance(body["presets"], dict): current = preset_manager.get_all() for key, value in body["presets"].items(): if isinstance(value, dict): current[key] = value elif isinstance(value, list): current[key] = value preset_manager.save(current) imported.append("presets") # ── Settings ── if "settings" in body and isinstance(body["settings"], dict): current = load_settings() current.update(body["settings"]) save_settings(current) imported.append("settings") # ── Features ── if "features" in body and isinstance(body["features"], dict): current = load_features() current.update(body["features"]) save_features(current) imported.append("features") # ── Preferences ── if "preferences" in body and isinstance(body["preferences"], dict): from routes.prefs_routes import _load_for_user, _save_for_user current = _load_for_user(user) current.update(body["preferences"]) _save_for_user(user, current) imported.append("preferences") if not imported: return {"ok": False, "message": "No recognized data found in the file"} return {"ok": True, "imported": imported, "message": f"Imported: {', '.join(imported)}"} return router