Basemap Pusher: catalog and distribution gateway for existing tiles

A Go service that scans existing basemap tile directories and vector files,
catalogues them in SQLite, issues scoped access tokens, and serves tiles and
downloads to downstream systems. It deliberately does not generate or process
raster data: GIS software produces the tiles, this service catalogues,
authorises and distributes them.

Capabilities
- scans basemap tile directories (manifest-based or raw z/x/y) and vector
  assets (.geojson, .json, .zip, shapefiles with sidecars)
- token auth with per-resource grants, expiry and coarse scopes (read, slice)
- resource health inspection flags a moved or deleted tile root before a client
  hits a 404
- downstream push with exponential-backoff retries and manual re-queue
- image slicing jobs driven by the GDAL bundled with QGIS, including VRT
  mosaicking so successive drone flights accumulate into one seamless layer
- a narrow, non-admin slicing API for external callers: submit, poll,
  idempotent by job code, completion callback
- ops console covering health, ledger, tokens, pushes, scans, slicing and
  service info
- native Windows service install/start/stop with crash recovery, auto-start and
  a rotating log file, so no NSSM or WinSW wrapper is needed

Verified with 49 tests plus live runs against real tile data and real GDAL.
This commit is contained in:
2026-10-16 10:36:12 +08:00
commit 747eed63b0
55 changed files with 13526 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# Demo Scripts
This folder contains runnable demos for the internal remote-sensing result service.
## Prerequisites
- PostgreSQL and PostGIS are available to the API through `config/service.env`.
- The Python environment is available at `PYTHON_EXE`.
- Go is installed and can run `go run`.
## Service Startup
You can start the stack manually from the repository root:
```powershell
.\start-all.bat
```
Or use the self-contained full-flow demo to start a temporary stack on demo ports:
```powershell
.\demo\demo_full_flow.ps1 -StartStack -StartCallbackReceiver
```
## Demo List
- `demo_import_and_slice.ps1`
Imports a raster result, binds a style, and triggers COG, preview, legend, and tile generation.
- `demo_register_target_system.ps1`
Creates or updates a callback-based target system.
- `demo_push_result.ps1`
Pushes an existing managed result to a target system or callback URL.
- `demo_geojson_flow.ps1`
Registers a GeoJSON resource and optionally pushes it through the same delivery interface.
- `demo_full_flow.ps1`
Runs the main end-to-end flow: sample raster, import, slice, target registration, and callback push.
- `demo_callback_receiver.py`
Small local receiver for inspecting push payloads.
## Suggested Order
1. Start the services manually, or run `demo_full_flow.ps1 -StartStack -StartCallbackReceiver`.
2. Run `demo_import_and_slice.ps1` to create a managed result.
3. Run `demo_register_target_system.ps1` to register a callback receiver.
4. Run `demo_push_result.ps1` to publish the managed result.
5. Run `demo_geojson_flow.ps1 -Push` if you want to verify the retained GeoJSON delivery path.
## Example Commands
```powershell
.\demo\demo_import_and_slice.ps1
.\demo\demo_register_target_system.ps1 -CallbackUrl http://127.0.0.1:8920/callback
.\demo\demo_push_result.ps1 -ResultCode demo_result_20260406_120000 -TargetSystemCode demo_callback
.\demo\demo_geojson_flow.ps1 -Push -CallbackUrl http://127.0.0.1:8920/callback
.\demo\demo_full_flow.ps1 -StartStack -StartCallbackReceiver
```
## Output
Each demo prints a JSON summary that includes the main identifiers and URLs. Temporary run data is written under `temp/demo-runs/`.
`source_system_code` is intentionally left empty in the demos. If you want to populate it, seed the `source_systems` table first.
+574
View File
@@ -0,0 +1,574 @@
param(
[string]$EnvFile = (Join-Path $PSScriptRoot "..\config\service.env")
)
$script:RsDemoRepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:RsDemoProcesses = @()
$script:RsDemoEnvLoaded = $false
function Get-RsRepoRoot {
return $script:RsDemoRepoRoot
}
function Import-RsDemoEnvironment {
param([string]$Path = $EnvFile)
if ($script:RsDemoEnvLoaded) {
return
}
if (Test-Path -LiteralPath $Path) {
foreach ($rawLine in (Get-Content -LiteralPath $Path)) {
$line = $rawLine.Trim()
if (-not $line -or $line.StartsWith("#")) {
continue
}
if ($line.StartsWith("export ")) {
$line = $line.Substring(7).Trim()
}
$parts = $line.Split("=", 2)
if ($parts.Count -ne 2) {
continue
}
$key = $parts[0].Trim()
$value = $parts[1].Trim().Trim("`"").Trim("'")
if (-not $key) {
continue
}
if (-not (Test-Path "Env:$key")) {
Set-Item -Path "Env:$key" -Value $value
}
}
}
if (-not $env:PYTHON_EXE) {
$env:PYTHON_EXE = "C:\Users\Administrator\.conda\envs\InSAR\python.exe"
}
if (-not $env:API_BASE_URL) {
$env:API_BASE_URL = "http://127.0.0.1:8910"
}
if (-not $env:TILE_STATIC_BASE_URL) {
$env:TILE_STATIC_BASE_URL = "http://127.0.0.1:8900"
}
if (-not $env:RS_API_BASE_URL) {
$env:RS_API_BASE_URL = $env:API_BASE_URL
}
if (-not $env:PYTHONIOENCODING) {
$env:PYTHONIOENCODING = "utf-8"
}
if (-not $env:NO_PAUSE) {
$env:NO_PAUSE = "1"
}
$script:RsDemoEnvLoaded = $true
}
function Get-RsApiBaseUrl {
Import-RsDemoEnvironment
return ([string]$env:API_BASE_URL).TrimEnd("/")
}
function Get-RsTileBaseUrl {
Import-RsDemoEnvironment
return ([string]$env:TILE_STATIC_BASE_URL).TrimEnd("/")
}
function Get-RsPythonExe {
Import-RsDemoEnvironment
if (-not (Test-Path -LiteralPath $env:PYTHON_EXE)) {
throw "Python interpreter not found: $($env:PYTHON_EXE)"
}
return $env:PYTHON_EXE
}
function Get-RsGoCommand {
return (Get-Command go -ErrorAction Stop).Source
}
function New-RsRunContext {
param([string]$Prefix = "demo")
$runId = "{0}_{1}" -f $Prefix, (Get-Date -Format "yyyyMMdd_HHmmss")
$runDir = Join-Path (Get-RsRepoRoot) ("temp\demo-runs\" + $runId)
$logDir = Join-Path $runDir "logs"
$outputDir = Join-Path $runDir "output"
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
return [pscustomobject]@{
RunID = $runId
RunDirectory = $runDir
LogDirectory = $logDir
OutputDirectory = $outputDir
}
}
function ConvertTo-RsFileUri {
param([Parameter(Mandatory)] [string]$Path)
$fullPath = [System.IO.Path]::GetFullPath($Path)
return ([System.Uri]$fullPath).AbsoluteUri
}
function Encode-RsURIComponent {
param([Parameter(Mandatory)] [string]$Value)
return [System.Uri]::EscapeDataString($Value)
}
function Wait-RsUrl {
param(
[Parameter(Mandatory)] [string]$Url,
[int]$TimeoutSec = 60
)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
do {
try {
Invoke-RestMethod -Uri $Url -Method Get -TimeoutSec 5 | Out-Null
return
} catch {
Start-Sleep -Milliseconds 500
}
} while ((Get-Date) -lt $deadline)
throw "Timed out waiting for $Url"
}
function Invoke-RsApi {
param(
[Parameter(Mandatory)] [string]$Method,
[Parameter(Mandatory)] [string]$Path,
[object]$Body,
[int]$TimeoutSec = 30
)
Import-RsDemoEnvironment
$uri = if ($Path -match "^https?://") {
$Path
} else {
"{0}{1}" -f (Get-RsApiBaseUrl), $Path
}
$params = @{
Uri = $uri
Method = $Method
TimeoutSec = $TimeoutSec
Headers = @{
Accept = "application/json"
}
}
if ($PSBoundParameters.ContainsKey("Body") -and $null -ne $Body) {
$params.ContentType = "application/json"
$params.Body = ($Body | ConvertTo-Json -Depth 32)
}
try {
$response = Invoke-RestMethod @params
} catch {
$message = $_.Exception.Message
$webResponse = $_.Exception.Response
if ($webResponse) {
try {
$stream = $webResponse.GetResponseStream()
if ($stream) {
$reader = New-Object System.IO.StreamReader($stream)
$rawBody = $reader.ReadToEnd()
$reader.Dispose()
if ($rawBody) {
try {
$errorEnvelope = $rawBody | ConvertFrom-Json
if ($errorEnvelope.error) {
$message = "{0} {1}" -f $errorEnvelope.error.code, $errorEnvelope.error.message
} else {
$message = $rawBody
}
} catch {
$message = $rawBody
}
}
}
} catch {
}
}
throw "API call failed: $Method $uri`n$message"
}
if ($null -eq $response) {
return $null
}
if ($response.error) {
throw "API error: $Method $uri => $($response.error.code) $($response.error.message)"
}
return $response.data
}
function Wait-RsJob {
param(
[Parameter(Mandatory)] [string]$JobID,
[int]$TimeoutSec = 300,
[int]$PollIntervalSec = 2
)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
do {
$job = Invoke-RsApi -Method "GET" -Path ("/api/v1/jobs/{0}" -f $JobID)
if ($job.status -eq "succeeded") {
return $job
}
if ($job.status -in @("failed", "cancelled")) {
throw "Job $JobID finished with status=$($job.status) error=$($job.last_error_message)"
}
Start-Sleep -Seconds $PollIntervalSec
} while ((Get-Date) -lt $deadline)
throw "Timed out waiting for job $JobID"
}
function Get-RsStyleByCode {
param(
[Parameter(Mandatory)] [string]$ProjectCode,
[Parameter(Mandatory)] [string]$Code,
[switch]$AllowMissing
)
$project = Encode-RsURIComponent $ProjectCode
$keyword = Encode-RsURIComponent $Code
$items = @(Invoke-RsApi -Method "GET" -Path "/api/v1/styles?project_code=$project&keyword=$keyword")
$match = $items | Where-Object { $_.code -eq $Code } | Select-Object -First 1
if ($match) {
return $match
}
if ($AllowMissing) {
return $null
}
throw "Style not found: $Code"
}
function Get-RsTargetSystemByCode {
param(
[Parameter(Mandatory)] [string]$ProjectCode,
[Parameter(Mandatory)] [string]$Code,
[switch]$AllowMissing
)
$project = Encode-RsURIComponent $ProjectCode
$items = @(Invoke-RsApi -Method "GET" -Path "/api/v1/target-systems?project_code=$project")
$match = $items | Where-Object { $_.code -eq $Code } | Select-Object -First 1
if ($match) {
return $match
}
if ($AllowMissing) {
return $null
}
throw "Target system not found: $Code"
}
function Get-RsResultByCode {
param(
[Parameter(Mandatory)] [string]$ProjectCode,
[Parameter(Mandatory)] [string]$Code,
[switch]$AllowMissing
)
$project = Encode-RsURIComponent $ProjectCode
$keyword = Encode-RsURIComponent $Code
$items = @(Invoke-RsApi -Method "GET" -Path "/api/v1/projects/$project/results?keyword=$keyword")
$match = $items | Where-Object { $_.code -eq $Code } | Select-Object -First 1
if ($match) {
return Invoke-RsApi -Method "GET" -Path ("/api/v1/results/{0}" -f $match.id)
}
if ($AllowMissing) {
return $null
}
throw "Result not found: $Code"
}
function New-RsDemoStyleSpec {
param(
[string]$StyleID = "demo_continuous_style",
[string]$Name = "Demo Continuous Style",
[string]$Scope = "project",
[double]$MinValue = -1.0,
[double]$MaxValue = 1.0,
[int]$TileSize = 256,
[string]$TileFormat = "webp",
[int]$MinZoom = 0,
[int]$MaxZoom = 4
)
return @{
spec_version = "1.0"
id = $StyleID
name = $Name
version = 1
scope = $Scope
render = @{
mode = "continuous"
band = 1
resampling = "bilinear"
value_domain = @{
min = $MinValue
max = $MaxValue
}
color_map = @{
type = "linear"
stops = @(
@{ value = $MinValue; color = "#8c510a"; label = "low" },
@{ value = (($MinValue + $MaxValue) / 2.0); color = "#f6e8c3"; label = "mid" },
@{ value = $MaxValue; color = "#01665e"; label = "high" }
)
null_color = "#00000000"
}
}
legend = @{
title = $Name
orientation = "horizontal"
precision = 2
}
output = @{
tile_format = $TileFormat
tile_size = $TileSize
min_zoom = $MinZoom
max_zoom = $MaxZoom
}
}
}
function Ensure-RsStyleVersion {
param(
[Parameter(Mandatory)] [string]$ProjectCode,
[Parameter(Mandatory)] [string]$StyleCode,
[string]$StyleName = "Demo Continuous Style",
[hashtable]$StyleSpec,
[switch]$CreateNewVersion
)
$style = Get-RsStyleByCode -ProjectCode $ProjectCode -Code $StyleCode -AllowMissing
if (-not $style) {
$style = Invoke-RsApi -Method "POST" -Path "/api/v1/styles" -Body @{
project_code = $ProjectCode
scope = "project"
code = $StyleCode
name = $StyleName
created_by = "demo"
}
}
if (-not $CreateNewVersion -and $style.latest_version_id) {
return Invoke-RsApi -Method "GET" -Path ("/api/v1/style-versions/{0}" -f $style.latest_version_id)
}
if (-not $StyleSpec) {
$StyleSpec = New-RsDemoStyleSpec -StyleID $StyleCode -Name $StyleName
}
return Invoke-RsApi -Method "POST" -Path ("/api/v1/styles/{0}/versions" -f $style.id) -Body @{
style_spec = $StyleSpec
}
}
function Ensure-RsTargetSystem {
param(
[Parameter(Mandatory)] [string]$ProjectCode,
[Parameter(Mandatory)] [string]$Code,
[Parameter(Mandatory)] [string]$Name,
[Parameter(Mandatory)] [string]$CallbackUrl,
[string]$CallbackMethod = "POST",
[hashtable]$CallbackHeaders,
[int]$TimeoutSeconds = 30,
[bool]$Enabled = $true,
[string]$CreatedBy = "demo"
)
$body = @{
code = $Code
name = $Name
callback_url = $CallbackUrl
callback_method = $CallbackMethod
timeout_seconds = $TimeoutSeconds
enabled = $Enabled
callback_headers = $CallbackHeaders
}
$existing = Get-RsTargetSystemByCode -ProjectCode $ProjectCode -Code $Code -AllowMissing
if ($existing) {
return Invoke-RsApi -Method "PATCH" -Path ("/api/v1/target-systems/{0}" -f $existing.id) -Body $body
}
$body.project_code = $ProjectCode
$body.created_by = $CreatedBy
return Invoke-RsApi -Method "POST" -Path "/api/v1/target-systems" -Body $body
}
function Start-RsBackgroundProcess {
param(
[Parameter(Mandatory)] [string]$FilePath,
[string[]]$ArgumentList,
[Parameter(Mandatory)] [string]$WorkingDirectory,
[Parameter(Mandatory)] [string]$Name,
[Parameter(Mandatory)] [string]$LogDirectory
)
New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
$stdout = Join-Path $LogDirectory ($Name + ".stdout.log")
$stderr = Join-Path $LogDirectory ($Name + ".stderr.log")
$process = Start-Process -FilePath $FilePath `
-ArgumentList $ArgumentList `
-WorkingDirectory $WorkingDirectory `
-PassThru `
-RedirectStandardOutput $stdout `
-RedirectStandardError $stderr
$script:RsDemoProcesses += $process
return $process
}
function Stop-RsBackgroundProcesses {
param([object[]]$Processes = $script:RsDemoProcesses)
foreach ($process in $Processes) {
if ($process -and -not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
}
}
function Start-RsDemoStack {
param(
[int]$ApiPort = 28910,
[int]$TilePort = 28900,
[switch]$IncludeWorker,
[string]$RunID = ("stack_" + (Get-Date -Format "yyyyMMdd_HHmmss"))
)
Import-RsDemoEnvironment
$runDir = Join-Path (Get-RsRepoRoot) ("temp\demo-runs\" + $RunID)
$logDir = Join-Path $runDir "logs"
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$apiBaseUrl = "http://127.0.0.1:$ApiPort"
$tileBaseUrl = "http://127.0.0.1:$TilePort"
$env:API_HOST = "127.0.0.1"
$env:API_PORT = "$ApiPort"
$env:API_BASE_URL = $apiBaseUrl
$env:TILE_HOST = "127.0.0.1"
$env:TILE_PORT = "$TilePort"
$env:TILE_STATIC_BASE_URL = $tileBaseUrl
$env:RS_API_BASE_URL = $apiBaseUrl
$env:NO_PAUSE = "1"
$pythonExe = Get-RsPythonExe
$goExe = Get-RsGoCommand
$tileProcess = Start-RsBackgroundProcess `
-FilePath $pythonExe `
-ArgumentList @((Join-Path (Get-RsRepoRoot) "server.py")) `
-WorkingDirectory (Get-RsRepoRoot) `
-Name "tile-server" `
-LogDirectory $logDir
$apiProcess = Start-RsBackgroundProcess `
-FilePath $goExe `
-ArgumentList @("run", ".\cmd\api") `
-WorkingDirectory (Join-Path (Get-RsRepoRoot) "services\api-go") `
-Name "api-service" `
-LogDirectory $logDir
Wait-RsUrl -Url "$tileBaseUrl/health" -TimeoutSec 90
Wait-RsUrl -Url "$apiBaseUrl/ready" -TimeoutSec 120
$workerProcess = $null
if ($IncludeWorker) {
$workerProcess = Start-RsBackgroundProcess `
-FilePath $pythonExe `
-ArgumentList @((Join-Path (Get-RsRepoRoot) "services\worker-py\main.py")) `
-WorkingDirectory (Get-RsRepoRoot) `
-Name "python-worker" `
-LogDirectory $logDir
Start-Sleep -Seconds 1
}
$processes = @($tileProcess, $apiProcess)
if ($workerProcess) {
$processes += $workerProcess
}
return [pscustomobject]@{
RunID = $RunID
RunDirectory = $runDir
LogDirectory = $logDir
ApiBaseUrl = $apiBaseUrl
TileBaseUrl = $tileBaseUrl
Processes = $processes
}
}
function Start-RsCallbackReceiver {
param(
[int]$Port = 28920,
[string]$Path = "/callback",
[string]$RunID = ("callback_" + (Get-Date -Format "yyyyMMdd_HHmmss"))
)
$runDir = Join-Path (Get-RsRepoRoot) ("temp\demo-runs\" + $RunID)
$logDir = Join-Path $runDir "logs"
$outputDir = Join-Path $runDir "callback-records"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$pythonExe = Get-RsPythonExe
$process = Start-RsBackgroundProcess `
-FilePath $pythonExe `
-ArgumentList @(
(Join-Path $PSScriptRoot "demo_callback_receiver.py"),
"--host", "127.0.0.1",
"--port", "$Port",
"--path", $Path,
"--output-dir", $outputDir
) `
-WorkingDirectory (Get-RsRepoRoot) `
-Name "callback-receiver" `
-LogDirectory $logDir
$callbackUrl = "http://127.0.0.1:$Port$Path"
Wait-RsUrl -Url "http://127.0.0.1:$Port/health" -TimeoutSec 30
return [pscustomobject]@{
RunID = $RunID
RunDirectory = $runDir
LogDirectory = $logDir
OutputDirectory = $outputDir
CallbackURL = $callbackUrl
Process = $process
}
}
function Get-RsLatestCallbackRecord {
param([Parameter(Mandatory)] [string]$OutputDirectory)
if (-not (Test-Path -LiteralPath $OutputDirectory)) {
return $null
}
$file = Get-ChildItem -LiteralPath $OutputDirectory -Filter "*.json" |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if (-not $file) {
return $null
}
return [pscustomobject]@{
Path = $file.FullName
Payload = (Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json)
}
}
+86
View File
@@ -0,0 +1,86 @@
import argparse
import os
import sys
from pathlib import Path
def bootstrap_gdal_env() -> None:
env_root = Path(sys.executable).resolve().parent
gdal_paths = [
env_root / "Lib" / "site-packages" / "rasterio" / "gdal_data",
env_root / "Library" / "share" / "gdal",
env_root / "share" / "gdal",
]
proj_paths = [
env_root / "Lib" / "site-packages" / "rasterio" / "proj_data",
env_root / "Library" / "share" / "proj",
env_root / "share" / "proj",
]
for path in gdal_paths:
if path.exists():
os.environ["GDAL_DATA"] = str(path)
break
for path in proj_paths:
if path.exists():
os.environ["PROJ_LIB"] = str(path)
os.environ["PROJ_DATA"] = str(path)
break
bootstrap_gdal_env()
import numpy as np
import rasterio
from rasterio.transform import from_origin
def build_values(width: int, height: int, preset: str) -> np.ndarray:
yy, xx = np.mgrid[0:height, 0:width]
x = (xx / max(width - 1, 1)).astype("float32")
y = (yy / max(height - 1, 1)).astype("float32")
if preset == "phase":
values = np.sin((x * 8.0) + (y * 4.0)) * np.cos(y * 10.0)
else:
values = (x * 2.0 - 1.0) * 0.7 + np.sin(y * np.pi * 3.0) * 0.3
return values.astype("float32")
def main() -> int:
parser = argparse.ArgumentParser(description="Create a sample GeoTIFF for demos.")
parser.add_argument("--output", required=True, help="Output GeoTIFF path.")
parser.add_argument("--width", type=int, default=512)
parser.add_argument("--height", type=int, default=512)
parser.add_argument("--preset", choices=["ndvi", "phase"], default="ndvi")
parser.add_argument("--origin-x", type=float, default=110.0)
parser.add_argument("--origin-y", type=float, default=22.0)
parser.add_argument("--pixel-size", type=float, default=0.01)
args = parser.parse_args()
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
data = build_values(args.width, args.height, args.preset)
with rasterio.open(
output,
"w",
driver="GTiff",
width=args.width,
height=args.height,
count=1,
dtype="float32",
crs="EPSG:4326",
transform=from_origin(args.origin_x, args.origin_y, args.pixel_size, args.pixel_size),
nodata=-9999.0,
) as dst:
dst.write(data, 1)
print(output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+109
View File
@@ -0,0 +1,109 @@
import argparse
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
def parse_args():
parser = argparse.ArgumentParser(description="Simple callback receiver for tile-server demos.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8920)
parser.add_argument("--path", default="/callback")
parser.add_argument("--output-dir", required=True)
return parser.parse_args()
class State:
def __init__(self, output_dir: Path, callback_path: str):
self.output_dir = output_dir
self.callback_path = callback_path
self.counter = 0
self.lock = threading.Lock()
def next_file(self) -> Path:
with self.lock:
self.counter += 1
name = f"{time.strftime('%Y%m%d_%H%M%S')}_{self.counter:04d}.json"
return self.output_dir / name
def make_handler(state: State):
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps({"status": "ok"}).encode("utf-8"))
return
self.send_response(404)
self.end_headers()
def do_POST(self):
self._handle_payload()
def do_PUT(self):
self._handle_payload()
def _handle_payload(self):
if self.path != state.callback_path:
self.send_response(404)
self.end_headers()
return
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length)
text = raw.decode("utf-8", errors="replace")
try:
body_json = json.loads(text)
except json.JSONDecodeError:
body_json = None
record = {
"received_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"method": self.command,
"path": self.path,
"headers": {key: value for key, value in self.headers.items()},
"body_text": text,
"body_json": body_json,
}
output_path = state.next_file()
output_path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[callback] saved {output_path}", flush=True)
response = {"saved": str(output_path), "ok": True}
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(response).encode("utf-8"))
def log_message(self, fmt, *args):
print("[callback] " + (fmt % args), flush=True)
return Handler
def main():
args = parse_args()
output_dir = Path(args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
state = State(output_dir=output_dir, callback_path=args.path)
server = ThreadingHTTPServer((args.host, args.port), make_handler(state))
print(f"Callback receiver listening on http://{args.host}:{args.port}{args.path}", flush=True)
print(f"Health endpoint: http://{args.host}:{args.port}/health", flush=True)
print(f"Output dir: {output_dir}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
if __name__ == "__main__":
main()
+213
View File
@@ -0,0 +1,213 @@
param(
[string]$ProjectCode = "default",
[string]$ResultCode = ("demo_full_" + (Get-Date -Format "yyyyMMdd_HHmmss")),
[string]$ResultType = "ndvi",
[string]$SourceSystemCode = "",
[string]$RequestedBy = "demo-user",
[string]$StyleCode = "demo_continuous_style",
[string]$TargetSystemCode = "demo_callback",
[switch]$StartStack,
[switch]$StartCallbackReceiver,
[switch]$KeepProcesses,
[int]$ApiPort = 28910,
[int]$TilePort = 28900,
[int]$CallbackPort = 28920,
[int]$ZoomMin = 0,
[int]$ZoomMax = 3
)
. (Join-Path $PSScriptRoot "common.ps1")
$stack = $null
$callback = $null
$run = New-RsRunContext -Prefix "full_flow"
try {
Import-RsDemoEnvironment
if ($StartStack) {
$stack = Start-RsDemoStack -ApiPort $ApiPort -TilePort $TilePort -IncludeWorker -RunID $run.RunID
}
$callbackUrl = ""
if ($StartCallbackReceiver) {
$callback = Start-RsCallbackReceiver -Port $CallbackPort -RunID ($run.RunID + "_callback")
$callbackUrl = $callback.CallbackURL
}
$inputPath = Join-Path $run.OutputDirectory ($ResultCode + ".tif")
& (Get-RsPythonExe) (Join-Path $PSScriptRoot "create_sample_raster.py") `
--output $inputPath `
--preset "ndvi" | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Failed to create sample raster"
}
$inspection = Invoke-RsApi -Method "POST" -Path "/api/v1/admin/raster-inspections" -Body @{
path = [System.IO.Path]::GetFullPath($inputPath)
}
$styleVersion = Ensure-RsStyleVersion -ProjectCode $ProjectCode -StyleCode $StyleCode -StyleName "Demo Continuous Style"
$project = Encode-RsURIComponent $ProjectCode
$importBody = @{
requested_by = $RequestedBy
asset = @{
source_ref = $inspection.storage_uri
asset_type = "raster"
display_name = $ResultCode
description = "Imported by demo_full_flow.ps1"
metadata = @{
demo_run = $run.RunID
}
}
asset_version = @{
format = $inspection.format
storage_uri = $inspection.storage_uri
mime_type = $inspection.mime_type
size_bytes = $inspection.size_bytes
crs = $inspection.crs
width = $inspection.width
height = $inspection.height
band_count = $inspection.band_count
dtype = $inspection.dtype
nodata_value = $inspection.nodata_value
bbox = $inspection.bbox
resolution_x = $inspection.resolution_x
resolution_y = $inspection.resolution_y
metadata = $inspection.metadata
}
product = @{
code = $ResultCode
type = $ResultType
name = $ResultCode
description = "Demo end-to-end result"
}
product_version = @{
value_type = "continuous"
unit = "index"
bbox = $inspection.bbox
status = "ready"
}
style_binding = @{
style_version_id = $styleVersion.id
is_default = $true
availability = "enabled"
}
}
if ($SourceSystemCode) {
$importBody.source_system_code = $SourceSystemCode
}
$import = Invoke-RsApi -Method "POST" -Path "/api/v1/projects/$project/imports/results" -Body $importBody
$slice = Invoke-RsApi -Method "POST" -Path ("/api/v1/results/{0}/slice" -f $import.product.product.id) -Body @{
requested_by = $RequestedBy
build_cog = $true
generate_preview = $true
generate_legend = $true
generate_tiles = $true
build_cog_input = @{
compression = "deflate"
block_size = 256
force_rebuild = $true
}
preview_input = @{
style_version_id = $styleVersion.id
width = 512
height = 512
format = "png"
force_rebuild = $true
}
legend_input = @{
style_version_id = $styleVersion.id
width = 320
height = 80
format = "png"
force_rebuild = $true
}
tiles_input = @{
style_version_id = $styleVersion.id
zoom_min = $ZoomMin
zoom_max = $ZoomMax
tile_size = 256
tile_format = "webp"
force_rebuild = $true
}
}
$sliceJobs = @()
foreach ($job in @($slice.jobs)) {
$sliceJobs += Wait-RsJob -JobID $job.id
}
if ($callbackUrl) {
Ensure-RsTargetSystem `
-ProjectCode $ProjectCode `
-Code $TargetSystemCode `
-Name "Demo Callback Receiver" `
-CallbackUrl $callbackUrl `
-CallbackMethod "POST" `
-TimeoutSeconds 30 `
-CreatedBy $RequestedBy `
-CallbackHeaders @{
"X-Demo-Mode" = "full-flow"
} | Out-Null
}
$pushBody = @{
requested_by = $RequestedBy
style_version_id = $styleVersion.id
include_artifacts = $true
}
if ($callbackUrl) {
$pushBody.target_system_code = $TargetSystemCode
} else {
$pushBody.callback_url = "http://127.0.0.1:8920/callback"
}
$push = Invoke-RsApi -Method "POST" -Path ("/api/v1/results/{0}/pushes" -f $import.product.product.id) -Body $pushBody
$pushJob = Wait-RsJob -JobID $push.job.id
$delivery = Invoke-RsApi -Method "GET" -Path ("/api/v1/projects/{0}/products/{1}/delivery?version=latest&style_version_id={2}" -f `
(Encode-RsURIComponent $ProjectCode), `
(Encode-RsURIComponent $ResultCode), `
(Encode-RsURIComponent $styleVersion.id))
$callbackRecord = $null
if ($callback) {
Start-Sleep -Seconds 1
$callbackRecord = Get-RsLatestCallbackRecord -OutputDirectory $callback.OutputDirectory
}
[ordered]@{
run_id = $run.RunID
api_base_url = Get-RsApiBaseUrl
tile_base_url = Get-RsTileBaseUrl
callback_url = if ($callback) { $callback.CallbackURL } else { $push.callback_url }
result_id = $import.product.product.id
result_code = $ResultCode
product_version_id = $import.product_version.id
style_version_id = $styleVersion.id
slice_job_ids = @($sliceJobs | ForEach-Object { $_.id })
push_job_id = $push.job.id
push_job_status = $pushJob.status
cog_url = $delivery.downloads.cog_url
preview_url = if ($delivery.preview) { $delivery.preview.url } else { $null }
legend_url = if ($delivery.legend) { $delivery.legend.url } else { $null }
tileset_url = if ($delivery.tileset) { $delivery.tileset.url_template } else { $null }
callback_record_path = if ($callbackRecord) { $callbackRecord.Path } else { $null }
run_directory = $run.RunDirectory
log_directory = if ($stack) { $stack.LogDirectory } else { $null }
} | ConvertTo-Json -Depth 16
}
finally {
if (-not $KeepProcesses) {
if ($callback) {
Stop-RsBackgroundProcesses -Processes @($callback.Process)
}
if ($stack) {
Stop-RsBackgroundProcesses -Processes $stack.Processes
}
}
}
+135
View File
@@ -0,0 +1,135 @@
param(
[string]$ProjectCode = "default",
[string]$ResourceCode = ("demo_geojson_" + (Get-Date -Format "yyyyMMdd_HHmmss")),
[string]$InputPath = "",
[string]$RequestedBy = "demo-user",
[switch]$Push,
[string]$TargetSystemCode = "",
[string]$CallbackUrl = "",
[bool]$WaitForJob = $true,
[switch]$StartStack,
[switch]$StartCallbackReceiver,
[switch]$KeepProcesses,
[int]$ApiPort = 28910,
[int]$TilePort = 28900,
[int]$CallbackPort = 28920
)
. (Join-Path $PSScriptRoot "common.ps1")
$stack = $null
$callback = $null
$run = New-RsRunContext -Prefix "geojson_flow"
try {
Import-RsDemoEnvironment
if ($StartStack) {
$stack = Start-RsDemoStack -ApiPort $ApiPort -TilePort $TilePort -IncludeWorker -RunID $run.RunID
}
if ($StartCallbackReceiver) {
$callback = Start-RsCallbackReceiver -Port $CallbackPort -RunID ($run.RunID + "_callback")
$Push = $true
$CallbackUrl = $callback.CallbackURL
}
if (-not $InputPath) {
$geojsonDir = Join-Path (Get-RsRepoRoot) "data\geojson\demo"
New-Item -ItemType Directory -Force -Path $geojsonDir | Out-Null
$InputPath = Join-Path $geojsonDir ($ResourceCode + ".geojson")
@"
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"code": "$ResourceCode",
"kind": "demo"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[110.0, 20.0],
[112.0, 20.0],
[112.0, 22.0],
[110.0, 22.0],
[110.0, 20.0]
]
]
}
}
]
}
"@ | Set-Content -LiteralPath $InputPath -Encoding UTF8
}
$publicUrl = "/geojson/demo/" + [System.IO.Path]::GetFileName($InputPath)
$project = Encode-RsURIComponent $ProjectCode
$resource = Invoke-RsApi -Method "POST" -Path "/api/v1/projects/$project/geojson-resources" -Body @{
code = $ResourceCode
name = $ResourceCode
description = "Registered by demo_geojson_flow.ps1"
storage_uri = ConvertTo-RsFileUri $InputPath
public_url = $publicUrl
metadata = @{
demo_run = $run.RunID
}
}
$pushResponse = $null
$job = $null
if ($Push -or $TargetSystemCode -or $CallbackUrl) {
$body = @{
resource_kind = "geojson_resource"
geojson_resource_id = $resource.id
requested_by = $RequestedBy
}
if ($TargetSystemCode) {
$body.target_system_code = $TargetSystemCode
}
if ($CallbackUrl) {
$body.callback_url = $CallbackUrl
}
if (-not $TargetSystemCode -and -not $CallbackUrl) {
$body.callback_url = "http://127.0.0.1:8920/callback"
}
$pushResponse = Invoke-RsApi -Method "POST" -Path "/api/v1/projects/$project/delivery-pushes" -Body $body
if ($WaitForJob) {
$job = Wait-RsJob -JobID $pushResponse.job.id
}
}
$callbackRecord = $null
if ($callback) {
Start-Sleep -Seconds 1
$callbackRecord = Get-RsLatestCallbackRecord -OutputDirectory $callback.OutputDirectory
}
[ordered]@{
run_id = $run.RunID
api_base_url = Get-RsApiBaseUrl
tile_base_url = Get-RsTileBaseUrl
resource_id = $resource.id
resource_code = $resource.code
input_path = [System.IO.Path]::GetFullPath($InputPath)
public_url = $resource.public_url
download_url = $resource.download_url
push_job_id = if ($pushResponse) { $pushResponse.job.id } else { $null }
push_job_status = if ($job) { $job.status } elseif ($pushResponse) { $pushResponse.job.status } else { $null }
callback_url = if ($pushResponse) { $pushResponse.callback_url } else { $CallbackUrl }
callback_record_path = if ($callbackRecord) { $callbackRecord.Path } else { $null }
} | ConvertTo-Json -Depth 12
}
finally {
if (-not $KeepProcesses) {
if ($callback) {
Stop-RsBackgroundProcesses -Processes @($callback.Process)
}
if ($stack) {
Stop-RsBackgroundProcesses -Processes $stack.Processes
}
}
}
+190
View File
@@ -0,0 +1,190 @@
param(
[string]$ProjectCode = "default",
[string]$ResultCode = ("demo_result_" + (Get-Date -Format "yyyyMMdd_HHmmss")),
[string]$ResultType = "ndvi",
[string]$InputPath = "",
[string]$SourceSystemCode = "",
[string]$RequestedBy = "demo-user",
[string]$StyleCode = "demo_continuous_style",
[string]$StyleName = "Demo Continuous Style",
[int]$ZoomMin = 0,
[int]$ZoomMax = 3,
[bool]$WaitForJobs = $true,
[switch]$StartStack,
[switch]$KeepStack,
[int]$ApiPort = 28910,
[int]$TilePort = 28900
)
. (Join-Path $PSScriptRoot "common.ps1")
$stack = $null
$run = New-RsRunContext -Prefix "import_slice"
try {
Import-RsDemoEnvironment
if ($StartStack) {
$stack = Start-RsDemoStack -ApiPort $ApiPort -TilePort $TilePort -IncludeWorker -RunID $run.RunID
}
if (-not $InputPath) {
$InputPath = Join-Path $run.OutputDirectory ($ResultCode + ".tif")
& (Get-RsPythonExe) (Join-Path $PSScriptRoot "create_sample_raster.py") `
--output $InputPath `
--preset "ndvi" | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Failed to create sample raster"
}
}
$inspection = Invoke-RsApi -Method "POST" -Path "/api/v1/admin/raster-inspections" -Body @{
path = [System.IO.Path]::GetFullPath($InputPath)
}
$styleVersion = Ensure-RsStyleVersion -ProjectCode $ProjectCode -StyleCode $StyleCode -StyleName $StyleName
$assetVersionBody = @{
format = $inspection.format
storage_uri = $inspection.storage_uri
mime_type = $inspection.mime_type
size_bytes = $inspection.size_bytes
crs = $inspection.crs
width = $inspection.width
height = $inspection.height
band_count = $inspection.band_count
dtype = $inspection.dtype
bbox = $inspection.bbox
metadata = $inspection.metadata
}
if ($inspection.PSObject.Properties.Name -contains "nodata_value" -and $null -ne $inspection.nodata_value) {
$assetVersionBody.nodata_value = $inspection.nodata_value
}
if ($inspection.PSObject.Properties.Name -contains "resolution_x" -and $null -ne $inspection.resolution_x) {
$assetVersionBody.resolution_x = $inspection.resolution_x
}
if ($inspection.PSObject.Properties.Name -contains "resolution_y" -and $null -ne $inspection.resolution_y) {
$assetVersionBody.resolution_y = $inspection.resolution_y
}
$project = Encode-RsURIComponent $ProjectCode
$importBody = @{
requested_by = $RequestedBy
asset = @{
source_ref = $inspection.storage_uri
asset_type = "raster"
display_name = $ResultCode
description = "Imported by demo_import_and_slice.ps1"
metadata = @{
demo_run = $run.RunID
}
}
asset_version = $assetVersionBody
product = @{
code = $ResultCode
type = $ResultType
name = $ResultCode
description = "Demo managed result"
}
product_version = @{
value_type = "continuous"
unit = "index"
bbox = $inspection.bbox
status = "ready"
}
style_binding = @{
style_version_id = $styleVersion.id
is_default = $true
availability = "enabled"
}
}
if ($SourceSystemCode) {
$importBody.source_system_code = $SourceSystemCode
}
$import = Invoke-RsApi -Method "POST" -Path "/api/v1/projects/$project/imports/results" -Body $importBody
$slice = Invoke-RsApi -Method "POST" -Path ("/api/v1/results/{0}/slice" -f $import.product.product.id) -Body @{
requested_by = $RequestedBy
build_cog = $true
generate_preview = $true
generate_legend = $true
generate_tiles = $true
build_cog_input = @{
compression = "deflate"
block_size = 256
force_rebuild = $true
}
preview_input = @{
style_version_id = $styleVersion.id
width = 512
height = 512
format = "png"
force_rebuild = $true
}
legend_input = @{
style_version_id = $styleVersion.id
width = 320
height = 80
format = "png"
force_rebuild = $true
}
tiles_input = @{
style_version_id = $styleVersion.id
zoom_min = $ZoomMin
zoom_max = $ZoomMax
tile_size = 256
tile_format = "webp"
force_rebuild = $true
}
}
$jobStates = @()
if ($WaitForJobs) {
foreach ($job in @($slice.jobs)) {
$jobStates += Wait-RsJob -JobID $job.id
}
}
$result = Invoke-RsApi -Method "GET" -Path ("/api/v1/results/{0}" -f $import.product.product.id)
$delivery = $null
if ($WaitForJobs) {
$delivery = Invoke-RsApi -Method "GET" -Path ("/api/v1/projects/{0}/products/{1}/delivery?version=latest&style_version_id={2}" -f `
(Encode-RsURIComponent $ProjectCode), `
(Encode-RsURIComponent $ResultCode), `
(Encode-RsURIComponent $styleVersion.id))
}
[ordered]@{
run_id = $run.RunID
api_base_url = Get-RsApiBaseUrl
tile_base_url = Get-RsTileBaseUrl
input_path = [System.IO.Path]::GetFullPath($InputPath)
result_id = $import.product.product.id
result_code = $ResultCode
product_version_id = $import.product_version.id
style_version_id = $styleVersion.id
imported_asset_id = $import.asset.id
imported_version_id = $import.asset_version.id
slice_job_ids = @($slice.jobs | ForEach-Object { $_.id })
slice_job_statuses = @($jobStates | ForEach-Object {
[ordered]@{
id = $_.id
job_type = $_.job_type
status = $_.status
}
})
cog_url = if ($delivery) { $delivery.downloads.cog_url } else { $null }
preview_url = if ($delivery -and $delivery.preview) { $delivery.preview.url } else { $null }
legend_url = if ($delivery -and $delivery.legend) { $delivery.legend.url } else { $null }
tileset_url = if ($delivery -and $delivery.tileset) { $delivery.tileset.url_template } else { $null }
result_status = $result.status
run_directory = $run.RunDirectory
log_directory = if ($stack) { $stack.LogDirectory } else { $null }
} | ConvertTo-Json -Depth 16
}
finally {
if ($stack -and -not $KeepStack) {
Stop-RsBackgroundProcesses -Processes $stack.Processes
}
}
+72
View File
@@ -0,0 +1,72 @@
param(
[string]$ProjectCode = "default",
[string]$ResultCode = "",
[string]$ResultID = "",
[string]$TargetSystemCode = "",
[string]$CallbackUrl = "",
[string]$StyleVersionID = "",
[string]$RequestedBy = "demo-user",
[bool]$WaitForJob = $true,
[bool]$IncludeArtifacts = $true,
[int]$TimeoutSec = 180
)
. (Join-Path $PSScriptRoot "common.ps1")
Import-RsDemoEnvironment
if (-not $ResultID -and -not $ResultCode) {
throw "ResultID or ResultCode is required"
}
$result = if ($ResultID) {
Invoke-RsApi -Method "GET" -Path ("/api/v1/results/{0}" -f $ResultID)
} else {
Get-RsResultByCode -ProjectCode $ProjectCode -Code $ResultCode
}
if (-not $StyleVersionID) {
if ($result.delivery -and $result.delivery.style_version -and $result.delivery.style_version.id) {
$StyleVersionID = $result.delivery.style_version.id
} elseif ($result.styles -and $result.styles.Count -gt 0) {
$StyleVersionID = $result.styles[0].id
}
}
if (-not $StyleVersionID) {
throw "No style version is bound to the target result"
}
$body = @{
requested_by = $RequestedBy
style_version_id = $StyleVersionID
include_artifacts = $IncludeArtifacts
}
if ($TargetSystemCode) {
$body.target_system_code = $TargetSystemCode
}
if ($CallbackUrl) {
$body.callback_url = $CallbackUrl
}
if (-not $TargetSystemCode -and -not $CallbackUrl) {
$body.callback_url = "http://127.0.0.1:8920/callback"
}
$push = Invoke-RsApi -Method "POST" -Path ("/api/v1/results/{0}/pushes" -f $result.id) -Body $body
$job = $null
if ($WaitForJob) {
$job = Wait-RsJob -JobID $push.job.id -TimeoutSec $TimeoutSec
}
[ordered]@{
api_base_url = Get-RsApiBaseUrl
result_id = $result.id
result_code = $result.code
style_version_id = $StyleVersionID
target_system_code = $push.target_system_code
callback_url = $push.callback_url
publish_id = $push.publish_id
job_id = $push.job.id
job_status = if ($job) { $job.status } else { $push.job.status }
resource_kind = $push.resource_kind
} | ConvertTo-Json -Depth 8
+27
View File
@@ -0,0 +1,27 @@
param(
[string]$ProjectCode = "default",
[string]$Code = "demo_callback",
[string]$Name = "Demo Callback Receiver",
[string]$CallbackUrl = "http://127.0.0.1:8920/callback",
[string]$CallbackMethod = "POST",
[int]$TimeoutSeconds = 30,
[string]$CreatedBy = "demo-user"
)
. (Join-Path $PSScriptRoot "common.ps1")
Import-RsDemoEnvironment
$target = Ensure-RsTargetSystem `
-ProjectCode $ProjectCode `
-Code $Code `
-Name $Name `
-CallbackUrl $CallbackUrl `
-CallbackMethod $CallbackMethod `
-TimeoutSeconds $TimeoutSeconds `
-CreatedBy $CreatedBy `
-CallbackHeaders @{
"X-Demo-System" = "tile-server-demo"
}
$target | ConvertTo-Json -Depth 8
+16
View File
@@ -0,0 +1,16 @@
@echo off
setlocal
chcp 65001 >nul 2>&1
set "ROOT=%~dp0"
if "%PYTHON_EXE%"=="" set "PYTHON_EXE=C:\Users\Administrator\.conda\envs\InSAR\python.exe"
if "%DEMO_CALLBACK_PORT%"=="" set "DEMO_CALLBACK_PORT=8920"
if "%DEMO_CALLBACK_PATH%"=="" set "DEMO_CALLBACK_PATH=/callback"
if "%DEMO_CALLBACK_OUTPUT_DIR%"=="" set "DEMO_CALLBACK_OUTPUT_DIR=%ROOT%..\temp\demo-callback"
if not exist "%PYTHON_EXE%" (
echo Python interpreter not found: %PYTHON_EXE%
exit /b 1
)
"%PYTHON_EXE%" "%ROOT%demo_callback_receiver.py" --host 127.0.0.1 --port %DEMO_CALLBACK_PORT% --path %DEMO_CALLBACK_PATH% --output-dir "%DEMO_CALLBACK_OUTPUT_DIR%"