Add result delivery workflow
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user