import logging from sqlalchemy import select from sqlalchemy.orm import selectinload from fabledassistant.models import async_session from fabledassistant.models.conversation import Conversation, Message from fabledassistant.models.note import Note from fabledassistant.models.setting import Setting from fabledassistant.models.user import User logger = logging.getLogger(__name__) async def export_full_backup() -> dict: """Export all users, notes, conversations+messages, and settings as JSON.""" async with async_session() as session: users = (await session.execute(select(User))).scalars().all() notes = (await session.execute(select(Note))).scalars().all() conversations = ( await session.execute( select(Conversation).options(selectinload(Conversation.messages)) ) ).scalars().all() settings = (await session.execute(select(Setting))).scalars().all() return { "version": 1, "scope": "full", "_security_notice": ( "This backup contains hashed passwords. " "Store it securely and restrict access." ), "users": [ { "id": u.id, "username": u.username, "email": u.email, "password_hash": u.password_hash, "role": u.role, "created_at": u.created_at.isoformat(), } for u in users ], "notes": [ { "id": n.id, "user_id": n.user_id, "title": n.title, "body": n.body, "tags": n.tags or [], "parent_id": n.parent_id, "status": n.status, "priority": n.priority, "due_date": n.due_date.isoformat() if n.due_date else None, "created_at": n.created_at.isoformat(), "updated_at": n.updated_at.isoformat(), } for n in notes ], "conversations": [ { "id": c.id, "user_id": c.user_id, "title": c.title, "model": c.model, "created_at": c.created_at.isoformat(), "updated_at": c.updated_at.isoformat(), "messages": [ { "id": m.id, "role": m.role, "content": m.content, "context_note_id": m.context_note_id, "created_at": m.created_at.isoformat(), } for m in c.messages ], } for c in conversations ], "settings": [ { "user_id": s.user_id, "key": s.key, "value": s.value, } for s in settings ], } async def export_user_backup(user_id: int) -> dict: """Export a single user's data as JSON.""" async with async_session() as session: user = await session.get(User, user_id) notes = ( await session.execute(select(Note).where(Note.user_id == user_id)) ).scalars().all() conversations = ( await session.execute( select(Conversation) .options(selectinload(Conversation.messages)) .where(Conversation.user_id == user_id) ) ).scalars().all() settings = ( await session.execute( select(Setting).where(Setting.user_id == user_id) ) ).scalars().all() return { "version": 1, "scope": "user", "user": { "id": user.id, "username": user.username, "email": user.email, "role": user.role, "created_at": user.created_at.isoformat(), } if user else None, "notes": [ { "id": n.id, "title": n.title, "body": n.body, "tags": n.tags or [], "parent_id": n.parent_id, "status": n.status, "priority": n.priority, "due_date": n.due_date.isoformat() if n.due_date else None, "created_at": n.created_at.isoformat(), "updated_at": n.updated_at.isoformat(), } for n in notes ], "conversations": [ { "id": c.id, "title": c.title, "model": c.model, "created_at": c.created_at.isoformat(), "updated_at": c.updated_at.isoformat(), "messages": [ { "id": m.id, "role": m.role, "content": m.content, "context_note_id": m.context_note_id, "created_at": m.created_at.isoformat(), } for m in c.messages ], } for c in conversations ], "settings": [ {"key": s.key, "value": s.value} for s in settings ], } async def restore_full_backup(data: dict) -> dict: """Restore from a full backup JSON. Returns stats about what was restored.""" from datetime import date, datetime, timezone stats = {"users": 0, "notes": 0, "conversations": 0, "messages": 0, "settings": 0} async with async_session() as session: # Restore users user_id_map: dict[int, int] = {} for u_data in data.get("users", []): old_id = u_data["id"] user = User( username=u_data["username"], email=u_data.get("email"), password_hash=u_data["password_hash"], role=u_data.get("role", "user"), created_at=datetime.fromisoformat(u_data["created_at"]) if u_data.get("created_at") else datetime.now(timezone.utc), ) session.add(user) await session.flush() user_id_map[old_id] = user.id stats["users"] += 1 # Restore notes note_id_map: dict[int, int] = {} for n_data in data.get("notes", []): old_id = n_data.get("id") mapped_user_id = user_id_map.get(n_data.get("user_id", 0)) if mapped_user_id is None: continue due = None if n_data.get("due_date"): due = date.fromisoformat(n_data["due_date"]) note = Note( user_id=mapped_user_id, title=n_data.get("title", ""), body=n_data.get("body", ""), tags=n_data.get("tags", []), parent_id=n_data.get("parent_id"), status=n_data.get("status"), priority=n_data.get("priority"), due_date=due, created_at=datetime.fromisoformat(n_data["created_at"]) if n_data.get("created_at") else datetime.now(timezone.utc), updated_at=datetime.fromisoformat(n_data["updated_at"]) if n_data.get("updated_at") else datetime.now(timezone.utc), ) session.add(note) await session.flush() if old_id is not None: note_id_map[old_id] = note.id stats["notes"] += 1 # Restore conversations + messages for c_data in data.get("conversations", []): mapped_user_id = user_id_map.get(c_data.get("user_id", 0)) if mapped_user_id is None: continue conv = Conversation( user_id=mapped_user_id, title=c_data.get("title", ""), model=c_data.get("model", ""), created_at=datetime.fromisoformat(c_data["created_at"]) if c_data.get("created_at") else datetime.now(timezone.utc), updated_at=datetime.fromisoformat(c_data["updated_at"]) if c_data.get("updated_at") else datetime.now(timezone.utc), ) session.add(conv) await session.flush() stats["conversations"] += 1 for m_data in c_data.get("messages", []): msg = Message( conversation_id=conv.id, role=m_data["role"], content=m_data.get("content", ""), context_note_id=note_id_map.get(m_data.get("context_note_id")) if m_data.get("context_note_id") else None, created_at=datetime.fromisoformat(m_data["created_at"]) if m_data.get("created_at") else datetime.now(timezone.utc), ) session.add(msg) stats["messages"] += 1 # Restore settings for s_data in data.get("settings", []): mapped_user_id = user_id_map.get(s_data.get("user_id", 0)) if mapped_user_id is None: continue setting = Setting( user_id=mapped_user_id, key=s_data["key"], value=s_data.get("value", ""), ) session.add(setting) stats["settings"] += 1 await session.commit() logger.info("Restored backup: %s", stats) return stats