use std::sync::Arc;
use datafusion::error::{DataFusionError, Result};
use datafusion::execution::runtime_env::RuntimeEnv;
#[cfg(not(target_arch = "wasm32"))]
use object_store::aws::{resolve_bucket_region, AmazonS3Builder};
#[cfg(not(target_arch = "wasm32"))]
use object_store::azure::MicrosoftAzureBuilder;
#[cfg(not(target_arch = "wasm32"))]
use object_store::gcp::GoogleCloudStorageBuilder;
use object_store::http::HttpBuilder;
use object_store::{ClientOptions, ObjectStore};
use url::{Position, Url};
pub async fn ensure_object_store(runtime: &RuntimeEnv, url: &Url) -> Result<()> {
if url.scheme() == "file" || runtime.object_store_registry.get_store(url).is_ok() {
return Ok(());
}
let store = build_object_store(url).await?;
runtime.register_object_store(&store_base(url)?, store);
Ok(())
}
pub async fn ensure_object_store_for(runtime: &RuntimeEnv, location: &str) -> Result<()> {
match Url::parse(location) {
Ok(url) if url.scheme().len() > 1 => ensure_object_store(runtime, &url).await,
_ => Ok(()),
}
}
fn store_base(url: &Url) -> Result<Url> {
Url::parse(&url[..Position::BeforePath])
.map_err(|e| DataFusionError::Plan(format!("'{url}' is not a valid URL: {e}")))
}
pub async fn build_object_store(url: &Url) -> Result<Arc<dyn ObjectStore>> {
let base = &url[..Position::BeforePath];
let failed = |e: object_store::Error| {
DataFusionError::Execution(format!("cannot connect to {base}: {e}"))
};
let store: Arc<dyn ObjectStore> = match url.scheme() {
#[cfg(not(target_arch = "wasm32"))]
"s3" | "s3a" => {
let mut builder = AmazonS3Builder::from_env().with_url(url.as_str());
#[allow(unused_mut)]
let mut profile_region: Option<String> = None;
#[cfg(feature = "aws-profile")]
if let Some(profile) = aws_profile::load()
.await
.map_err(|e| DataFusionError::Execution(format!("cannot connect to {base}: {e}")))?
{
if let Some(credentials) = profile.credentials {
builder = builder.with_credentials(credentials);
}
profile_region = profile.region;
}
if !env_set(&["AWS_REGION", "AWS_DEFAULT_REGION"]) {
let mut region = None;
if !env_set(&["AWS_ENDPOINT", "AWS_ENDPOINT_URL"]) {
if let Some(bucket) = url.host_str() {
region = resolve_bucket_region(bucket, &ClientOptions::new())
.await
.ok();
}
}
if let Some(region) = region.or(profile_region) {
builder = builder.with_region(region);
}
}
Arc::new(builder.build().map_err(failed)?)
}
#[cfg(not(target_arch = "wasm32"))]
"gs" | "gcs" => Arc::new(
GoogleCloudStorageBuilder::from_env()
.with_url(url.as_str())
.build()
.map_err(failed)?,
),
#[cfg(not(target_arch = "wasm32"))]
"az" | "azure" | "abfs" | "abfss" | "adl" => Arc::new(
MicrosoftAzureBuilder::from_env()
.with_url(url.as_str())
.build()
.map_err(failed)?,
),
"http" | "https" => Arc::new(
HttpBuilder::new()
.with_url(base)
.with_client_options(ClientOptions::new().with_allow_http(true))
.build()
.map_err(failed)?,
),
#[cfg(target_arch = "wasm32")]
other @ ("s3" | "s3a" | "gs" | "gcs" | "az" | "azure" | "abfs" | "abfss" | "adl") => {
return Err(DataFusionError::Plan(format!(
"'{other}://' URLs are not available in the browser build; use an http:// or \
https:// URL"
)))
}
other => {
return Err(DataFusionError::Plan(format!(
"Ursa cannot read '{other}://' URLs; use a local path or an s3://, gs://, az://, \
http:// or https:// URL"
)))
}
};
Ok(store)
}
#[cfg(not(target_arch = "wasm32"))]
fn env_set(keys: &[&str]) -> bool {
keys.iter()
.any(|k| std::env::var_os(k).is_some_and(|v| !v.is_empty()))
}
#[cfg(feature = "aws-profile")]
pub mod aws_profile {
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use aws_config::meta::region::ProvideRegion;
use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
use aws_config::provider_config::ProviderConfig;
use aws_credential_types::provider::error::CredentialsError;
use aws_credential_types::provider::ProvideCredentials;
use aws_credential_types::Credentials;
use aws_smithy_http_client::tls::{self, rustls_provider::CryptoMode};
use object_store::aws::AwsCredential;
use object_store::CredentialProvider;
use tokio::sync::Mutex;
pub struct Profile {
pub credentials: Option<Arc<ProfileCredentials>>,
pub region: Option<String>,
}
pub async fn load() -> Result<Option<Profile>, String> {
let skip_signature = std::env::var("AWS_SKIP_SIGNATURE")
.is_ok_and(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1"));
if super::env_set(&["AWS_ACCESS_KEY_ID"]) || skip_signature {
return Ok(None);
}
let named = std::env::var("AWS_PROFILE").ok().filter(|p| !p.is_empty());
let name = named.clone().unwrap_or_else(|| "default".to_string());
let http = aws_smithy_http_client::Builder::new()
.tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
.build_https();
let config = ProviderConfig::without_region().with_http_client(http);
let region = ProfileFileRegionProvider::builder()
.profile_name(&name)
.configure(&config)
.build()
.region()
.await;
let config = config.with_region(region.clone());
let provider = ProfileFileCredentialsProvider::builder()
.profile_name(&name)
.configure(&config)
.build();
let region = region.map(|r| r.to_string());
match provider.provide_credentials().await {
Ok(first) => Ok(Some(Profile {
credentials: Some(Arc::new(ProfileCredentials {
provider,
current: Mutex::new(first),
})),
region,
})),
Err(CredentialsError::CredentialsNotLoaded(_)) if named.is_none() => {
Ok(Some(Profile {
credentials: None,
region,
}))
}
Err(e) => Err(format!(
"AWS profile '{name}' gives no credentials: {}",
error_chain(&e)
)),
}
}
fn error_chain(e: &(dyn std::error::Error + 'static)) -> String {
let mut parts = vec![e.to_string()];
let mut source = e.source();
while let Some(s) = source {
let text = s.to_string();
if !parts.iter().any(|p| p.contains(&text)) {
parts.push(text);
}
source = s.source();
}
parts.join(": ")
}
#[derive(Debug)]
pub struct ProfileCredentials {
provider: ProfileFileCredentialsProvider,
current: Mutex<Credentials>,
}
const REFRESH_BEFORE: Duration = Duration::from_secs(5 * 60);
#[async_trait]
impl CredentialProvider for ProfileCredentials {
type Credential = AwsCredential;
async fn get_credential(&self) -> object_store::Result<Arc<AwsCredential>> {
let mut current = self.current.lock().await;
let stale = current
.expiry()
.is_some_and(|at| at <= SystemTime::now() + REFRESH_BEFORE);
if stale {
*current = self.provider.provide_credentials().await.map_err(|e| {
object_store::Error::Generic {
store: "S3",
source: error_chain(&e).into(),
}
})?;
}
Ok(Arc::new(AwsCredential {
key_id: current.access_key_id().to_string(),
secret_key: current.secret_access_key().to_string(),
token: current.session_token().map(str::to_string),
}))
}
}
}