Add result delivery workflow

This commit is contained in:
2026-06-30 15:54:26 +08:00
parent 9c80b95385
commit 04210b0551
18 changed files with 2021 additions and 98 deletions
+2
View File
@@ -21,6 +21,7 @@ from . import (
orbit,
pairing,
ps_products,
result_deliveries,
sbas_insar_products,
radar,
root_registry,
@@ -55,6 +56,7 @@ def include_all_routers(router: APIRouter) -> None:
router.include_router(pairing.router)
router.include_router(dinsar.router)
router.include_router(dinsar_products.router)
router.include_router(result_deliveries.router)
router.include_router(dinsar_production.router)
router.include_router(landsar_lt1_production.router)
router.include_router(sbas_insar_production.router)
+11 -1
View File
@@ -296,6 +296,12 @@ def _is_high_risk_write_path(path: str, method: str) -> bool:
return any(normalized.startswith(prefix) for prefix in HIGH_RISK_WRITE_PATH_PREFIXES)
def _is_user_self_service_write_path(path: str, method: str) -> bool:
normalized = _normalize_request_path(path)
upper_method = (method or "").upper()
return upper_method == "POST" and normalized == "/api/result-deliveries"
def _get_client_ip(request: Request) -> Optional[str]:
direct_ip = request.client.host if request.client else None
if direct_ip in _TRUSTED_PROXY_IPS:
@@ -400,7 +406,11 @@ async def _require_auth(request: Request, db: AsyncSession = Depends(get_db)):
await db.commit()
raise HTTPException(status_code=401, detail="Authentication required.")
if (not _is_read_only_operation(path, method)) and user.role != ROLE_ADMIN:
if (
(not _is_read_only_operation(path, method))
and user.role != ROLE_ADMIN
and not _is_user_self_service_write_path(path, method)
):
if _is_high_risk_write_path(path, method):
await add_audit_log(
db,
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import mimetypes
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 ..database import get_db
from ..models import AuthUserORM
from ..services.result_delivery_service import result_delivery_service
from .dependencies import _add_operation_audit_log, _get_current_user
router = APIRouter()
class ResultDeliveryCreateRequest(BaseModel):
channel: str
product_ids: Optional[List[int]] = None
compat_result_ids: Optional[List[int]] = None
package_mode: str = "directory"
include_checksums: Optional[bool] = None
@field_validator("channel", mode="before")
@classmethod
def _validate_channel(cls, value):
text = str(value or "").strip().lower()
if not text:
raise ValueError("channel is required")
return text
@field_validator("package_mode", mode="before")
@classmethod
def _validate_package_mode(cls, value):
text = str(value or "directory").strip().lower()
if text not in {"directory", "zip"}:
raise ValueError("package_mode must be directory or zip")
return text
@router.get("/result-deliveries/channels")
async def get_result_delivery_channels(
current_user: AuthUserORM = Depends(_get_current_user),
):
_ = current_user
return {"items": result_delivery_service.channels()}
@router.post("/result-deliveries", status_code=202)
async def create_result_delivery(
request: ResultDeliveryCreateRequest,
http_request: Request,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
try:
delivery = await result_delivery_service.create_delivery(
db,
user=current_user,
channel=request.channel,
product_ids=request.product_ids,
compat_result_ids=request.compat_result_ids,
package_mode=request.package_mode,
include_checksums=request.include_checksums,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
await _add_operation_audit_log(
db,
request=http_request,
action="result_delivery_created",
resource="result-deliveries",
detail={
"delivery_id": delivery.delivery_id,
"channel": delivery.channel,
"package_mode": delivery.package_mode,
"item_count": delivery.item_count,
"task_id": delivery.task_id,
"job_id": delivery.job_id,
},
user=current_user,
)
await db.commit()
return result_delivery_service.serialize_delivery(delivery)
@router.get("/result-deliveries")
async def list_result_deliveries(
mine: bool = True,
include_all: bool = False,
limit: int = 50,
offset: int = 0,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
allow_all = bool(include_all) and str(current_user.role or "").lower() == "admin"
if mine:
allow_all = False
return await result_delivery_service.list_deliveries(
db,
user=current_user,
include_all=allow_all,
include_items=True,
item_limit=5,
limit=limit,
offset=offset,
)
@router.get("/result-deliveries/{delivery_id}")
async def get_result_delivery(
delivery_id: str,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
delivery = await result_delivery_service.get_delivery(
db,
delivery_id=delivery_id,
user=current_user,
include_items=True,
)
if delivery is None:
raise HTTPException(status_code=404, detail="delivery not found")
return result_delivery_service.serialize_delivery(delivery, include_items=True)
@router.get("/result-deliveries/{delivery_id}/manifest")
async def download_result_delivery_manifest(
delivery_id: str,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
try:
path = await result_delivery_service.resolve_manifest_path(
db,
delivery_id=delivery_id,
user=current_user,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return FileResponse(path, filename=os.path.basename(path), media_type="application/json")
@router.get("/result-deliveries/{delivery_id}/archive/download")
async def download_result_delivery_archive(
delivery_id: str,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
try:
path = await result_delivery_service.resolve_archive_path(
db,
delivery_id=delivery_id,
user=current_user,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return FileResponse(path, filename=os.path.basename(path), media_type="application/zip")
@router.get("/result-deliveries/{delivery_id}/files/{item_id}/download")
async def download_result_delivery_item(
delivery_id: str,
item_id: int,
db: AsyncSession = Depends(get_db),
current_user: AuthUserORM = Depends(_get_current_user),
):
try:
path = await result_delivery_service.resolve_item_path(
db,
delivery_id=delivery_id,
item_id=item_id,
user=current_user,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
media_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
return FileResponse(path, filename=os.path.basename(path), media_type=media_type)