diff --git a/modules/routes/routeAdminDatabaseHealth.py b/modules/routes/routeAdminDatabaseHealth.py index 84e3443f..560c0fbd 100644 --- a/modules/routes/routeAdminDatabaseHealth.py +++ b/modules/routes/routeAdminDatabaseHealth.py @@ -377,16 +377,38 @@ async def postMigrationImport( # Per-DB endpoints (progress-friendly) # --------------------------------------------------------------------------- +_pendingExports: Dict[str, dict] = {} + + +@router.post("/migration/export-start") +@limiter.limit("10/minute") +def postMigrationExportStart( + request: Request, + currentUser: User = Depends(requireSysAdmin), +) -> Dict[str, Any]: + """Start an export session. Returns a token for subsequent per-DB calls.""" + import uuid + token = str(uuid.uuid4()) + _pendingExports[token] = {"databases": {}} + logger.info("SysAdmin migration export-start: user=%s token=%s", currentUser.username, token) + return {"token": token} + + @router.get("/migration/export-single") @limiter.limit("60/minute") def getMigrationExportSingle( request: Request, + token: str, database: str, currentUser: User = Depends(requireSysAdmin), ) -> Dict[str, Any]: - """Export a single database as JSON (used by the frontend for per-DB progress).""" + """Export a single database and store it server-side. Returns only metadata.""" from modules.shared.dbRegistry import getRegisteredDatabases + pending = _pendingExports.get(token) + if not pending: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid export token.") + if database not in getRegisteredDatabases(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -404,7 +426,57 @@ def getMigrationExportSingle( detail=f"Export failed for '{database}': {e}", ) from e - return {"database": database, "data": dbPayload} + pending["databases"][database] = dbPayload + logger.info("SysAdmin migration export-single done: user=%s db=%s tables=%s records=%s", + currentUser.username, database, dbPayload.get("tableCount", 0), dbPayload.get("totalRecords", 0)) + + return { + "database": database, + "tableCount": dbPayload.get("tableCount", 0), + "totalRecords": dbPayload.get("totalRecords", 0), + } + + +@router.get("/migration/export-download") +@limiter.limit("5/minute") +def getMigrationExportDownload( + request: Request, + token: str, + filename: str = "backup.json", + currentUser: User = Depends(requireSysAdmin), +) -> StreamingResponse: + """Assemble and stream the final export file from server-side data.""" + from datetime import datetime, timezone + + pending = _pendingExports.pop(token, None) + if not pending or not pending.get("databases"): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired export token.") + + databases = pending["databases"] + totalTables = sum(d.get("tableCount", 0) for d in databases.values()) + totalRecords = sum(d.get("totalRecords", 0) for d in databases.values()) + + exportData = { + "meta": { + "exportedAt": datetime.now(timezone.utc).isoformat(), + "version": "1.0", + "databaseCount": len(databases), + "totalTables": totalTables, + "totalRecords": totalRecords, + }, + "databases": databases, + } + + logger.info("SysAdmin migration export-download: user=%s dbs=%s tables=%s records=%s", + currentUser.username, len(databases), totalTables, totalRecords) + + content = json.dumps(exportData, ensure_ascii=False, default=str) + + return StreamingResponse( + iter([content]), + media_type="application/json", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) @router.post("/migration/prepare-import")