use std::path::Path;
use super::{Conditional, FetchResult, HttpTransport, Transport};
use crate::auth::AuthResolver;
use crate::error::{Error, Result};
pub const DEFAULT_S3_REGION: &str = "us-east-2";
pub fn resolve_region(explicit: Option<&str>) -> String {
if let Some(r) = explicit {
return r.to_string();
}
for var in ["EARTHSCI_S3_REGION", "AWS_REGION"] {
if let Ok(v) = std::env::var(var) {
if !v.is_empty() {
return v;
}
}
}
DEFAULT_S3_REGION.to_string()
}
pub fn s3_https_url(s3_url: &str, region: &str) -> Result<String> {
let bad = |detail: String| Error::BadUrl {
url: s3_url.to_string(),
detail,
};
let rest = s3_url
.strip_prefix("s3://")
.ok_or_else(|| bad("not an s3:// URL".to_string()))?;
let (bucket, key) = rest
.split_once('/')
.ok_or_else(|| bad("s3:// URL has no object key".to_string()))?;
if bucket.is_empty() {
return Err(bad("s3:// URL has an empty bucket".to_string()));
}
Ok(format!("https://{bucket}.s3.{region}.amazonaws.com/{key}"))
}
pub struct S3Transport {
http: HttpTransport,
region: Option<String>,
}
impl S3Transport {
pub fn new() -> Self {
Self {
http: HttpTransport::new(),
region: None,
}
}
pub fn with_region(region: impl Into<String>) -> Self {
Self {
http: HttpTransport::new(),
region: Some(region.into()),
}
}
}
impl Default for S3Transport {
fn default() -> Self {
Self::new()
}
}
impl Transport for S3Transport {
fn schemes(&self) -> &'static [&'static str] {
&["s3"]
}
fn fetch(
&self,
url: &str,
dest: &Path,
conditional: &Conditional,
auth: Option<&dyn AuthResolver>,
) -> Result<FetchResult> {
let region = resolve_region(self.region.as_deref());
let https = s3_https_url(url, ®ion)?;
self.http.fetch(&https, dest, conditional, auth)
}
fn fetch_hashed(
&self,
url: &str,
dest: &Path,
conditional: &Conditional,
auth: Option<&dyn AuthResolver>,
) -> Result<(FetchResult, Option<String>)> {
let region = resolve_region(self.region.as_deref());
let https = s3_https_url(url, ®ion)?;
self.http.fetch_hashed(&https, dest, conditional, auth)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scheme_is_s3() {
assert_eq!(S3Transport::new().schemes(), &["s3"]);
}
#[test]
fn rewrite_default_and_explicit_region() {
assert_eq!(
s3_https_url(
"s3://inmap-model/isrm_v1.2.1.zarr/PrimaryPM25/0.5.0",
"us-east-2"
)
.unwrap(),
"https://inmap-model.s3.us-east-2.amazonaws.com/isrm_v1.2.1.zarr/PrimaryPM25/0.5.0"
);
assert_eq!(
s3_https_url("s3://b/k/o", "eu-west-1").unwrap(),
"https://b.s3.eu-west-1.amazonaws.com/k/o"
);
}
#[test]
fn rewrite_rejects_bad_urls() {
assert!(s3_https_url("https://x/y", "us-east-2").is_err());
assert!(s3_https_url("s3://bucket-only", "us-east-2").is_err());
assert!(s3_https_url("s3:///key", "us-east-2").is_err());
}
}