swap for 2gb upload db
All checks were successful
Deploy Plattform-Core / test (push) Successful in 47s
Deploy Plattform-Core / deploy (push) Successful in 4s

This commit is contained in:
ValueOn AG 2026-05-24 17:34:24 +02:00
parent e7874d8e38
commit afbb8177a3

View file

@ -26,14 +26,13 @@ from modules.system.databaseHealth import (
_scanOrphans, _scanOrphans,
) )
from modules.system.databaseMigration import ( from modules.system.databaseMigration import (
_buildIdRemapFromPayload,
_exportDatabases, _exportDatabases,
_exportSingleDb, _exportSingleDb,
_getAvailableDatabases, _getAvailableDatabases,
_getInstanceLabel, _getInstanceLabel,
_importDatabases, _importDatabases,
_importSingleDb, _importSingleDb,
_loadLiveSystemObjectIds, _prepareImport,
_validateImportPayload, _validateImportPayload,
) )
@ -487,9 +486,10 @@ async def postMigrationUploadImport(
file: UploadFile = File(...), file: UploadFile = File(...),
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Upload a backup file to disk (chunked, no full RAM load), validate it, """Upload a backup file to disk (chunked), validate, remap IDs,
and return a token + metadata for per-DB import. split into per-DB temp files so the full payload doesn't stay in RAM.
""" """
import gc
import os import os
import tempfile import tempfile
import uuid import uuid
@ -516,7 +516,8 @@ async def postMigrationUploadImport(
os.remove(filePath) os.remove(filePath)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Upload failed: {e}") from e raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Upload failed: {e}") from e
logger.info("SysAdmin migration upload-import: %s bytes written to disk", totalBytes) logger.info("SysAdmin migration upload-import: %s bytes on disk (%.1f MB)",
totalBytes, totalBytes / 1024 / 1024)
try: try:
with open(filePath, "r", encoding="utf-8") as f: with open(filePath, "r", encoding="utf-8") as f:
@ -525,22 +526,41 @@ async def postMigrationUploadImport(
os.remove(filePath) os.remove(filePath)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON file: {e}") from e raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON file: {e}") from e
from modules.system.databaseMigration import _prepareImport try:
os.remove(filePath)
except OSError:
pass
result = _prepareImport(payload) result = _prepareImport(payload)
liveIds = _loadLiveSystemObjectIds() if not result.get("valid"):
remap = _buildIdRemapFromPayload(payload, liveIds) del payload
if remap: gc.collect()
logger.info("System-object ID remap: %s", remap) raise HTTPException(
from modules.system.databaseMigration import _remapSystemObjectIds status_code=status.HTTP_400_BAD_REQUEST,
_remapSystemObjectIds(payload, remap) detail={"message": "Payload validation failed", "warnings": result.get("warnings", [])},
)
protectedIds = list(set(liveIds.values())) protectedIds = result.get("protectedIds", [])
dbFiles = {}
databases = payload.get("databases", {})
for dbName, dbData in databases.items():
dbPath = os.path.join(tmpDir, f"poweron_import_{token}_{dbName}.json")
with open(dbPath, "w", encoding="utf-8") as dbF:
json.dump(dbData, dbF, ensure_ascii=False, default=str)
dbFiles[dbName] = dbPath
del payload
del databases
gc.collect()
logger.info("SysAdmin migration upload-import: split into %d per-DB files, payload freed",
len(dbFiles))
_pendingImports[token] = { _pendingImports[token] = {
"payload": payload, "dbFiles": dbFiles,
"protectedIds": protectedIds, "protectedIds": protectedIds,
"filePath": filePath,
} }
return { return {
@ -566,6 +586,8 @@ def postMigrationImportSingle(
Body: ``{token, database, mode}`` Body: ``{token, database, mode}``
""" """
import os
token = body.get("token", "") token = body.get("token", "")
database = body.get("database", "") database = body.get("database", "")
mode = body.get("mode", "merge") mode = body.get("mode", "merge")
@ -577,10 +599,29 @@ def postMigrationImportSingle(
if not pending: if not pending:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired import token.") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired import token.")
dbFiles = pending.get("dbFiles", {})
dbFilePath = dbFiles.get(database)
if not dbFilePath or not os.path.exists(dbFilePath):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"No data for database '{database}'.",
)
logger.info("SysAdmin migration import-single: user=%s db=%s mode=%s", currentUser.username, database, mode) logger.info("SysAdmin migration import-single: user=%s db=%s mode=%s", currentUser.username, database, mode)
try: try:
result = _importSingleDb(pending["payload"], database, mode, pending["protectedIds"]) with open(dbFilePath, "r", encoding="utf-8") as f:
dbData = json.load(f)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to read import data for '{database}': {e}",
) from e
payload = {"databases": {database: dbData}}
try:
result = _importSingleDb(payload, database, mode, pending["protectedIds"])
except Exception as e: except Exception as e:
logger.error("Import-single failed for %s: %s", database, e) logger.error("Import-single failed for %s: %s", database, e)
raise HTTPException( raise HTTPException(
@ -598,14 +639,15 @@ def postMigrationImportDone(
body: dict, body: dict,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Clean up the server-side payload cache and temp file.""" """Clean up the per-DB temp files."""
import os import os
token = body.get("token", "") token = body.get("token", "")
pending = _pendingImports.pop(token, None) pending = _pendingImports.pop(token, None)
if pending and pending.get("filePath"): if pending:
try: for dbPath in pending.get("dbFiles", {}).values():
os.remove(pending["filePath"]) try:
except OSError: os.remove(dbPath)
pass except OSError:
pass
return {"ok": True} return {"ok": True}