diff --git a/Cargo.toml b/Cargo.toml index 4c17d35b..0b3fefff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ bb8-postgres = "0.8.1" bytes = "1.7" chrono = "0.4.38" clap = "4.5" +cql2 = "0.3.0" duckdb = "=1.0.0" fluent-uri = "0.3.1" futures = "0.3.31" @@ -60,7 +61,7 @@ mime = "0.3.17" mockito = "1.5" object_store = "0.11.0" openssl = { version = "0.10.68", features = ["vendored"] } -openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python +openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python parquet = { version = "52.2", default-features = false } pgstac = { version = "0.2.1", path = "crates/pgstac" } pyo3 = "0.22.3" @@ -89,6 +90,9 @@ tokio-test = "0.4.4" tower = "0.5.1" tower-http = "0.6.1" tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-subscriber = { version = "0.3.18", features = [ + "env-filter", + "tracing-log", +] } url = "2.3" webpki-roots = "0.26.6" diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index dd466d88..4590db27 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -25,6 +25,7 @@ geo = ["dep:geo", "stac/geo"] [dependencies] async-stream = { workspace = true, optional = true } chrono.workspace = true +cql2.workspace = true futures = { workspace = true, optional = true } http = { workspace = true, optional = true } reqwest = { workspace = true, features = ["json"], optional = true } diff --git a/crates/api/src/client.rs b/crates/api/src/client.rs index e78177bb..da808ac8 100644 --- a/crates/api/src/client.rs +++ b/crates/api/src/client.rs @@ -220,12 +220,7 @@ impl Client { if let Some(headers) = headers.into() { request = request.headers(headers); } - let response = request.send().await?; - if !response.status().is_success() { - let status_code = response.status(); - let text = response.text().await.ok().unwrap_or_default(); - return Err(Error::Request { status_code, text }); - } + let response = request.send().await?.error_for_status()?; response.json().await.map_err(Error::from) } @@ -361,8 +356,12 @@ fn stream_pages( fn not_found_to_none(result: Result) -> Result> { let mut result = result.map(Some); - if let Err(Error::Request { status_code, .. }) = result { - if status_code == StatusCode::NOT_FOUND { + if let Err(Error::Reqwest(ref err)) = result { + if err + .status() + .map(|s| s == StatusCode::NOT_FOUND) + .unwrap_or_default() + { result = Ok(None); } } diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs index e8a4799d..39f02f63 100644 --- a/crates/api/src/error.rs +++ b/crates/api/src/error.rs @@ -21,6 +21,10 @@ pub enum Error { #[error(transparent)] ChronoParse(#[from] chrono::ParseError), + /// [cql2::Error] + #[error(transparent)] + Cql2(#[from] cql2::Error), + /// [geojson::Error] #[error(transparent)] GeoJson(#[from] Box), @@ -75,17 +79,6 @@ pub enum Error { #[cfg(feature = "client")] Reqwest(#[from] reqwest::Error), - /// A search error. - #[error("HTTP status error ({status_code}): {text}")] - #[cfg(feature = "client")] - Request { - /// The status code - status_code: reqwest::StatusCode, - - /// The text of the server response. - text: String, - }, - /// A search has both bbox and intersects. #[error("search has bbox and intersects")] SearchHasBboxAndIntersects(Box), diff --git a/crates/api/src/filter.rs b/crates/api/src/filter.rs index 44e44e8b..47af3d0f 100644 --- a/crates/api/src/filter.rs +++ b/crates/api/src/filter.rs @@ -1,7 +1,7 @@ -use std::{convert::Infallible, str::FromStr}; - +use crate::Result; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use std::{convert::Infallible, str::FromStr}; /// The language of the filter expression. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -16,6 +16,21 @@ pub enum Filter { Cql2Json(Map), } +impl Filter { + /// Converts this filter to cql2-json. + pub fn into_cql2_json(self) -> Result { + match self { + Filter::Cql2Json(_) => Ok(self), + Filter::Cql2Text(text) => { + let expr = cql2::parse_text(&text)?; + Ok(Filter::Cql2Json(serde_json::from_value( + serde_json::to_value(expr)?, + )?)) + } + } + } +} + impl Default for Filter { fn default() -> Self { Filter::Cql2Json(Default::default()) @@ -24,7 +39,7 @@ impl Default for Filter { impl FromStr for Filter { type Err = Infallible; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { Ok(Filter::Cql2Text(s.to_string())) } } diff --git a/crates/api/src/items.rs b/crates/api/src/items.rs index 16131ce8..6471d13d 100644 --- a/crates/api/src/items.rs +++ b/crates/api/src/items.rs @@ -291,6 +291,14 @@ impl Items { collections: Some(vec![collection_id.to_string()]), } } + + /// Converts the filter to cql2-json, if it is set. + pub fn into_cql2_json(mut self) -> Result { + if let Some(filter) = self.filter { + self.filter = Some(filter.into_cql2_json()?); + } + Ok(self) + } } impl TryFrom for GetItems { diff --git a/crates/api/src/search.rs b/crates/api/src/search.rs index 94a1b457..74ffea65 100644 --- a/crates/api/src/search.rs +++ b/crates/api/src/search.rs @@ -211,6 +211,12 @@ impl Search { Ok(true) } } + + /// Converts this search's filter to cql2-json, if set. + pub fn into_cql2_json(mut self) -> Result { + self.items = self.items.into_cql2_json()?; + Ok(self) + } } impl TryFrom for GetSearch { diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 9a3b5723..42e91311 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -70,6 +70,10 @@ pub struct Args { #[derive(Debug, clap::Subcommand, Clone)] #[allow(clippy::large_enum_variant)] pub enum Subcommand { + /// Interact with a pgstac database + #[cfg(feature = "pgstac")] + Pgstac(crate::subcommand::pgstac::Args), + /// Search for STAC items Search(search::Args), @@ -99,6 +103,8 @@ impl Args { /// Runs whatever these arguments say that we should run. pub async fn run(self) -> Result<()> { match &self.subcommand { + #[cfg(feature = "pgstac")] + Subcommand::Pgstac(args) => self.pgstac(args).await, Subcommand::Search(args) => self.search(args).await, Subcommand::Serve(args) => self.serve(args).await, Subcommand::Translate(args) => self.translate(args).await, diff --git a/crates/cli/src/subcommand/mod.rs b/crates/cli/src/subcommand/mod.rs index 3a929481..e9c24713 100644 --- a/crates/cli/src/subcommand/mod.rs +++ b/crates/cli/src/subcommand/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "pgstac")] +pub mod pgstac; pub mod search; pub mod serve; pub mod translate; diff --git a/crates/cli/src/subcommand/pgstac.rs b/crates/cli/src/subcommand/pgstac.rs new file mode 100644 index 00000000..7e00c22f --- /dev/null +++ b/crates/cli/src/subcommand/pgstac.rs @@ -0,0 +1,57 @@ +use crate::Result; +use stac_server::PgstacBackend; + +#[derive(Debug, Clone, clap::Args)] +pub struct Args { + /// The pgstac subcommand + #[command(subcommand)] + subcommand: Subcommand, +} + +#[derive(clap::Subcommand, Debug, Clone)] +pub enum Subcommand { + /// Loads data into the pgstac database + Load(LoadArgs), +} + +#[derive(clap::Args, Debug, Clone)] +pub struct LoadArgs { + /// The connection string. + dsn: String, + + /// Hrefs to load into the database. + /// + /// If not provided or `-`, data will be read from standard input. + hrefs: Vec, + + /// Load in all "item" links on collections. + #[arg(short, long)] + load_collection_items: bool, + + /// Auto-generate collections for any collection-less items. + #[arg(short, long)] + create_collections: bool, +} + +impl crate::Args { + pub async fn pgstac(&self, args: &Args) -> Result<()> { + match &args.subcommand { + Subcommand::Load(load_args) => { + let mut backend = PgstacBackend::new_from_stringlike(&load_args.dsn).await?; + let load = self + .load( + &mut backend, + load_args.hrefs.iter().map(|h| h.as_str()), + load_args.load_collection_items, + load_args.create_collections, + ) + .await?; + eprintln!( + "Loaded {} collection(s) and {} item(s)", + load.collections, load.items + ); + Ok(()) + } + } + } +} diff --git a/crates/pgstac/Cargo.toml b/crates/pgstac/Cargo.toml index 8b899585..5de2cb8c 100644 --- a/crates/pgstac/Cargo.toml +++ b/crates/pgstac/Cargo.toml @@ -24,6 +24,7 @@ stac-api.workspace = true thiserror.workspace = true tokio-postgres = { workspace = true, features = ["with-serde_json-1"] } tokio-postgres-rustls = { workspace = true, optional = true } +tracing.workspace = true webpki-roots = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/pgstac/README.md b/crates/pgstac/README.md index 7539419c..28acb036 100644 --- a/crates/pgstac/README.md +++ b/crates/pgstac/README.md @@ -24,9 +24,9 @@ See the [documentation](https://docs.rs/pgstac) for more. To test: ```shell -docker-compose -f pgstac/docker-compose.yml up -d +docker compose up -d cargo test -p pgstac -docker-compose -f pgstac/docker-compose.yml down +docker compose down ``` Each test is run in its own transaction, which is rolled back after the test. diff --git a/crates/pgstac/src/client.rs b/crates/pgstac/src/client.rs index 10aa1f66..73911aa5 100644 --- a/crates/pgstac/src/client.rs +++ b/crates/pgstac/src/client.rs @@ -177,7 +177,9 @@ impl<'a, C: GenericClient> Client<'a, C> { /// Searches for items. pub async fn search(&self, search: Search) -> Result { + let search = search.into_cql2_json()?; let search = serde_json::to_value(search)?; + tracing::debug!("searching: {:?}", search); self.value("search", &[&search]).await } diff --git a/crates/pgstac/src/lib.rs b/crates/pgstac/src/lib.rs index ff7f90e2..59676987 100644 --- a/crates/pgstac/src/lib.rs +++ b/crates/pgstac/src/lib.rs @@ -60,6 +60,10 @@ pub enum Error { #[error(transparent)] SerdeJson(#[from] serde_json::Error), + /// [stac_api::Error] + #[error(transparent)] + StacApi(#[from] stac_api::Error), + /// [tokio_postgres::Error] #[error(transparent)] TokioPostgres(#[from] tokio_postgres::Error), diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index f2aa63b9..0ecf7364 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -32,6 +32,7 @@ stac-types.workspace = true thiserror.workspace = true tokio-postgres = { workspace = true, optional = true } tower-http = { workspace = true, features = ["cors"], optional = true } +tracing.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/server/README.md b/crates/server/README.md index f6d5aa00..33006ea3 100644 --- a/crates/server/README.md +++ b/crates/server/README.md @@ -23,6 +23,13 @@ To use the [pgstac](https://github.com/stac-utils/pgstac) backend: stac serve --pgstac postgresql://username:password@localhost:5432/postgis ``` +If you'd like to serve your own **pgstac** backend with some sample items: + +```shell +docker compose up -d pgstac +scripts/load-pgstac-fixtures # This might take a while, e.g. 30 seconds or so +``` + ### Library To use this library in another application: diff --git a/crates/server/src/backend/mod.rs b/crates/server/src/backend/mod.rs index 0ed898e3..808efcfd 100644 --- a/crates/server/src/backend/mod.rs +++ b/crates/server/src/backend/mod.rs @@ -88,8 +88,9 @@ pub trait Backend: Clone + Sync + Send + 'static { /// ``` fn add_item(&mut self, item: Item) -> impl Future> + Send; - /// Adds several items. + /// Adds multiple items. fn add_items(&mut self, items: Vec) -> impl Future> + Send { + tracing::debug!("adding {} items using naïve loading", items.len()); async move { for item in items { self.add_item(item).await?; diff --git a/crates/server/src/backend/pgstac.rs b/crates/server/src/backend/pgstac.rs index d410cf2a..1ede528e 100644 --- a/crates/server/src/backend/pgstac.rs +++ b/crates/server/src/backend/pgstac.rs @@ -71,6 +71,7 @@ where params: impl ToString, tls: Tls, ) -> Result> { + let params = params.to_string(); let connection_manager = PostgresConnectionManager::new_from_stringlike(params, tls)?; let pool = Pool::builder().build(connection_manager).await?; Ok(PgstacBackend { pool }) @@ -112,6 +113,13 @@ where client.add_item(item).await.map_err(Error::from) } + async fn add_items(&mut self, items: Vec) -> Result<()> { + tracing::debug!("adding {} items using pgstac loading", items.len()); + let client = self.pool.get().await?; + let client = Client::new(&*client); + client.add_items(&items).await.map_err(Error::from) + } + async fn items(&self, collection_id: &str, items: Items) -> Result> { // TODO should we check for collection existence? let search = items.search_collection(collection_id); @@ -127,12 +135,6 @@ where .map_err(Error::from) } - async fn add_items(&mut self, items: Vec) -> Result<()> { - let client = self.pool.get().await?; - let client = Client::new(&*client); - client.add_items(&items).await.map_err(Error::from) - } - async fn search(&self, search: Search) -> Result { let client = self.pool.get().await?; let client = Client::new(&*client); diff --git a/crates/server/src/routes.rs b/crates/server/src/routes.rs index 29fd3996..f0e8d213 100644 --- a/crates/server/src/routes.rs +++ b/crates/server/src/routes.rs @@ -216,6 +216,7 @@ pub async fn get_search( State(api): State>, search: Query, ) -> Result> { + tracing::debug!("GET /search: {:?}", search.0); let search = Search::try_from(search.0) .and_then(Search::valid) .map_err(|error| Error::BadRequest(error.to_string()))?; diff --git a/crates/pgstac/docker-compose.yml b/docker-compose.yml similarity index 96% rename from crates/pgstac/docker-compose.yml rename to docker-compose.yml index 7d7835ef..4bf33251 100644 --- a/crates/pgstac/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - database: + pgstac: container_name: stac-rs image: ghcr.io/stac-utils/pgstac:v0.8.6 environment: diff --git a/pyproject.toml b/pyproject.toml index 466cad1d..22d30355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "stac-rs" version = "0.0.0" -description = "This package should never be released, its just for uv." +description = "This package should never be released, it's just for uv." requires-python = ">=3.12" dependencies = [] diff --git a/scripts/fixtures/1000-sentinel-2-items.parquet b/scripts/fixtures/1000-sentinel-2-items.parquet new file mode 100644 index 00000000..6d8e0546 Binary files /dev/null and b/scripts/fixtures/1000-sentinel-2-items.parquet differ diff --git a/scripts/fixtures/sentinel-2-l2a.json b/scripts/fixtures/sentinel-2-l2a.json new file mode 100644 index 00000000..efc4a0df --- /dev/null +++ b/scripts/fixtures/sentinel-2-l2a.json @@ -0,0 +1 @@ +{"id":"sentinel-2-l2a","type":"Collection","links":[{"rel":"items","type":"application/geo+json","href":"https://planetarycomputer.microsoft.com/api/stac/v1/collections/sentinel-2-l2a/items"},{"rel":"parent","type":"application/json","href":"https://planetarycomputer.microsoft.com/api/stac/v1/"},{"rel":"root","type":"application/json","href":"https://planetarycomputer.microsoft.com/api/stac/v1/"},{"rel":"self","type":"application/json","href":"https://planetarycomputer.microsoft.com/api/stac/v1/collections/sentinel-2-l2a"},{"rel":"license","href":"https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf","title":"Copernicus Sentinel data terms"},{"rel":"describedby","href":"https://planetarycomputer.microsoft.com/dataset/sentinel-2-l2a","title":"Human readable dataset overview and reference","type":"text/html"}],"title":"Sentinel-2 Level-2A","assets":{"thumbnail":{"href":"https://ai4edatasetspublicassets.blob.core.windows.net/assets/pc_thumbnails/sentinel-2.png","type":"image/png","roles":["thumbnail"],"title":"Sentinel 2 L2A"},"geoparquet-items":{"href":"abfs://items/sentinel-2-l2a.parquet","type":"application/x-parquet","roles":["stac-items"],"title":"GeoParquet STAC items","description":"Snapshot of the collection's STAC items exported to GeoParquet format.","msft:partition_info":{"is_partitioned":true,"partition_frequency":"W-MON"},"table:storage_options":{"account_name":"pcstacitems"}}},"extent":{"spatial":{"bbox":[[-180,-90,180,90]]},"temporal":{"interval":[["2015-06-27T10:25:31Z",null]]}},"license":"proprietary","keywords":["Sentinel","Copernicus","ESA","Satellite","Global","Imagery","Reflectance"],"providers":[{"url":"https://sentinel.esa.int/web/sentinel/missions/sentinel-2","name":"ESA","roles":["producer","licensor"]},{"url":"https://www.esri.com/","name":"Esri","roles":["processor"]},{"url":"https://planetarycomputer.microsoft.com","name":"Microsoft","roles":["host","processor"]}],"summaries":{"gsd":[10,20,60],"eo:bands":[{"name":"AOT","description":"aerosol optical thickness"},{"gsd":60,"name":"B01","common_name":"coastal","description":"coastal aerosol","center_wavelength":0.443,"full_width_half_max":0.027},{"gsd":10,"name":"B02","common_name":"blue","description":"visible blue","center_wavelength":0.49,"full_width_half_max":0.098},{"gsd":10,"name":"B03","common_name":"green","description":"visible green","center_wavelength":0.56,"full_width_half_max":0.045},{"gsd":10,"name":"B04","common_name":"red","description":"visible red","center_wavelength":0.665,"full_width_half_max":0.038},{"gsd":20,"name":"B05","common_name":"rededge","description":"vegetation classification red edge","center_wavelength":0.704,"full_width_half_max":0.019},{"gsd":20,"name":"B06","common_name":"rededge","description":"vegetation classification red edge","center_wavelength":0.74,"full_width_half_max":0.018},{"gsd":20,"name":"B07","common_name":"rededge","description":"vegetation classification red edge","center_wavelength":0.783,"full_width_half_max":0.028},{"gsd":10,"name":"B08","common_name":"nir","description":"near infrared","center_wavelength":0.842,"full_width_half_max":0.145},{"gsd":20,"name":"B8A","common_name":"rededge","description":"vegetation classification red edge","center_wavelength":0.865,"full_width_half_max":0.033},{"gsd":60,"name":"B09","description":"water vapor","center_wavelength":0.945,"full_width_half_max":0.026},{"gsd":20,"name":"B11","common_name":"swir16","description":"short-wave infrared, snow/ice/cloud classification","center_wavelength":1.61,"full_width_half_max":0.143},{"gsd":20,"name":"B12","common_name":"swir22","description":"short-wave infrared, snow/ice/cloud classification","center_wavelength":2.19,"full_width_half_max":0.242}],"platform":["Sentinel-2A","Sentinel-2B"],"instruments":["msi"],"constellation":["sentinel-2"],"view:off_nadir":[0]},"description":"The [Sentinel-2](https://sentinel.esa.int/web/sentinel/missions/sentinel-2) program provides global imagery in thirteen spectral bands at 10m-60m resolution and a revisit time of approximately five days. This dataset represents the global Sentinel-2 archive, from 2016 to the present, processed to L2A (bottom-of-atmosphere) using [Sen2Cor](https://step.esa.int/main/snap-supported-plugins/sen2cor/) and converted to [cloud-optimized GeoTIFF](https://www.cogeo.org/) format.","item_assets":{"AOT":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Aerosol optical thickness (AOT)"},"B01":{"gsd":60.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 1 - Coastal aerosol - 60m","eo:bands":[{"name":"B01","common_name":"coastal","description":"Band 1 - Coastal aerosol","center_wavelength":0.443,"full_width_half_max":0.027}]},"B02":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 2 - Blue - 10m","eo:bands":[{"name":"B02","common_name":"blue","description":"Band 2 - Blue","center_wavelength":0.49,"full_width_half_max":0.098}]},"B03":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 3 - Green - 10m","eo:bands":[{"name":"B03","common_name":"green","description":"Band 3 - Green","center_wavelength":0.56,"full_width_half_max":0.045}]},"B04":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 4 - Red - 10m","eo:bands":[{"name":"B04","common_name":"red","description":"Band 4 - Red","center_wavelength":0.665,"full_width_half_max":0.038}]},"B05":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 5 - Vegetation red edge 1 - 20m","eo:bands":[{"name":"B05","common_name":"rededge","description":"Band 5 - Vegetation red edge 1","center_wavelength":0.704,"full_width_half_max":0.019}]},"B06":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 6 - Vegetation red edge 2 - 20m","eo:bands":[{"name":"B06","common_name":"rededge","description":"Band 6 - Vegetation red edge 2","center_wavelength":0.74,"full_width_half_max":0.018}]},"B07":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 7 - Vegetation red edge 3 - 20m","eo:bands":[{"name":"B07","common_name":"rededge","description":"Band 7 - Vegetation red edge 3","center_wavelength":0.783,"full_width_half_max":0.028}]},"B08":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 8 - NIR - 10m","eo:bands":[{"name":"B08","common_name":"nir","description":"Band 8 - NIR","center_wavelength":0.842,"full_width_half_max":0.145}]},"B09":{"gsd":60.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 9 - Water vapor - 60m","eo:bands":[{"name":"B09","description":"Band 9 - Water vapor","center_wavelength":0.945,"full_width_half_max":0.026}]},"B11":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 11 - SWIR (1.6) - 20m","eo:bands":[{"name":"B11","common_name":"swir16","description":"Band 11 - SWIR (1.6)","center_wavelength":1.61,"full_width_half_max":0.143}]},"B12":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 12 - SWIR (2.2) - 20m","eo:bands":[{"name":"B12","common_name":"swir22","description":"Band 12 - SWIR (2.2)","center_wavelength":2.19,"full_width_half_max":0.242}]},"B8A":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Band 8A - Vegetation red edge 4 - 20m","eo:bands":[{"name":"B8A","common_name":"rededge","description":"Band 8A - Vegetation red edge 4","center_wavelength":0.865,"full_width_half_max":0.033}]},"SCL":{"gsd":20.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Scene classfication map (SCL)"},"WVP":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"Water vapour (WVP)"},"visual":{"gsd":10.0,"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["data"],"title":"True color image","eo:bands":[{"name":"B04","common_name":"red","description":"Band 4 - Red","center_wavelength":0.665,"full_width_half_max":0.038},{"name":"B03","common_name":"green","description":"Band 3 - Green","center_wavelength":0.56,"full_width_half_max":0.045},{"name":"B02","common_name":"blue","description":"Band 2 - Blue","center_wavelength":0.49,"full_width_half_max":0.098}]},"preview":{"type":"image/tiff; application=geotiff; profile=cloud-optimized","roles":["thumbnail"],"title":"Thumbnail"},"safe-manifest":{"type":"application/xml","roles":["metadata"],"title":"SAFE manifest"},"granule-metadata":{"type":"application/xml","roles":["metadata"],"title":"Granule metadata"},"inspire-metadata":{"type":"application/xml","roles":["metadata"],"title":"INSPIRE metadata"},"product-metadata":{"type":"application/xml","roles":["metadata"],"title":"Product metadata"},"datastrip-metadata":{"type":"application/xml","roles":["metadata"],"title":"Datastrip metadata"}},"stac_version":"1.0.0","msft:container":"sentinel2-l2","stac_extensions":["https://stac-extensions.github.io/item-assets/v1.0.0/schema.json","https://stac-extensions.github.io/table/v1.2.0/schema.json"],"msft:storage_account":"sentinel2l2a01","msft:short_description":"The Sentinel-2 program provides global imagery in thirteen spectral bands at 10m-60m resolution and a revisit time of approximately five days. This dataset contains the global Sentinel-2 archive, from 2016 to the present, processed to L2A (bottom-of-atmosphere).","msft:region":"westeurope"} \ No newline at end of file diff --git a/scripts/load-pgstac-fixtures b/scripts/load-pgstac-fixtures new file mode 100755 index 00000000..c0c294a5 --- /dev/null +++ b/scripts/load-pgstac-fixtures @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +set -e + +scripts=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +fixtures="$scripts/fixtures" +dsn=postgresql://username:password@localhost:5432/postgis + +cargo run -- pgstac load "$dsn" "$fixtures/sentinel-2-l2a.json" "$fixtures/1000-sentinel-2-items.parquet"