111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any, Final
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from k1link.compute import (
|
|
LidarReplayError,
|
|
LidarReplayPackV2,
|
|
lidar_pack_catalog_item,
|
|
lidar_pack_detail,
|
|
)
|
|
|
|
LIDAR_CATALOG_SCHEMA: Final = "missioncore.lidar-replay-pack-catalog/v1"
|
|
_PACK_ID = re.compile(r"^lidar-replay-pack-[a-f0-9]{64}$")
|
|
RootProvider = Callable[[], Path | None]
|
|
|
|
|
|
def configured_lidar_replay_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_LIDAR_REPLAY_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def build_lidar_router(
|
|
*,
|
|
root_provider: RootProvider = configured_lidar_replay_root,
|
|
) -> APIRouter:
|
|
router = APIRouter(prefix="/api/v1/lidar", tags=["lidar"])
|
|
|
|
@router.get("/replay-packs")
|
|
def list_lidar_replay_packs(
|
|
limit: int = Query(default=20, ge=1, le=100),
|
|
) -> dict[str, Any]:
|
|
root = root_provider()
|
|
if root is None or not root.is_dir():
|
|
return {
|
|
"schema_version": LIDAR_CATALOG_SCHEMA,
|
|
"configured": root is not None,
|
|
"items": [],
|
|
"valid_total": 0,
|
|
"invalid_total": 0,
|
|
"duplicate_total": 0,
|
|
"access": "read-only",
|
|
}
|
|
items: list[dict[str, object]] = []
|
|
invalid_total = 0
|
|
duplicate_total = 0
|
|
seen_logical_content: set[str] = set()
|
|
candidates = sorted(
|
|
(
|
|
candidate
|
|
for candidate in root.iterdir()
|
|
if candidate.is_dir() and _PACK_ID.fullmatch(candidate.name) is not None
|
|
),
|
|
key=lambda candidate: candidate.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for candidate in candidates:
|
|
try:
|
|
pack = LidarReplayPackV2(candidate)
|
|
try:
|
|
item = lidar_pack_catalog_item(pack)
|
|
logical_content = str(item["logical_content_sha256"])
|
|
if logical_content in seen_logical_content:
|
|
duplicate_total += 1
|
|
continue
|
|
seen_logical_content.add(logical_content)
|
|
item["created_at_utc"] = pack.manifest.get("created_at_utc")
|
|
items.append(item)
|
|
finally:
|
|
pack.close()
|
|
except (LidarReplayError, OSError):
|
|
invalid_total += 1
|
|
return {
|
|
"schema_version": LIDAR_CATALOG_SCHEMA,
|
|
"configured": True,
|
|
"items": items[:limit],
|
|
"valid_total": len(items),
|
|
"invalid_total": invalid_total,
|
|
"duplicate_total": duplicate_total,
|
|
"access": "read-only",
|
|
}
|
|
|
|
@router.get("/replay-packs/{pack_id}")
|
|
def get_lidar_replay_pack(pack_id: str) -> dict[str, object]:
|
|
if _PACK_ID.fullmatch(pack_id) is None:
|
|
raise HTTPException(status_code=404, detail="LiDAR replay pack не найден")
|
|
root = root_provider()
|
|
if root is None or not root.is_dir():
|
|
raise HTTPException(status_code=503, detail="LiDAR replay storage не настроен")
|
|
candidate = root / pack_id
|
|
if not candidate.is_dir():
|
|
raise HTTPException(status_code=404, detail="LiDAR replay pack не найден")
|
|
try:
|
|
pack = LidarReplayPackV2(candidate)
|
|
try:
|
|
return lidar_pack_detail(pack)
|
|
finally:
|
|
pack.close()
|
|
except (LidarReplayError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR replay pack не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
return router
|