- Bump github.com/rabbitmq/amqp091-go 1.12.0 -> 1.13.0 (3 Critical, 7 High,
reachable via rabbitmq.Connect) and golang.org/x/{text,net,crypto},
klauspost/compress; toolchain go1.26.8, builder image pinned to
golang:1.26.8-alpine3.24.
- Build Content-Disposition per RFC 6266/8187 (ASCII fallback +
filename*=UTF-8''), stripping CR/LF, quotes and backslashes (CWE-113).
- Cap request bodies with MaxBytesReader (HTTP_MAX_UPLOAD_BYTES, default
2 GiB); multipart uploads over the limit return 413 (CWE-400).
- /readyz reports "unavailable" instead of raw backend errors (CWE-209).
- Drop deprecated middleware.RealIP; add X-Content-Type-Options: nosniff.
- docker-compose credentials come from ${VAR:-default}.
- Check the RegisterValidation error; annotate verified gosec false
positives with #nosec and a reason.
Re-scan: govulncheck 0 reachable, trivy fs/image 0 High/Critical,
gosec High 0. go test -race passes against PostGIS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
3.5 KiB
Go
110 lines
3.5 KiB
Go
// Package raster converts rasters to Cloud-Optimized GeoTIFFs and reads their
|
|
// footprints using the GDAL command-line tools (gdal_translate, gdalinfo),
|
|
// which must be installed in the worker environment.
|
|
package raster
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// GDALConverter shells out to GDAL.
|
|
type GDALConverter struct {
|
|
compression string
|
|
}
|
|
|
|
// NewGDALConverter returns a converter using DEFLATE compression.
|
|
func NewGDALConverter() *GDALConverter {
|
|
return &GDALConverter{compression: "DEFLATE"}
|
|
}
|
|
|
|
// ToCOG converts the source raster to a Cloud-Optimized GeoTIFF at dst. The COG
|
|
// driver builds internal tiling and overviews.
|
|
func (c *GDALConverter) ToCOG(ctx context.Context, src, dst string) error {
|
|
// Fixed binary, no shell; src/dst are service-generated temp-file paths.
|
|
cmd := exec.CommandContext(ctx, "gdal_translate", // #nosec G204 -- see above
|
|
"-of", "COG",
|
|
"-co", "COMPRESS="+c.compression,
|
|
src, dst,
|
|
)
|
|
var stderr strings.Builder
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("gdal_translate: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Footprint returns the raster's footprint as a GeoJSON polygon in EPSG:4326, or
|
|
// nil if the raster has no spatial reference.
|
|
func (c *GDALConverter) Footprint(ctx context.Context, src string) ([]byte, error) {
|
|
out, err := exec.CommandContext(ctx, "gdalinfo", "-json", src).Output() // #nosec G204 -- fixed binary, no shell; src is a service temp file
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gdalinfo: %w", err)
|
|
}
|
|
|
|
var info struct {
|
|
Wgs84Extent json.RawMessage `json:"wgs84Extent"`
|
|
}
|
|
if err := json.Unmarshal(out, &info); err != nil {
|
|
return nil, fmt.Errorf("parse gdalinfo: %w", err)
|
|
}
|
|
if len(info.Wgs84Extent) == 0 || string(info.Wgs84Extent) == "null" {
|
|
return nil, nil
|
|
}
|
|
return info.Wgs84Extent, nil
|
|
}
|
|
|
|
// VectorFeatures reads every feature of a vector file and returns their
|
|
// geometries, reprojected to EPSG:4326, in file order (nil if the file has no
|
|
// features). A feature with no geometry yields a nil entry at its index, so
|
|
// the result stays aligned with attribute rows read from the same file (the
|
|
// shapefile format itself has no other way to pair a .shp geometry record with
|
|
// its .dbf attribute row than matching by position). Zipped ESRI shapefiles
|
|
// are read in place via GDAL's /vsizip/ virtual filesystem; GeoJSON and
|
|
// GeoPackage are read directly.
|
|
func (c *GDALConverter) VectorFeatures(ctx context.Context, src string) ([]json.RawMessage, error) {
|
|
input := src
|
|
if strings.EqualFold(filepath.Ext(src), ".zip") {
|
|
input = "/vsizip/" + src
|
|
}
|
|
|
|
// Fixed binary, no shell; input is a service-generated temp-file path.
|
|
cmd := exec.CommandContext(ctx, "ogr2ogr", // #nosec G204 -- see above
|
|
"-f", "GeoJSON",
|
|
"-t_srs", "EPSG:4326",
|
|
"/vsistdout/", input,
|
|
)
|
|
var stderr strings.Builder
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ogr2ogr: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
|
|
var fc struct {
|
|
Features []struct {
|
|
Geometry json.RawMessage `json:"geometry"`
|
|
} `json:"features"`
|
|
}
|
|
if err := json.Unmarshal(out, &fc); err != nil {
|
|
return nil, fmt.Errorf("parse ogr2ogr output: %w", err)
|
|
}
|
|
if len(fc.Features) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
geoms := make([]json.RawMessage, len(fc.Features))
|
|
for i, f := range fc.Features {
|
|
if len(f.Geometry) == 0 || string(f.Geometry) == "null" {
|
|
continue
|
|
}
|
|
geoms[i] = f.Geometry
|
|
}
|
|
return geoms, nil
|
|
}
|