Files
insar-management-system-v2/backend/app/routers/dinsar_products.py
T

262 lines
7.9 KiB
Python

from __future__ import annotations
import os
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, field_validator
from sqlalchemy.ext.asyncio import AsyncSession
from ..config import read_int_env
from ..database import get_db
from ..models import AuthUserORM
from ..services.job_queue_service import job_queue_service
from ..services.result_catalog_service import (
JOB_TYPE_PUBLISH_DINSAR_PRODUCTS,
JOB_TYPE_REBUILD_DINSAR_CATALOG,
TASK_TYPE_PUBLISH_DINSAR_PRODUCTS,
TASK_TYPE_REBUILD_DINSAR_CATALOG,
result_catalog_service,
)
from ..services.task_service import task_service
from .dependencies import (
_add_operation_audit_log,
_get_current_user,
_require_admin,
_validate_export_path,
_validate_root_dir,
)
router = APIRouter()
MAX_SCAN_DIRECTORY_COUNT = read_int_env(
"MAX_SCAN_DIRECTORY_COUNT",
64,
minimum=1,
maximum=500,
)
MAX_SCAN_PATH_LENGTH = read_int_env(
"MAX_SCAN_PATH_LENGTH",
2048,
minimum=64,
maximum=32767,
)
class DinsarProductPublishRequest(BaseModel):
source_directories: List[str]
publish_root: Optional[str] = None
rebuild_catalog: bool = True
@field_validator("source_directories", mode="before")
@classmethod
def _validate_source_directories(cls, value):
if value is None or not isinstance(value, list):
raise ValueError("source_directories must be a list")
normalized: List[str] = []
for raw in value:
path = str(raw or "").strip()
if not path:
continue
if len(path) > MAX_SCAN_PATH_LENGTH:
raise ValueError(
f"source_directories contains a path longer than {MAX_SCAN_PATH_LENGTH} characters."
)
if path not in normalized:
normalized.append(path)
if not normalized:
raise ValueError("source_directories cannot be empty")
if len(normalized) > MAX_SCAN_DIRECTORY_COUNT:
raise ValueError(
f"source_directories exceeds max directory count ({MAX_SCAN_DIRECTORY_COUNT})."
)
return normalized
@field_validator("publish_root", mode="before")
@classmethod
def _validate_publish_root(cls, value):
if value is None:
return None
path = str(value).strip()
if not path:
return None
if len(path) > MAX_SCAN_PATH_LENGTH:
raise ValueError(f"publish_root exceeds max length ({MAX_SCAN_PATH_LENGTH})")
return path
class DinsarCatalogRebuildRequest(BaseModel):
publish_root: Optional[str] = None
full_rebuild: bool = True
@field_validator("publish_root", mode="before")
@classmethod
def _validate_publish_root(cls, value):
if value is None:
return None
path = str(value).strip()
if not path:
return None
if len(path) > MAX_SCAN_PATH_LENGTH:
raise ValueError(f"publish_root exceeds max length ({MAX_SCAN_PATH_LENGTH})")
return path
@router.get("/dinsar-products/catalog-status")
async def get_dinsar_catalog_status(
current_user: AuthUserORM = Depends(_get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
return await result_catalog_service.get_catalog_status(db)
@router.post("/dinsar-products/publish", status_code=202)
async def queue_dinsar_product_publish(
request: DinsarProductPublishRequest,
http_request: Request,
db: AsyncSession = Depends(get_db),
admin_user: AuthUserORM = Depends(_require_admin),
):
_ = admin_user
source_dirs = [_validate_root_dir(path, "source_directories") for path in request.source_directories]
publish_root = request.publish_root
if publish_root:
publish_root = _validate_export_path(publish_root, "publish_root")
task_id = await task_service.create_task(
TASK_TYPE_PUBLISH_DINSAR_PRODUCTS,
"D-InSAR 结果发布",
params={
"source_directories": source_dirs,
"publish_root": publish_root,
"rebuild_catalog": request.rebuild_catalog,
},
db=db,
)
await job_queue_service.create_job(
JOB_TYPE_PUBLISH_DINSAR_PRODUCTS,
payload={
"source_directories": source_dirs,
"publish_root": publish_root,
"rebuild_catalog": request.rebuild_catalog,
},
task_id=task_id,
db=db,
)
await _add_operation_audit_log(
db,
request=http_request,
action="dinsar_products_publish_queued",
resource="dinsar-products/publish",
detail={
"task_id": task_id,
"directory_count": len(source_dirs),
"rebuild_catalog": request.rebuild_catalog,
},
)
await db.commit()
return {
"message": "D-InSAR 结果发布任务已入队",
"task_id": task_id,
}
@router.post("/dinsar-products/rebuild", status_code=202)
async def queue_dinsar_catalog_rebuild(
request: DinsarCatalogRebuildRequest,
http_request: Request,
db: AsyncSession = Depends(get_db),
admin_user: AuthUserORM = Depends(_require_admin),
):
_ = admin_user
publish_root = request.publish_root
if publish_root:
publish_root = _validate_export_path(publish_root, "publish_root")
task_id = await task_service.create_task(
TASK_TYPE_REBUILD_DINSAR_CATALOG,
"D-InSAR 结果目录重建",
params={
"publish_root": publish_root,
"full_rebuild": request.full_rebuild,
},
db=db,
)
await job_queue_service.create_job(
JOB_TYPE_REBUILD_DINSAR_CATALOG,
payload={
"publish_root": publish_root,
"full_rebuild": request.full_rebuild,
},
task_id=task_id,
db=db,
)
await _add_operation_audit_log(
db,
request=http_request,
action="dinsar_catalog_rebuild_queued",
resource="dinsar-products/rebuild",
detail={
"task_id": task_id,
"full_rebuild": request.full_rebuild,
},
)
await db.commit()
return {
"message": "D-InSAR 结果目录重建任务已入队",
"task_id": task_id,
}
@router.get("/dinsar-products")
async def list_dinsar_products(
limit: int = 100,
offset: int = 0,
engine_code: Optional[str] = None,
status: Optional[str] = None,
query: Optional[str] = None,
current_user: AuthUserORM = Depends(_get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
return await result_catalog_service.list_products(
db,
limit=limit,
offset=offset,
engine_code=engine_code,
status=status,
query=query,
)
@router.get("/dinsar-products/{product_db_id}")
async def get_dinsar_product_detail(
product_db_id: int,
current_user: AuthUserORM = Depends(_get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
detail = await result_catalog_service.get_product_detail(db, product_db_id=product_db_id)
if detail is None:
raise HTTPException(status_code=404, detail="Result product not found")
return detail
@router.get("/dinsar-products/{product_db_id}/preview")
async def get_dinsar_product_preview(
product_db_id: int,
current_user: AuthUserORM = Depends(_get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
detail = await result_catalog_service.get_product_detail(db, product_db_id=product_db_id)
if detail is None:
raise HTTPException(status_code=404, detail="Result product not found")
preview_path = str(detail.get("preview_path") or "").strip()
if not preview_path or not os.path.isfile(preview_path):
raise HTTPException(status_code=404, detail="Preview not found")
return FileResponse(preview_path, media_type="image/webp")