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(feature = "async")]
291 pub async fn get(&self) -> Result<bytes::Bytes> {
292 use backends::CloudStorageBackend;
293
294 match self {
295 #[cfg(feature = "s3")]
296 Self::S3 { backend, key } => backend.get(key).await,
297
298 #[cfg(feature = "azure-blob")]
299 Self::Azure { backend, blob } => backend.get(blob).await,
300
301 #[cfg(feature = "gcs")]
302 Self::Gcs { backend, object } => backend.get(object).await,
303
304 #[cfg(feature = "http")]
305 Self::Http { backend, path } => backend.get(path).await,
306 }
307 }
308
309 #[cfg(feature = "async")]
311 pub async fn put(&self, data: &[u8]) -> Result<()> {
312 use backends::CloudStorageBackend;
313
314 match self {
315 #[cfg(feature = "s3")]
316 Self::S3 { backend, key } => backend.put(key, data).await,
317
318 #[cfg(feature = "azure-blob")]
319 Self::Azure { backend, blob } => backend.put(blob, data).await,
320
321 #[cfg(feature = "gcs")]
322 Self::Gcs { backend, object } => backend.put(object, data).await,
323
324 #[cfg(feature = "http")]
325 Self::Http { .. } => Err(CloudError::NotSupported {
326 operation: "HTTP backend is read-only".to_string(),
327 }),
328 }
329 }
330
331 #[cfg(feature = "async")]
333 pub async fn exists(&self) -> Result<bool> {
334 use backends::CloudStorageBackend;
335
336 match self {
337 #[cfg(feature = "s3")]
338 Self::S3 { backend, key } => backend.exists(key).await,
339
340 #[cfg(feature = "azure-blob")]
341 Self::Azure { backend, blob } => backend.exists(blob).await,
342
343 #[cfg(feature = "gcs")]
344 Self::Gcs { backend, object } => backend.exists(object).await,
345
346 #[cfg(feature = "http")]
347 Self::Http { backend, path } => backend.exists(path).await,
348 }
349 }
350}
351
352#[cfg(test)]
353#[allow(clippy::panic)]
354mod tests {
355 use super::*;
356
357 #[test]
358 #[cfg(feature = "s3")]
359 fn test_cloud_backend_from_url_s3() {
360 let backend = CloudBackend::from_url("s3://my-bucket/path/to/file.tif");
361 assert!(backend.is_ok());
362
363 if let Ok(CloudBackend::S3 { backend, key }) = backend {
364 assert_eq!(backend.bucket, "my-bucket");
365 assert_eq!(key, "path/to/file.tif");
366 } else {
367 panic!("Expected S3 backend");
368 }
369 }
370
371 #[test]
372 #[cfg(feature = "gcs")]
373 fn test_cloud_backend_from_url_gcs() {
374 let backend = CloudBackend::from_url("gs://my-bucket/path/to/file.tif");
375 assert!(backend.is_ok());
376
377 if let Ok(CloudBackend::Gcs { backend, object }) = backend {
378 assert_eq!(backend.bucket, "my-bucket");
379 assert_eq!(object, "path/to/file.tif");
380 } else {
381 panic!("Expected GCS backend");
382 }
383 }
384
385 #[test]
386 #[cfg(feature = "http")]
387 fn test_cloud_backend_from_url_http() {
388 let backend = CloudBackend::from_url("https://example.com/path/to/file.tif");
389 assert!(backend.is_ok());
390
391 if let Ok(CloudBackend::Http { backend, path }) = backend {
392 assert!(backend.base_url.contains("example.com"));
393 assert_eq!(path, "path/to/file.tif");
394 } else {
395 panic!("Expected HTTP backend");
396 }
397 }
398
399 #[test]
400 fn test_cloud_backend_from_url_invalid() {
401 let backend = CloudBackend::from_url("invalid://url");
402 assert!(backend.is_err());
403 }
404}