fixed db stream upload
All checks were successful
Deploy Plattform-Core / test (push) Successful in 41s
Deploy Plattform-Core / deploy (push) Successful in 4s

This commit is contained in:
ValueOn AG 2026-05-24 14:59:20 +02:00
parent c4a9a66c60
commit e7874d8e38
3 changed files with 86 additions and 46 deletions

View file

@ -26,13 +26,14 @@ from modules.system.databaseHealth import (
_scanOrphans,
)
from modules.system.databaseMigration import (
_buildIdRemapFromPayload,
_exportDatabases,
_exportSingleDb,
_getAvailableDatabases,
_getInstanceLabel,
_importDatabases,
_importSingleDb,
_prepareImport,
_loadLiveSystemObjectIds,
_validateImportPayload,
)
@ -479,50 +480,75 @@ def getMigrationExportDownload(
)
@router.post("/migration/prepare-import")
@router.post("/migration/upload-import")
@limiter.limit("5/minute")
async def postMigrationPrepareImport(
async def postMigrationUploadImport(
request: Request,
file: UploadFile = File(...),
currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]:
"""Validate + remap system-object IDs and return metadata for per-DB import.
The remapped payload is stored server-side in memory (returned as opaque token)
so the frontend can drive per-DB import calls without re-uploading.
"""Upload a backup file to disk (chunked, no full RAM load), validate it,
and return a token + metadata for per-DB import.
"""
try:
rawBytes = await file.read()
payload = json.loads(rawBytes.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON file: {e}",
) from e
logger.info("SysAdmin migration prepare-import: user=%s", currentUser.username)
result = _prepareImport(payload)
if not result.get("valid"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"message": "Payload validation failed", "warnings": result.get("warnings", [])},
)
import os
import tempfile
import uuid
token = str(uuid.uuid4())
tmpDir = tempfile.gettempdir()
filePath = os.path.join(tmpDir, f"poweron_import_{token}.json")
logger.info("SysAdmin migration upload-import: user=%s streaming to %s", currentUser.username, filePath)
totalBytes = 0
chunkSize = 1024 * 1024
try:
with open(filePath, "wb") as f:
while True:
chunk = await file.read(chunkSize)
if not chunk:
break
f.write(chunk)
totalBytes += len(chunk)
except Exception as e:
logger.error("Upload-import write failed: %s", e)
if os.path.exists(filePath):
os.remove(filePath)
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)
try:
with open(filePath, "r", encoding="utf-8") as f:
payload = json.load(f)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
os.remove(filePath)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON file: {e}") from e
from modules.system.databaseMigration import _prepareImport
result = _prepareImport(payload)
liveIds = _loadLiveSystemObjectIds()
remap = _buildIdRemapFromPayload(payload, liveIds)
if remap:
logger.info("System-object ID remap: %s", remap)
from modules.system.databaseMigration import _remapSystemObjectIds
_remapSystemObjectIds(payload, remap)
protectedIds = list(set(liveIds.values()))
_pendingImports[token] = {
"payload": payload,
"protectedIds": result["protectedIds"],
"protectedIds": protectedIds,
"filePath": filePath,
}
return {
"valid": True,
"token": token,
"databases": result["databases"],
"warnings": result["warnings"],
"systemObjectsFound": result["systemObjectsFound"],
"protectedIds": result["protectedIds"],
"valid": result.get("valid", False),
"databases": result.get("databases", []),
"warnings": result.get("warnings", []),
"systemObjectsFound": result.get("systemObjectsFound", []),
}
@ -536,7 +562,7 @@ def postMigrationImportSingle(
body: dict,
currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]:
"""Import a single database from a previously prepared payload.
"""Import a single database from a previously uploaded + prepared payload.
Body: ``{token, database, mode}``
"""
@ -545,17 +571,11 @@ def postMigrationImportSingle(
mode = body.get("mode", "merge")
if mode not in ("replace", "merge"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid mode: '{mode}'.",
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid mode: '{mode}'.")
pending = _pendingImports.get(token)
if not pending:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired import token. Please re-upload the file.",
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired import token.")
logger.info("SysAdmin migration import-single: user=%s db=%s mode=%s", currentUser.username, database, mode)
@ -578,8 +598,14 @@ def postMigrationImportDone(
body: dict,
currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]:
"""Clean up the server-side payload cache after import is complete."""
"""Clean up the server-side payload cache and temp file."""
import os
token = body.get("token", "")
if token in _pendingImports:
del _pendingImports[token]
pending = _pendingImports.pop(token, None)
if pending and pending.get("filePath"):
try:
os.remove(pending["filePath"])
except OSError:
pass
return {"ok": True}

View file

@ -303,6 +303,18 @@ def _remapSystemObjectIds(payload: dict, remap: Dict[str, str]) -> dict:
return payload
def _remapDbTables(tables: dict, remap: Dict[str, str]) -> None:
"""In-place remap system-object IDs in a single DB's tables dict."""
if not remap:
return
remapSet = set(remap.keys())
for tableName, rows in tables.items():
if not isinstance(rows, list):
continue
for row in rows:
_remapRowValues(row, remap, remapSet)
def _remapRowValues(row: dict, remap: Dict[str, str], remapSet: Set[str]) -> None:
"""In-place replace string values in a row dict that match a remap key."""
for key, val in row.items():

View file

@ -31,11 +31,13 @@ openpyxl>=3.1.2 # Für Excel-Dateien
python-pptx>=0.6.21 # Für PowerPoint-Dateien
## Data Processing & Analysis
numpy==1.26.3 # Version die mit pandas und matplotlib kompatibel ist
pandas==2.2.3 # Aktuelle Version beibehalten
numpy==1.26.3; python_version < "3.13"
numpy>=2.1.0; python_version >= "3.13"
pandas==2.2.3
## Data Visualization
matplotlib==3.8.0 # Aktuelle Version beibehalten
matplotlib==3.8.0; python_version < "3.13"
matplotlib>=3.9.0; python_version >= "3.13"
seaborn==0.13.0
markdown