298 lines
9.4 KiB
Python
298 lines
9.4 KiB
Python
import json
|
|
from pathlib import Path
|
|
from tempfile import NamedTemporaryFile
|
|
from uuid import UUID, uuid4
|
|
|
|
from fastapi import (
|
|
APIRouter,
|
|
Body,
|
|
Depends,
|
|
File,
|
|
Header,
|
|
HTTPException,
|
|
Path as ApiPath,
|
|
Query,
|
|
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 ChangeSet, import_inp, 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/import",
|
|
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.post(
|
|
"/admin/projects/{project_id}/model/update",
|
|
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}
|
|
|
|
|
|
@router.post("/importinp/", deprecated=True)
|
|
async def legacy_import_inp(
|
|
request: Request,
|
|
network: str = Query(...),
|
|
x_project_id: UUID = Header(..., alias="X-Project-Id"),
|
|
current_user=Depends(get_current_metadata_admin),
|
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
):
|
|
project = await _get_active_project(x_project_id, metadata_repo)
|
|
if network != project.code:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Project scope denied",
|
|
)
|
|
payload = await request.json()
|
|
inp_text = payload.get("inp") if isinstance(payload, dict) else None
|
|
if not isinstance(inp_text, str):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Missing INP content",
|
|
)
|
|
_validate_inp_bytes(inp_text.encode("utf-8"), "model.inp")
|
|
result = import_inp(network, ChangeSet({"inp": inp_text}))
|
|
await _audit_model_change(
|
|
request=request,
|
|
current_user=current_user,
|
|
metadata_repo=metadata_repo,
|
|
project_id=project.id,
|
|
action="import",
|
|
)
|
|
return result
|
|
|
|
|
|
@router.post("/uploadinp/", deprecated=True)
|
|
async def legacy_upload_inp(
|
|
request: Request,
|
|
content: bytes = Body(...),
|
|
name: str = Query(...),
|
|
x_project_id: UUID = Header(..., alias="X-Project-Id"),
|
|
current_user=Depends(get_current_metadata_admin),
|
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
) -> bool:
|
|
project = await _get_active_project(x_project_id, metadata_repo)
|
|
safe_name = Path(name).name
|
|
if safe_name != name:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid INP file name",
|
|
)
|
|
_validate_inp_bytes(content, safe_name)
|
|
target_dir = Path("data")
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
(target_dir / safe_name).write_bytes(content)
|
|
await _audit_model_change(
|
|
request=request,
|
|
current_user=current_user,
|
|
metadata_repo=metadata_repo,
|
|
project_id=project.id,
|
|
action="upload",
|
|
)
|
|
return True
|
|
|
|
|
|
@router.post("/network_project/", deprecated=True)
|
|
async def legacy_network_project(
|
|
request: Request,
|
|
file: UploadFile = File(...),
|
|
x_project_id: UUID = Header(..., alias="X-Project-Id"),
|
|
current_user=Depends(get_current_metadata_admin),
|
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
):
|
|
project = await _get_active_project(x_project_id, metadata_repo)
|
|
content, _ = 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 result
|
|
|
|
|
|
@router.post("/network_update/", deprecated=True)
|
|
async def legacy_network_update(
|
|
request: Request,
|
|
file: UploadFile = File(...),
|
|
x_project_id: UUID = Header(..., alias="X-Project-Id"),
|
|
current_user=Depends(get_current_metadata_admin),
|
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
) -> str:
|
|
project = await _get_active_project(x_project_id, metadata_repo)
|
|
content, _ = 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 json.dumps({"message": "管网更新成功"})
|