158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
"""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
|