1#![cfg_attr(not(feature = "std"), no_std)]
84#![allow(missing_docs)]
86#![allow(dead_code)]
88#![allow(clippy::match_like_matches_macro)]
90#![allow(clippy::expect_used)]
92#![allow(clippy::type_complexity)]
94#![allow(clippy::manual_div_ceil)]
96#![allow(unused_variables)]
98#![allow(clippy::collapsible_match)]
100#![allow(async_fn_in_trait)]
102#![allow(clippy::manual_strip)]
104#![allow(clippy::get_first)]
106#![allow(clippy::field_reassign_with_default)]
108#![allow(unused_imports)]
110#![allow(clippy::should_implement_trait)]
112
113#[cfg(feature = "alloc")]
114extern crate alloc;
115
116pub mod auth;
117pub mod backends;
118#[cfg(feature = "cache")]
119pub mod cache;
120pub mod error;
121#[cfg(feature = "async")]
122pub mod multicloud;
123#[cfg(feature = "prefetch")]
124pub mod prefetch;
125#[cfg(feature = "retry")]
126pub mod retry;
127
128pub use error::{CloudError, Result};
129
130#[cfg(feature = "s3")]
131pub use backends::s3::S3Backend;
132
133#[cfg(feature = "azure-blob")]
134pub use backends::azure::AzureBlobBackend;
135
136#[cfg(feature = "gcs")]
137pub use backends::gcs::GcsBackend;
138
139#[cfg(feature = "http")]
140pub use backends::http::HttpBackend;
141
142#[cfg(feature = "async")]
143pub use multicloud::{
144 CloudProvider, CloudProviderConfig, CloudRegion, CrossCloudTransferConfig,
145 CrossCloudTransferResult, MultiCloudManager, MultiCloudManagerBuilder, ProviderHealth,
146 RoutingStrategy, TransferCostEstimate,
147};
148
149use url::Url;
150
151#[derive(Debug)]
153pub enum CloudBackend {
154 #[cfg(feature = "s3")]
156 S3 {
157 backend: S3Backend,
159 key: String,
161 },
162
163 #[cfg(feature = "azure-blob")]
165 Azure {
166 backend: AzureBlobBackend,
168 blob: String,
170 },
171
172 #[cfg(feature = "gcs")]
174 Gcs {
175 backend: GcsBackend,
177 object: String,
179 },
180
181 #[cfg(feature = "http")]
183 Http {
184 backend: HttpBackend,
186 path: String,
188 },
189}
190
191impl CloudBackend {
192 pub fn from_url(url: &str) -> Result<Self> {
211 let parsed = Url::parse(url)?;
212
213 match parsed.scheme() {
214 #[cfg(feature = "s3")]
215 "s3" => {
216 let bucket = parsed.host_str().ok_or_else(|| CloudError::InvalidUrl {
217 url: url.to_string(),
218 })?;
219
220 let key = parsed.path().trim_start_matches('/').to_string();
221
222 Ok(Self::S3 {
223 backend: S3Backend::new(bucket, ""),
224 key,
225 })
226 }
227
228 #[cfg(feature = "azure-blob")]
229 "az" | "azure" => {
230 let container = parsed.host_str().ok_or_else(|| CloudError::InvalidUrl {
231 url: url.to_string(),
232 })?;
233
234 let account = parsed.username();
236 if account.is_empty() {
237 return Err(CloudError::InvalidUrl {
238 url: url.to_string(),
239 });
240 }
241
242 let blob = parsed.path().trim_start_matches('/').to_string();
243
244 Ok(Self::Azure {
245 backend: AzureBlobBackend::new(account, container),
246 blob,
247 })
248 }
249
250 #[cfg(feature = "gcs")]
251 "gs" | "gcs" => {
252 let bucket = parsed.host_str().ok_or_else(|| CloudError::InvalidUrl {
253 url: url.to_string(),
254 })?;
255
256 let object = parsed.path().trim_start_matches('/').to_string();
257
258 Ok(Self::Gcs {
259 backend: GcsBackend::new(bucket),
260 object,
261 })
262 }
263
264 #[cfg(feature = "http")]
265 "http" | "https" => {
266 let base_url = format!(
268 "{}://{}",
269 parsed.scheme(),
270 parsed.host_str().ok_or_else(|| CloudError::InvalidUrl {
271 url: url.to_string(),
272 })?
273 );
274
275 let path = parsed.path().trim_start_matches('/').to_string();
276
277 Ok(Self::Http {
278 backend: HttpBackend::new(base_url),
279 path,
280 })
281 }
282
283 scheme => Err(CloudError::UnsupportedProtocol {
284 protocol: scheme.to_string(),
285 }),
286 }
287 }
288
289 #[cfg(all(
291 feature = "async",
292 any(
293 feature = "s3",
294 feature = "azure-blob",
295 feature = "gcs",
296 feature = "http"
297 )
298 ))]
299 pub async fn get(&self) -> Result<bytes::Bytes> {
300 use backends::CloudStorageBackend;
301
302 match self {
303 #[cfg(feature = "s3")]
304 Self::S3 { backend, key } => backend.get(key).await,
305
306 #[cfg(feature = "azure-blob")]
307 Self::Azure { backend, blob } => backend.get(blob).await,
308
309 #[cfg(feature = "gcs")]
310 Self::Gcs { backend, object } => backend.get(object).await,
311
312 #[cfg(feature = "http")]
313 Self::Http { backend, path } => backend.get(path).await,
314 }
315 }
316
317 #[cfg(all(
319 feature = "async",
320 any(
321 feature = "s3",
322 feature = "azure-blob",
323 feature = "gcs",
324 feature = "http"
325 )
326 ))]
327 pub async fn put(&self, data: &[u8]) -> Result<()> {
328 use backends::CloudStorageBackend;
329
330 match self {
331 #[cfg(feature = "s3")]
332 Self::S3 { backend, key } => backend.put(key, data).await,
333
334 #[cfg(feature = "azure-blob")]
335 Self::Azure { backend, blob } => backend.put(blob, data).await,
336
337 #[cfg(feature = "gcs")]
338 Self::Gcs { backend, object } => backend.put(object, data).await,
339
340 #[cfg(feature = "http")]
341 Self::Http { .. } => Err(CloudError::NotSupported {
342 operation: "HTTP backend is read-only".to_string(),
343 }),
344 }
345 }
346
347 #[cfg(all(
349 feature = "async",
350 any(
351 feature = "s3",
352 feature = "azure-blob",
353 feature = "gcs",
354 feature = "http"
355 )
356 ))]
357 pub async fn exists(&self) -> Result<bool> {
358 use backends::CloudStorageBackend;
359
360 match self {
361 #[cfg(feature = "s3")]
362 Self::S3 { backend, key } => backend.exists(key).await,
363
364 #[cfg(feature = "azure-blob")]
365 Self::Azure { backend, blob } => backend.exists(blob).await,
366
367 #[cfg(feature = "gcs")]
368 Self::Gcs { backend, object } => backend.exists(object).await,
369
370 #[cfg(feature = "http")]
371 Self::Http { backend, path } => backend.exists(path).await,
372 }
373 }
374}
375
376#[cfg(test)]
377#[allow(clippy::panic)]
378mod tests {
379 use super::*;
380
381 #[test]
382 #[cfg(feature = "s3")]
383 fn test_cloud_backend_from_url_s3() {
384 let backend = CloudBackend::from_url("s3://my-bucket/path/to/file.tif");
385 assert!(backend.is_ok());
386
387 if let Ok(CloudBackend::S3 { backend, key }) = backend {
388 assert_eq!(backend.bucket, "my-bucket");
389 assert_eq!(key, "path/to/file.tif");
390 } else {
391 panic!("Expected S3 backend");
392 }
393 }
394
395 #[test]
396 #[cfg(feature = "gcs")]
397 fn test_cloud_backend_from_url_gcs() {
398 let backend = CloudBackend::from_url("gs://my-bucket/path/to/file.tif");
399 assert!(backend.is_ok());
400
401 if let Ok(CloudBackend::Gcs { backend, object }) = backend {
402 assert_eq!(backend.bucket, "my-bucket");
403 assert_eq!(object, "path/to/file.tif");
404 } else {
405 panic!("Expected GCS backend");
406 }
407 }
408
409 #[test]
410 #[cfg(feature = "http")]
411 fn test_cloud_backend_from_url_http() {
412 let backend = CloudBackend::from_url("https://example.com/path/to/file.tif");
413 assert!(backend.is_ok());
414
415 if let Ok(CloudBackend::Http { backend, path }) = backend {
416 assert!(backend.base_url.contains("example.com"));
417 assert_eq!(path, "path/to/file.tif");
418 } else {
419 panic!("Expected HTTP backend");
420 }
421 }
422
423 #[test]
424 fn test_cloud_backend_from_url_invalid() {
425 let backend = CloudBackend::from_url("invalid://url");
426 assert!(backend.is_err());
427 }
428}