A Rust client for browsing and downloading Sentinel satellite products from the Copernicus Data Space Ecosystem (CDSE).
- Search the CDSE catalogue by satellite, product type, date range, cloud cover, tile ID, point, bounding box, or GeoJSON geometry
- Download scenes by name with Bearer-token authentication, to local filesystem or directly to an S3-compatible bucket
- Batch download multiple products concurrently with configurable parallelism
- Authenticate against the CDSE OAuth2 identity provider
- Supports Sentinel-1, Sentinel-2, Sentinel-3, Sentinel-5P, and Sentinel-6
- Async-first design (tokio) with synchronous
blockingwrappers - Usable as a Rust library, a CLI, or from Python via native bindings
- Rust 1.85+ (edition 2024)
- A free Copernicus Data Space account (required for authentication and downloads; searching is anonymous)
- For Python bindings: Python 3.9+, uv (recommended), and maturin
The repository is a Cargo workspace with two members:
copernicus_explorer/ Cargo workspace root
Cargo.toml Workspace manifest
copernicus_explorer/ Core Rust library + CLI binary
Cargo.toml
src/
lib.rs Module declarations and re-exports
main.rs CLI binary (clap)
error.rs CopernicusError enum (thiserror)
models.rs Satellite enum, Product struct (serde)
geometry.rs Point, BoundingBox, Polygon, GeoJSON parsing, WKT conversion
auth.rs OAuth2 token retrieval (reqwest, async)
search.rs SearchQuery builder, OData filter construction (async)
download.rs Single & batch download with streaming I/O + progress bars (async)
s3.rs S3Config, OutputDestination, INI config parser, S3 upload
blocking.rs Synchronous wrappers (block_on) for non-async contexts
examples/
test_rust_api.rs Interactive demo: search, download one or all results
python/ Python bindings (PyO3 + maturin)
Cargo.toml
pyproject.toml
src/
lib.rs #[pymodule] wrapping the core library
examples/
test_python_api.py Interactive demo: search, download one or all results
git clone <repo-url>
cd copernicus_explorer
cargo build --releaseThe CLI binary is produced at target/release/copernicus_explorer.
copernicus_explorer <COMMAND>
Commands:
search Search the CDSE catalogue for satellite products
download Download one or more scenes by name or by product ID
auth Test authentication and print a token summary
help Print this message or the help of the given subcommand(s)
Search the catalogue. Dates default to the last 30 days if omitted.
# Sentinel-2 L2A near Toulouse, max 30% cloud cover
copernicus_explorer search sentinel-2 -p L2A --point 43.6,1.44 -c 30
# Sentinel-1 GRD over the Alps with explicit date range
copernicus_explorer search sentinel-1 -p GRD \
--bbox 47.5,6.0,45.5,11.0 \
--start 2026-03-01 --end 2026-03-24
# Sentinel-2 by tile, limit to 3 results
copernicus_explorer search sentinel-2 -p L2A --tile T31TFJ -n 3
# Search using a GeoJSON file as the area of interest
copernicus_explorer search sentinel-2 -p L2A --geojson roi.geojson -c 30Options:
| Flag | Description |
|---|---|
<SATELLITE> |
sentinel-1, sentinel-2, sentinel-3, sentinel-5p, sentinel-6 |
-p, --product <TYPE> |
Product type filter (e.g. L2A, L1C, GRD) |
--start <YYYY-MM-DD> |
Start of acquisition window (default: 30 days ago) |
--end <YYYY-MM-DD> |
End of acquisition window (default: today) |
--tile <TILE> |
Sentinel-2 tile identifier (e.g. T31TFJ) |
-c, --cloud <0-100> |
Maximum cloud cover percentage |
--point <LAT,LON> |
Point geometry (e.g. 43.6,1.44) |
--bbox <TLAT,LLON,BLAT,RLON> |
Bounding box (e.g. 47.5,6.0,45.5,11.0) |
--geojson <FILE> |
GeoJSON file defining the area of interest |
-n, --max-results <N> |
Maximum number of results (default: 10) |
Test your credentials. Reads COPERNICUS_USER and COPERNICUS_PASS from the
environment, or accepts them as flags.
# From environment variables
export COPERNICUS_USER="[email protected]"
export COPERNICUS_PASS="yourpassword"
copernicus_explorer auth
# Or pass directly
copernicus_explorer auth -u [email protected] -P yourpasswordDownload one or more scenes by name or by CDSE product ID. Requires authentication.
# Single scene by name
copernicus_explorer download \
"S2B_MSIL2A_20260315T105019_N0512_R051_T31TCJ_20260315T144522.SAFE" \
-o ./data
# Multiple scenes concurrently (max 4 in parallel by default)
copernicus_explorer download \
"S2B_MSIL2A_20260315T105019_N0512_R051_T31TCJ_20260315T144522.SAFE" \
"S2A_MSIL2A_20260317T104021_N0512_R008_T31TCJ_20260317T160837.SAFE" \
-o ./data -j 2
# Download by product UUID (skips the name-to-ID resolution query)
copernicus_explorer download --id \
"a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-o ./data
# Download directly to an S3-compatible bucket
copernicus_explorer download --id \
"a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-o s3://my-bucket/SAFE/ --s3-config ~/.config/copernicus_explorer/s3.confS3 credentials are resolved in order: --s3-config flag, default config at
~/.config/copernicus_explorer/s3.conf, then environment variables (S3_*,
then AWS_*). The config file uses rclone-style INI format where the section
name matches the bucket name:
[my-bucket]
access_key_id = ...
secret_access_key = ...
region = sbg
endpoint = https://s3.sbg.perf.cloud.ovh.netIf the file contains multiple sections, the one matching the bucket from the
s3:// URI is used. If no section matches, resolution falls back to
environment variables.
| Variable (checked first) | Fallback variable |
|---|---|
S3_ACCESS_KEY_ID |
AWS_ACCESS_KEY_ID |
S3_SECRET_ACCESS_KEY |
AWS_SECRET_ACCESS_KEY |
S3_ENDPOINT |
AWS_ENDPOINT_URL |
S3_REGION |
AWS_REGION |
Options:
| Flag | Description |
|---|---|
<SCENES>... |
One or more scene names or product IDs (depending on --id) |
--id |
Treat arguments as CDSE product UUIDs instead of scene names |
-o, --output-dir <DIR or S3 URI> |
Output directory or s3://bucket/prefix/ (default: .) |
--s3-config <FILE> |
Path to S3 credentials config file (rclone-style INI) |
-j, --concurrent <N> |
Maximum concurrent downloads (default: 4) |
-u, --user <USER> |
Username (or set COPERNICUS_USER) |
-P, --pass <PASS> |
Password (or set COPERNICUS_PASS) |
The library is async-first (tokio). A blocking module provides synchronous
wrappers for contexts that don't use an async runtime.
Add to your Cargo.toml:
[dependencies]
copernicus_explorer = { path = "../copernicus_explorer" }
chrono = "0.4"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }use chrono::{Duration, Utc};
use copernicus_explorer::{
Geometry, Point, Products, Satellite, SearchQuery,
download_products, get_access_token_from_env,
};
#[tokio::main]
async fn main() -> Result<(), copernicus_explorer::CopernicusError> {
// Search (no authentication required)
let products = SearchQuery::new(Satellite::Sentinel2)
.product("L2A")
.dates(Utc::now() - Duration::days(20), Utc::now())
.max_cloud_cover(30.0)
.geometry(Geometry::Point(Point::new(43.6, 1.44)))
.max_results(5)
.execute()
.await?;
println!("{}", Products(&products));
// Authenticate and download all results (3 concurrent)
let token = get_access_token_from_env().await?;
let results = download_products(&products, "./data".as_ref(), &token, 3).await;
for (product, result) in products.iter().zip(results.iter()) {
match result {
Ok(path) => println!(" OK: {} -> {}", product.name, path.display()),
Err(e) => println!(" FAILED: {} -> {e}", product.name),
}
}
Ok(())
}use copernicus_explorer::{Satellite, SearchQuery, blocking};
fn main() -> Result<(), copernicus_explorer::CopernicusError> {
let products = SearchQuery::new(Satellite::Sentinel2)
.product("L2A")
.max_results(5)
.execute_blocking()?;
let token = blocking::get_access_token_from_env()?;
let results = blocking::download_products(&products, "./data".as_ref(), &token, 4);
for (product, result) in products.iter().zip(results.iter()) {
match result {
Ok(path) => println!(" OK: {} -> {}", product.name, path.display()),
Err(e) => println!(" FAILED: {} -> {e}", product.name),
}
}
Ok(())
}More examples can be found in rust examples
The python/ crate provides native Python bindings via
PyO3 and maturin.
The compiled extension runs at full Rust speed with no serialization overhead.
cd python
uv venv .venv
source .venv/bin/activate
uv pip install maturin
maturin developfrom datetime import datetime, timedelta, timezone
import copernicus_explorer_py as ce
query = ce.SearchQuery(ce.Satellite.sentinel2())
query.product("L2A")
query.dates(
datetime.now(timezone.utc) - timedelta(days=20),
datetime.now(timezone.utc),
)
query.max_cloud_cover(30.0)
query.geometry_point(ce.Point(43.6, 1.44))
query.max_results(5)
products = query.execute()
ce.print_products(products)token = ce.get_access_token_from_env()
path = ce.download_scene(products[0].name, "./data", token)
print(f"Downloaded to {path}")token = ce.get_access_token_from_env()
results = ce.download_products(products, "./data", token, max_concurrent=3)
for product, result in zip(products, results):
if result is not None:
print(f" OK: {product.name} -> {result}")
else:
print(f" FAILED: {product.name}")More examples can be found in python examples
Classes:
| Class | Constructor | Key methods / attributes |
|---|---|---|
Satellite |
Satellite.sentinel1(), .sentinel2(), .sentinel3(), .sentinel5p(), .sentinel6() |
.collection_name(), .known_products(), .is_valid_product(str) |
SearchQuery |
SearchQuery(satellite) |
.product(str), .dates(start, end), .tile(str), .max_cloud_cover(float), .geometry_point(Point), .geometry_bbox(BoundingBox), .geometry_geojson(str), .max_results(int), .execute() |
Product |
returned by SearchQuery.execute() |
.name, .id, .acquisition_date, .publication_date, .online, .cloud_cover |
Point |
Point(lat, lon) |
.lat, .lon |
BoundingBox |
BoundingBox((lat, lon), (lat, lon)) |
.upper_left, .lower_right |
Functions:
| Function | Description |
|---|---|
get_access_token(username, password) |
Authenticate and return an access token string |
get_access_token_from_env() |
Authenticate using COPERNICUS_USER / COPERNICUS_PASS env vars |
download_scene(scene_name, directory, token, s3_config=None) |
Download a single scene by name; returns the output file path (or S3 URI) |
download_by_id(id, directory, token, s3_config=None) |
Download a single product by CDSE UUID; skips name-to-ID resolution |
download_products(products, directory, token, max_concurrent=4, s3_config=None) |
Download multiple products concurrently; returns a list of paths (or None on failure) |
get_scene_id(scene_name) |
Resolve a scene name to its CDSE UUID |
# Rust unit tests (from workspace root)
cargo test
# Quick Python smoke test
cd python
source .venv/bin/activate
python -c "
import copernicus_explorer_py as ce
s2 = ce.Satellite.sentinel2()
print(s2, s2.known_products())
q = ce.SearchQuery(s2).product('L2A').max_results(3)
print(q)
"See CHANGELOG.md for release notes.
This project is licensed under the MIT License.