from pathlib import Path from tempfile import NamedTemporaryFile from uuid import UUID, uuid4 from fastapi import ( APIRouter, Depends, File, HTTPException, Path as ApiPath, Request, UploadFile, status, ) from app.auth.metadata_dependencies import ( get_current_metadata_admin, get_metadata_repository, ) from app.core.audit import AuditAction, log_audit_event from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository from app.services.network_import import network_update from app.services.tjnetwork import run_inp router = APIRouter() MAX_INP_FILE_BYTES = 50 * 1024 * 1024 INP_SECTIONS = ("[TITLE]", "[JUNCTIONS]", "[RESERVOIRS]", "[TANKS]", "[PIPES]") async def _get_active_project(project_id: UUID, metadata_repo: MetadataRepository): project = await metadata_repo.get_project_by_id(project_id) if project is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Project not found", ) if project.status != "active": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active", ) return project def _validate_inp_bytes(content: bytes, filename: str) -> str: if Path(filename).suffix.lower() != ".inp": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only .inp model files are accepted", ) if not content: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="INP file is empty", ) if len(content) > MAX_INP_FILE_BYTES: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="INP file exceeds the 50 MiB limit", ) for encoding in ("utf-8-sig", "gb18030"): try: text = content.decode(encoding) break except UnicodeDecodeError: continue else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="INP file encoding is not supported", ) upper_text = text.upper() if not any(section in upper_text for section in INP_SECTIONS): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid INP file structure", ) return text async def _read_upload(file: UploadFile) -> tuple[bytes, str]: filename = Path(file.filename or "").name content = await file.read(MAX_INP_FILE_BYTES + 1) _validate_inp_bytes(content, filename) return content, filename async def _audit_model_change( *, request: Request, current_user, metadata_repo: MetadataRepository, project_id: UUID, action: str, ) -> None: await log_audit_event( action=AuditAction.UPDATE, user_id=current_user.id, project_id=project_id, resource_type="hydraulic_model", resource_id=action, request_data={"operation": action}, ip_address=request.client.host if request.client else None, request_method=request.method, request_path=request.url.path, response_status=status.HTTP_200_OK, session=metadata_repo.session, ) async def _run_uploaded_inp(content: bytes) -> str: target_dir = Path("inp") target_dir.mkdir(parents=True, exist_ok=True) model_name = f"admin_model_{uuid4().hex}" target_path = target_dir / f"{model_name}.inp" target_path.write_bytes(content) return run_inp(model_name) async def _update_from_inp(content: bytes) -> None: temp_path: Path | None = None try: with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file: temp_file.write(content) temp_path = Path(temp_file.name) network_update(str(temp_path)) finally: if temp_path is not None: temp_path.unlink(missing_ok=True) async def _apply_model_update(content: bytes) -> None: try: await _update_from_inp(content) except Exception as exc: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"数据库操作失败: {exc}", ) from exc @router.post( "/admin/projects/{project_id}/model-imports", summary="导入桌面端水力模型", ) async def import_project_model( request: Request, project_id: UUID = ApiPath(...), file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"), current_user=Depends(get_current_metadata_admin), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ) -> dict: project = await _get_active_project(project_id, metadata_repo) content, filename = await _read_upload(file) result = await _run_uploaded_inp(content) await _audit_model_change( request=request, current_user=current_user, metadata_repo=metadata_repo, project_id=project.id, action="import", ) return {"project_id": str(project.id), "filename": filename, "result": result} @router.patch( "/admin/projects/{project_id}/model-imports", summary="更新桌面端水力模型", ) async def update_project_model( request: Request, project_id: UUID = ApiPath(...), file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"), current_user=Depends(get_current_metadata_admin), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ) -> dict: project = await _get_active_project(project_id, metadata_repo) content, filename = await _read_upload(file) await _apply_model_update(content) await _audit_model_change( request=request, current_user=current_user, metadata_repo=metadata_repo, project_id=project.id, action="update", ) return {"project_id": str(project.id), "filename": filename, "updated": True}