1use crate::ObjectStore;
19#[cfg(all(feature = "fs", not(target_arch = "wasm32")))]
20use crate::local::LocalFileSystem;
21use crate::memory::InMemory;
22use crate::path::Path;
23use url::Url;
24
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27 #[error("Unable to recognise URL \"{}\"", url)]
28 Unrecognised { url: Url },
29
30 #[error(transparent)]
31 Path {
32 #[from]
33 source: crate::path::Error,
34 },
35}
36
37impl From<Error> for super::Error {
38 fn from(e: Error) -> Self {
39 Self::Generic {
40 store: "URL",
41 source: Box::new(e),
42 }
43 }
44}
45
46#[non_exhaustive] #[derive(Debug, Eq, PartialEq, Clone)]
66pub enum ObjectStoreScheme {
67 Local,
69 Memory,
71 AmazonS3,
73 GoogleCloudStorage,
75 MicrosoftAzure,
77 Http,
79}
80
81impl ObjectStoreScheme {
82 pub fn parse(url: &Url) -> Result<(Self, Path), Error> {
106 let strip_bucket = || Some(url.path().strip_prefix('/')?.split_once('/')?.1);
107
108 let (scheme, path) = match (url.scheme(), url.host_str()) {
109 ("file", None) => (Self::Local, url.path()),
110 ("memory", None) => (Self::Memory, url.path()),
111 ("s3" | "s3a", Some(_)) => (Self::AmazonS3, url.path()),
112 ("gs", Some(_)) => (Self::GoogleCloudStorage, url.path()),
113 ("az" | "adl" | "azure" | "abfs" | "abfss", Some(_)) => {
114 (Self::MicrosoftAzure, url.path())
115 }
116 ("http", Some(_)) => (Self::Http, url.path()),
117 ("https", Some(host)) => {
118 if host.ends_with("dfs.core.windows.net")
119 || host.ends_with("blob.core.windows.net")
120 || host.ends_with("dfs.fabric.microsoft.com")
121 || host.ends_with("blob.fabric.microsoft.com")
122 {
123 (Self::MicrosoftAzure, strip_bucket().unwrap_or_default())
124 } else if host.ends_with("amazonaws.com") {
125 match host.starts_with("s3") {
126 true => (Self::AmazonS3, strip_bucket().unwrap_or_default()),
127 false => (Self::AmazonS3, url.path()),
128 }
129 } else if host.ends_with("r2.cloudflarestorage.com") {
130 (Self::AmazonS3, strip_bucket().unwrap_or_default())
131 } else {
132 (Self::Http, url.path())
133 }
134 }
135 _ => return Err(Error::Unrecognised { url: url.clone() }),
136 };
137
138 Ok((scheme, Path::from_url_path(path)?))
139 }
140}
141
142#[cfg(feature = "cloud-base")]
143macro_rules! builder_opts {
144 ($builder:ty, $url:expr, $options:expr) => {{
145 let builder = $options.into_iter().fold(
146 <$builder>::new().with_url($url.to_string()),
147 |builder, (key, value)| match key.as_ref().to_ascii_lowercase().parse() {
148 Ok(k) => builder.with_config(k, value),
149 Err(_) => builder,
150 },
151 );
152 Box::new(builder.build()?) as _
153 }};
154}
155
156pub fn parse_url(url: &Url) -> Result<(Box<dyn ObjectStore>, Path), super::Error> {
162 parse_url_opts(url, std::iter::empty::<(&str, &str)>())
163}
164
165pub fn parse_url_opts<I, K, V>(
188 url: &Url,
189 options: I,
190) -> Result<(Box<dyn ObjectStore>, Path), super::Error>
191where
192 I: IntoIterator<Item = (K, V)>,
193 K: AsRef<str>,
194 V: Into<String>,
195{
196 let _options = options;
197 let (scheme, path) = ObjectStoreScheme::parse(url)?;
198 let path = Path::parse(path)?;
199
200 let store = match scheme {
201 #[cfg(all(feature = "fs", not(target_arch = "wasm32")))]
202 ObjectStoreScheme::Local => Box::new(LocalFileSystem::new()) as _,
203 ObjectStoreScheme::Memory => Box::new(InMemory::new()) as _,
204 #[cfg(feature = "aws-base")]
205 ObjectStoreScheme::AmazonS3 => {
206 builder_opts!(crate::aws::AmazonS3Builder, url, _options)
207 }
208 #[cfg(feature = "gcp-base")]
209 ObjectStoreScheme::GoogleCloudStorage => {
210 builder_opts!(crate::gcp::GoogleCloudStorageBuilder, url, _options)
211 }
212 #[cfg(feature = "azure-base")]
213 ObjectStoreScheme::MicrosoftAzure => {
214 builder_opts!(crate::azure::MicrosoftAzureBuilder, url, _options)
215 }
216 #[cfg(feature = "http-base")]
217 ObjectStoreScheme::Http => {
218 let url = &url[..url::Position::BeforePath];
219 builder_opts!(crate::http::HttpBuilder, url, _options)
220 }
221 #[cfg(not(all(
222 feature = "fs",
223 feature = "aws-base",
224 feature = "azure-base",
225 feature = "gcp-base",
226 feature = "http-base",
227 not(target_arch = "wasm32")
228 )))]
229 s => {
230 return Err(super::Error::Generic {
231 store: "parse_url",
232 source: format!("feature for {s:?} not enabled").into(),
233 });
234 }
235 };
236
237 Ok((store, path))
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use url::Url;
244
245 #[test]
246 fn test_parse() {
247 let cases = [
248 ("file:/path", (ObjectStoreScheme::Local, "path")),
249 ("file:///path", (ObjectStoreScheme::Local, "path")),
250 ("memory:/path", (ObjectStoreScheme::Memory, "path")),
251 ("memory:///", (ObjectStoreScheme::Memory, "")),
252 ("s3://bucket/path", (ObjectStoreScheme::AmazonS3, "path")),
253 ("s3a://bucket/path", (ObjectStoreScheme::AmazonS3, "path")),
254 (
255 "https://s3.region.amazonaws.com/bucket",
256 (ObjectStoreScheme::AmazonS3, ""),
257 ),
258 (
259 "https://s3.region.amazonaws.com/bucket/path",
260 (ObjectStoreScheme::AmazonS3, "path"),
261 ),
262 (
263 "https://bucket.s3.region.amazonaws.com",
264 (ObjectStoreScheme::AmazonS3, ""),
265 ),
266 (
267 "https://s3.dualstack.us-east-1.amazonaws.com/bucket/path",
268 (ObjectStoreScheme::AmazonS3, "path"),
269 ),
270 (
271 "https://bucket.s3.dualstack.us-east-1.amazonaws.com/path",
272 (ObjectStoreScheme::AmazonS3, "path"),
273 ),
274 (
275 "https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket",
276 (ObjectStoreScheme::AmazonS3, ""),
277 ),
278 (
279 "https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket/path",
280 (ObjectStoreScheme::AmazonS3, "path"),
281 ),
282 (
283 "abfs://container/path",
284 (ObjectStoreScheme::MicrosoftAzure, "path"),
285 ),
286 (
287 "abfs://file_system@account_name.dfs.core.windows.net/path",
288 (ObjectStoreScheme::MicrosoftAzure, "path"),
289 ),
290 (
291 "abfss://file_system@account_name.dfs.core.windows.net/path",
292 (ObjectStoreScheme::MicrosoftAzure, "path"),
293 ),
294 (
295 "https://account.dfs.core.windows.net",
296 (ObjectStoreScheme::MicrosoftAzure, ""),
297 ),
298 (
299 "https://account.dfs.core.windows.net/container/path",
300 (ObjectStoreScheme::MicrosoftAzure, "path"),
301 ),
302 (
303 "https://account.blob.core.windows.net",
304 (ObjectStoreScheme::MicrosoftAzure, ""),
305 ),
306 (
307 "https://account.blob.core.windows.net/container/path",
308 (ObjectStoreScheme::MicrosoftAzure, "path"),
309 ),
310 (
311 "az://container/path",
312 (ObjectStoreScheme::MicrosoftAzure, "path"),
313 ),
314 (
315 "az://container@account/path",
316 (ObjectStoreScheme::MicrosoftAzure, "path"),
317 ),
318 (
319 "abfs://container/path",
320 (ObjectStoreScheme::MicrosoftAzure, "path"),
321 ),
322 (
323 "abfs://container@account/path",
324 (ObjectStoreScheme::MicrosoftAzure, "path"),
325 ),
326 (
327 "abfss://container/path",
328 (ObjectStoreScheme::MicrosoftAzure, "path"),
329 ),
330 (
331 "abfss://container@account/path",
332 (ObjectStoreScheme::MicrosoftAzure, "path"),
333 ),
334 (
335 "adl://container/path",
336 (ObjectStoreScheme::MicrosoftAzure, "path"),
337 ),
338 (
339 "adl://container@account/path",
340 (ObjectStoreScheme::MicrosoftAzure, "path"),
341 ),
342 (
343 "gs://bucket/path",
344 (ObjectStoreScheme::GoogleCloudStorage, "path"),
345 ),
346 (
347 "gs://test.example.com/path",
348 (ObjectStoreScheme::GoogleCloudStorage, "path"),
349 ),
350 ("http://mydomain/path", (ObjectStoreScheme::Http, "path")),
351 ("https://mydomain/path", (ObjectStoreScheme::Http, "path")),
352 (
353 "s3://bucket/foo%20bar",
354 (ObjectStoreScheme::AmazonS3, "foo bar"),
355 ),
356 (
357 "s3://bucket/foo bar",
358 (ObjectStoreScheme::AmazonS3, "foo bar"),
359 ),
360 ("s3://bucket/😀", (ObjectStoreScheme::AmazonS3, "😀")),
361 (
362 "s3://bucket/%F0%9F%98%80",
363 (ObjectStoreScheme::AmazonS3, "😀"),
364 ),
365 (
366 "https://foo/bar%20baz",
367 (ObjectStoreScheme::Http, "bar baz"),
368 ),
369 (
370 "file:///bar%252Efoo",
371 (ObjectStoreScheme::Local, "bar%2Efoo"),
372 ),
373 (
374 "abfss://file_system@account.dfs.fabric.microsoft.com/",
375 (ObjectStoreScheme::MicrosoftAzure, ""),
376 ),
377 (
378 "abfss://file_system@account.dfs.fabric.microsoft.com/",
379 (ObjectStoreScheme::MicrosoftAzure, ""),
380 ),
381 (
382 "https://account.dfs.fabric.microsoft.com/",
383 (ObjectStoreScheme::MicrosoftAzure, ""),
384 ),
385 (
386 "https://account.dfs.fabric.microsoft.com/container",
387 (ObjectStoreScheme::MicrosoftAzure, ""),
388 ),
389 (
390 "https://account.dfs.fabric.microsoft.com/container/path",
391 (ObjectStoreScheme::MicrosoftAzure, "path"),
392 ),
393 (
394 "https://account.blob.fabric.microsoft.com/",
395 (ObjectStoreScheme::MicrosoftAzure, ""),
396 ),
397 (
398 "https://account.blob.fabric.microsoft.com/container",
399 (ObjectStoreScheme::MicrosoftAzure, ""),
400 ),
401 (
402 "https://account.blob.fabric.microsoft.com/container/path",
403 (ObjectStoreScheme::MicrosoftAzure, "path"),
404 ),
405 ];
406
407 for (s, (expected_scheme, expected_path)) in cases {
408 let url = Url::parse(s).unwrap();
409 let (scheme, path) = ObjectStoreScheme::parse(&url).unwrap();
410
411 assert_eq!(scheme, expected_scheme, "{s}");
412 assert_eq!(path, Path::parse(expected_path).unwrap(), "{s}");
413 }
414
415 let neg_cases = [
416 "unix:/run/foo.socket",
417 "file://remote/path",
418 "memory://remote/",
419 ];
420 for s in neg_cases {
421 let url = Url::parse(s).unwrap();
422 assert!(ObjectStoreScheme::parse(&url).is_err());
423 }
424 }
425
426 #[test]
427 fn test_url_spaces() {
428 let url = Url::parse("file:///my file with spaces").unwrap();
429 assert_eq!(url.path(), "/my%20file%20with%20spaces");
430 let (_, path) = parse_url(&url).unwrap();
431 assert_eq!(path.as_ref(), "my file with spaces");
432 }
433
434 #[test]
435 #[cfg(feature = "gcp")]
436 fn test_url_gcs_bearer_token_opts() {
437 let url = Url::parse("gs://bucket/path").unwrap();
438
439 for alias in ["google_bearer_token", "bearer_token"] {
440 let opts = [
441 (alias, "test-token"),
442 ("google_proxy_url", "https://example.com"),
443 ];
444
445 let (store, path) = parse_url_opts(&url, opts).unwrap();
446 assert_eq!(path.as_ref(), "path");
447 assert_eq!(store.to_string(), "GoogleCloudStorage(bucket)");
448 }
449 }
450
451 #[tokio::test]
452 #[cfg(all(
453 feature = "reqwest",
454 feature = "http-base",
455 not(target_arch = "wasm32")
456 ))]
457 async fn test_url_http() {
458 use crate::{ObjectStoreExt, client::mock_server::MockServer};
459 use http::{Response, header::USER_AGENT};
460
461 let server = MockServer::new().await;
462
463 server.push_fn(|r| {
464 assert_eq!(r.uri().path(), "/foo/bar");
465 assert_eq!(r.headers().get(USER_AGENT).unwrap(), "test_url");
466 Response::new(String::from("result"))
467 });
468
469 let test = format!("{}/foo/bar", server.url());
470 let opts = [("USER_AGENT", "test_url"), ("allow_http", "true")];
471 let url = test.parse().unwrap();
472 let (store, path) = parse_url_opts(&url, opts).unwrap();
473 assert_eq!(path.as_ref(), "foo/bar");
474
475 let res = store.get(&path).await.unwrap();
476 let body = res.bytes().await.unwrap();
477 let body = std::str::from_utf8(&body).unwrap();
478 assert_eq!(body, "result");
479
480 server.shutdown().await;
481 }
482}