fix(platform): govern geospatial analysis and raster handoffs
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -16,6 +16,7 @@ from app.models import Area, Dataset, DatasetVersion
|
||||
from app.services.derived_dataset_governance_service import DerivedDatasetGovernanceService
|
||||
from app.services.raster_service import extract_raster_metadata
|
||||
from app.services.storage_service import StorageService
|
||||
from app.services.tile_manifest_service import TileManifestService, canonical_manifest_json
|
||||
|
||||
|
||||
def _import_rasterio():
|
||||
@@ -70,6 +71,39 @@ class RasterOperationsService:
|
||||
raise AppError(code="DATASET_FILE_MISSING", message="Stored raster file missing", status_code=404)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def _validate_storage_checksum(dataset: Dataset) -> None:
|
||||
"""Refuse tiling pixels that no longer match the governed Dataset row."""
|
||||
|
||||
expected_checksum = str(dataset.checksum_sha256 or "").strip().lower()
|
||||
governed_artifact = bool(getattr(dataset, "data_contract_key", None))
|
||||
valid_checksum = len(expected_checksum) == 64 and all(character in "0123456789abcdef" for character in expected_checksum)
|
||||
if not valid_checksum:
|
||||
if expected_checksum or governed_artifact:
|
||||
raise AppError(
|
||||
code="DATASET_STORAGE_CHECKSUM_UNVERIFIABLE",
|
||||
message="Governed raster storage requires a valid SHA-256 checksum before tiling.",
|
||||
details={"checksum_sha256": dataset.checksum_sha256},
|
||||
status_code=409,
|
||||
)
|
||||
return
|
||||
|
||||
digest = sha256()
|
||||
with Path(str(dataset.storage_path)).open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
actual_checksum = digest.hexdigest()
|
||||
if actual_checksum != expected_checksum:
|
||||
raise AppError(
|
||||
code="DATASET_STORAGE_CHECKSUM_MISMATCH",
|
||||
message="Raster dataset storage no longer matches its validated checksum.",
|
||||
details={
|
||||
"expected_checksum_sha256": expected_checksum,
|
||||
"actual_checksum_sha256": actual_checksum,
|
||||
},
|
||||
status_code=409,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _raster_dependencies() -> tuple[Any, Any]:
|
||||
try:
|
||||
@@ -970,14 +1004,17 @@ class RasterOperationsService:
|
||||
tile_size: int = 512,
|
||||
overlap: int = 64,
|
||||
output_name: str | None = None,
|
||||
max_tiles: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
RasterOperationsService._validate_tile_request(tile_size=tile_size, overlap=overlap)
|
||||
if max_tiles is not None and max_tiles <= 0:
|
||||
raise AppError(code="INVALID_PARAMETERS", message="max_tiles must be positive", status_code=400)
|
||||
dataset = RasterOperationsService._load_dataset(db, dataset_id)
|
||||
RasterOperationsService._validate_storage_checksum(dataset)
|
||||
rasterio, _ = RasterOperationsService._raster_dependencies()
|
||||
|
||||
tile_set_id = str(uuid.uuid4())
|
||||
tile_root = StorageService.raster_tiles_root(str(dataset.project_id), str(dataset.id), tile_set_id)
|
||||
tile_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manifest_tiles: list[dict[str, Any]] = []
|
||||
tile_paths: list[str] = []
|
||||
@@ -997,9 +1034,23 @@ class RasterOperationsService:
|
||||
source_width = int(source.width)
|
||||
source_height = int(source.height)
|
||||
step = max(1, tile_size - overlap)
|
||||
x_offsets = RasterOperationsService._tile_offsets(source_width, tile_size, step)
|
||||
y_offsets = RasterOperationsService._tile_offsets(source_height, tile_size, step)
|
||||
expected_tile_count = len(x_offsets) * len(y_offsets)
|
||||
if max_tiles is not None and expected_tile_count > max_tiles:
|
||||
raise AppError(
|
||||
code="RASTER_TILE_LIMIT_EXCEEDED",
|
||||
message="Raster tile generation exceeds the guest analysis limit.",
|
||||
details={
|
||||
"expected_tile_count": expected_tile_count,
|
||||
"max_tiles": max_tiles,
|
||||
},
|
||||
status_code=422,
|
||||
)
|
||||
tile_root.mkdir(parents=True, exist_ok=True)
|
||||
tile_index = 0
|
||||
for yoff in range(0, source_height, step):
|
||||
for xoff in range(0, source_width, step):
|
||||
for yoff in y_offsets:
|
||||
for xoff in x_offsets:
|
||||
tile_width = min(tile_size, source_width - xoff)
|
||||
tile_height = min(tile_size, source_height - yoff)
|
||||
if tile_width <= 0 or tile_height <= 0:
|
||||
@@ -1022,6 +1073,7 @@ class RasterOperationsService:
|
||||
tile_dest.write(tile_data)
|
||||
|
||||
tile_paths.append(str(tile_path))
|
||||
tile_integrity = TileManifestService.tile_integrity(tile_path)
|
||||
manifest_tiles.append(
|
||||
{
|
||||
"path": str(tile_path),
|
||||
@@ -1030,6 +1082,7 @@ class RasterOperationsService:
|
||||
"transform": [float(item) for item in transform.to_gdal()],
|
||||
"crs": source_crs,
|
||||
"index": tile_index,
|
||||
**tile_integrity,
|
||||
},
|
||||
)
|
||||
tile_index += 1
|
||||
@@ -1044,9 +1097,8 @@ class RasterOperationsService:
|
||||
bounds = source_metadata.get("bounds", [0.0, 0.0, 0.0, 0.0])
|
||||
manifest_crs = source_crs or source_metadata.get("crs") or dataset.crs
|
||||
manifest_payload = {
|
||||
**TileManifestService.dataset_binding(db, dataset),
|
||||
"tile_set_id": tile_set_id,
|
||||
"source_dataset_id": str(dataset.id),
|
||||
"source_raster_id": str(dataset.id),
|
||||
"crs": manifest_crs,
|
||||
"source_crs": manifest_crs,
|
||||
"dataset_crs": dataset.crs,
|
||||
@@ -1066,7 +1118,7 @@ class RasterOperationsService:
|
||||
"tile_server": None,
|
||||
}
|
||||
manifest_path = tile_root / "manifest.json"
|
||||
manifest_path.write_text(json.dumps(manifest_payload), encoding="utf-8")
|
||||
manifest_path.write_text(canonical_manifest_json(manifest_payload), encoding="utf-8")
|
||||
|
||||
return {
|
||||
"dataset_id": str(dataset.id),
|
||||
@@ -1079,3 +1131,17 @@ class RasterOperationsService:
|
||||
"count": len(manifest_tiles),
|
||||
"manifest": manifest_payload,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _tile_offsets(dimension: int, tile_size: int, step: int) -> list[int]:
|
||||
"""Return full-tile starts plus one unique edge-aligned final start."""
|
||||
|
||||
if dimension <= 0 or tile_size <= 0 or step <= 0:
|
||||
raise AppError(code="INVALID_PARAMETERS", message="Raster tile dimensions must be positive", status_code=400)
|
||||
if dimension <= tile_size:
|
||||
return [0]
|
||||
final_start = dimension - tile_size
|
||||
offsets = list(range(0, final_start + 1, step))
|
||||
if offsets[-1] != final_start:
|
||||
offsets.append(final_start)
|
||||
return offsets
|
||||
|
||||
Reference in New Issue
Block a user