1use std::sync::Arc;
25
26use datafusion::error::{DataFusionError, Result};
27use datafusion::execution::runtime_env::RuntimeEnv;
28#[cfg(not(target_arch = "wasm32"))]
29use object_store::aws::{resolve_bucket_region, AmazonS3Builder};
30#[cfg(not(target_arch = "wasm32"))]
31use object_store::azure::MicrosoftAzureBuilder;
32#[cfg(not(target_arch = "wasm32"))]
33use object_store::gcp::GoogleCloudStorageBuilder;
34use object_store::http::HttpBuilder;
35use object_store::{ClientOptions, ObjectStore};
36use url::{Position, Url};
37
38pub async fn ensure_object_store(runtime: &RuntimeEnv, url: &Url) -> Result<()> {
42 if url.scheme() == "file" || runtime.object_store_registry.get_store(url).is_ok() {
43 return Ok(());
44 }
45 let store = build_object_store(url).await?;
46 runtime.register_object_store(&store_base(url)?, store);
47 Ok(())
48}
49
50pub async fn ensure_object_store_for(runtime: &RuntimeEnv, location: &str) -> Result<()> {
53 match Url::parse(location) {
54 Ok(url) if url.scheme().len() > 1 => ensure_object_store(runtime, &url).await,
56 _ => Ok(()),
57 }
58}
59
60fn store_base(url: &Url) -> Result<Url> {
62 Url::parse(&url[..Position::BeforePath])
63 .map_err(|e| DataFusionError::Plan(format!("'{url}' is not a valid URL: {e}")))
64}
65
66pub async fn build_object_store(url: &Url) -> Result<Arc<dyn ObjectStore>> {
68 let base = &url[..Position::BeforePath];
69 let failed = |e: object_store::Error| {
70 DataFusionError::Execution(format!("cannot connect to {base}: {e}"))
71 };
72 let store: Arc<dyn ObjectStore> = match url.scheme() {
73 #[cfg(not(target_arch = "wasm32"))]
74 "s3" | "s3a" => {
75 let mut builder = AmazonS3Builder::from_env().with_url(url.as_str());
76 #[allow(unused_mut)]
79 let mut profile_region: Option<String> = None;
80 #[cfg(feature = "aws-profile")]
81 if let Some(profile) = aws_profile::load()
82 .await
83 .map_err(|e| DataFusionError::Execution(format!("cannot connect to {base}: {e}")))?
84 {
85 if let Some(credentials) = profile.credentials {
86 builder = builder.with_credentials(credentials);
87 }
88 profile_region = profile.region;
89 }
90 if !env_set(&["AWS_REGION", "AWS_DEFAULT_REGION"]) {
91 let mut region = None;
96 if !env_set(&["AWS_ENDPOINT", "AWS_ENDPOINT_URL"]) {
97 if let Some(bucket) = url.host_str() {
98 region = resolve_bucket_region(bucket, &ClientOptions::new())
99 .await
100 .ok();
101 }
102 }
103 if let Some(region) = region.or(profile_region) {
104 builder = builder.with_region(region);
105 }
106 }
107 Arc::new(builder.build().map_err(failed)?)
108 }
109 #[cfg(not(target_arch = "wasm32"))]
110 "gs" | "gcs" => Arc::new(
111 GoogleCloudStorageBuilder::from_env()
112 .with_url(url.as_str())
113 .build()
114 .map_err(failed)?,
115 ),
116 #[cfg(not(target_arch = "wasm32"))]
117 "az" | "azure" | "abfs" | "abfss" | "adl" => Arc::new(
118 MicrosoftAzureBuilder::from_env()
119 .with_url(url.as_str())
120 .build()
121 .map_err(failed)?,
122 ),
123 "http" | "https" => Arc::new(
126 HttpBuilder::new()
127 .with_url(base)
128 .with_client_options(ClientOptions::new().with_allow_http(true))
129 .build()
130 .map_err(failed)?,
131 ),
132 #[cfg(target_arch = "wasm32")]
133 other @ ("s3" | "s3a" | "gs" | "gcs" | "az" | "azure" | "abfs" | "abfss" | "adl") => {
134 return Err(DataFusionError::Plan(format!(
135 "'{other}://' URLs are not available in the browser build; use an http:// or \
136 https:// URL"
137 )))
138 }
139 other => {
140 return Err(DataFusionError::Plan(format!(
141 "Ursa cannot read '{other}://' URLs; use a local path or an s3://, gs://, az://, \
142 http:// or https:// URL"
143 )))
144 }
145 };
146 Ok(store)
147}
148
149#[cfg(not(target_arch = "wasm32"))]
151fn env_set(keys: &[&str]) -> bool {
152 keys.iter()
153 .any(|k| std::env::var_os(k).is_some_and(|v| !v.is_empty()))
154}
155
156#[cfg(feature = "aws-profile")]
168pub mod aws_profile {
169 use std::sync::Arc;
170 use std::time::{Duration, SystemTime};
171
172 use async_trait::async_trait;
173 use aws_config::meta::region::ProvideRegion;
174 use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
175 use aws_config::provider_config::ProviderConfig;
176 use aws_credential_types::provider::error::CredentialsError;
177 use aws_credential_types::provider::ProvideCredentials;
178 use aws_credential_types::Credentials;
179 use aws_smithy_http_client::tls::{self, rustls_provider::CryptoMode};
180 use object_store::aws::AwsCredential;
181 use object_store::CredentialProvider;
182 use tokio::sync::Mutex;
183
184 pub struct Profile {
186 pub credentials: Option<Arc<ProfileCredentials>>,
188 pub region: Option<String>,
189 }
190
191 pub async fn load() -> Result<Option<Profile>, String> {
195 let skip_signature = std::env::var("AWS_SKIP_SIGNATURE")
196 .is_ok_and(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1"));
197 if super::env_set(&["AWS_ACCESS_KEY_ID"]) || skip_signature {
198 return Ok(None);
199 }
200 let named = std::env::var("AWS_PROFILE").ok().filter(|p| !p.is_empty());
201 let name = named.clone().unwrap_or_else(|| "default".to_string());
202 let http = aws_smithy_http_client::Builder::new()
205 .tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
206 .build_https();
207 let config = ProviderConfig::without_region().with_http_client(http);
208 let region = ProfileFileRegionProvider::builder()
209 .profile_name(&name)
210 .configure(&config)
211 .build()
212 .region()
213 .await;
214 let config = config.with_region(region.clone());
215 let provider = ProfileFileCredentialsProvider::builder()
216 .profile_name(&name)
217 .configure(&config)
218 .build();
219 let region = region.map(|r| r.to_string());
220 match provider.provide_credentials().await {
221 Ok(first) => Ok(Some(Profile {
222 credentials: Some(Arc::new(ProfileCredentials {
223 provider,
224 current: Mutex::new(first),
225 })),
226 region,
227 })),
228 Err(CredentialsError::CredentialsNotLoaded(_)) if named.is_none() => {
229 Ok(Some(Profile {
230 credentials: None,
231 region,
232 }))
233 }
234 Err(e) => Err(format!(
235 "AWS profile '{name}' gives no credentials: {}",
236 error_chain(&e)
237 )),
238 }
239 }
240
241 fn error_chain(e: &(dyn std::error::Error + 'static)) -> String {
243 let mut parts = vec![e.to_string()];
244 let mut source = e.source();
245 while let Some(s) = source {
246 let text = s.to_string();
247 if !parts.iter().any(|p| p.contains(&text)) {
248 parts.push(text);
249 }
250 source = s.source();
251 }
252 parts.join(": ")
253 }
254
255 #[derive(Debug)]
258 pub struct ProfileCredentials {
259 provider: ProfileFileCredentialsProvider,
260 current: Mutex<Credentials>,
261 }
262
263 const REFRESH_BEFORE: Duration = Duration::from_secs(5 * 60);
265
266 #[async_trait]
267 impl CredentialProvider for ProfileCredentials {
268 type Credential = AwsCredential;
269
270 async fn get_credential(&self) -> object_store::Result<Arc<AwsCredential>> {
271 let mut current = self.current.lock().await;
272 let stale = current
273 .expiry()
274 .is_some_and(|at| at <= SystemTime::now() + REFRESH_BEFORE);
275 if stale {
276 *current = self.provider.provide_credentials().await.map_err(|e| {
277 object_store::Error::Generic {
278 store: "S3",
279 source: error_chain(&e).into(),
280 }
281 })?;
282 }
283 Ok(Arc::new(AwsCredential {
284 key_id: current.access_key_id().to_string(),
285 secret_key: current.secret_access_key().to_string(),
286 token: current.session_token().map(str::to_string),
287 }))
288 }
289 }
290}