189 lines
5.9 KiB
Python
189 lines
5.9 KiB
Python
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}
|