1use std::collections::HashMap;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
37use std::time::{Duration, Instant};
38
39use bytes::Bytes;
40
41#[cfg(feature = "async")]
42use crate::backends::CloudStorageBackend;
43use crate::error::{CloudError, Result};
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum CloudProvider {
52 AwsS3,
54 AzureBlob,
56 Gcs,
58 Http,
60 Custom,
62}
63
64impl CloudProvider {
65 #[must_use]
67 pub fn from_url(url: &str) -> Option<Self> {
68 let lower = url.to_lowercase();
69
70 if lower.starts_with("s3://") {
72 return Some(Self::AwsS3);
73 }
74 if lower.starts_with("az://") || lower.starts_with("azure://") {
75 return Some(Self::AzureBlob);
76 }
77 if lower.starts_with("gs://") || lower.starts_with("gcs://") {
78 return Some(Self::Gcs);
79 }
80
81 if lower.starts_with("http://") || lower.starts_with("https://") {
83 if lower.contains(".s3.") || lower.contains(".amazonaws.com") {
84 return Some(Self::AwsS3);
85 }
86 if lower.contains(".blob.core.windows.net") || lower.contains(".azure.") {
87 return Some(Self::AzureBlob);
88 }
89 if lower.contains("storage.googleapis.com")
90 || lower.contains("storage.cloud.google.com")
91 {
92 return Some(Self::Gcs);
93 }
94 return Some(Self::Http);
95 }
96
97 None
98 }
99
100 #[must_use]
102 pub fn display_name(&self) -> &'static str {
103 match self {
104 Self::AwsS3 => "AWS S3",
105 Self::AzureBlob => "Azure Blob Storage",
106 Self::Gcs => "Google Cloud Storage",
107 Self::Http => "HTTP/HTTPS",
108 Self::Custom => "Custom Provider",
109 }
110 }
111
112 #[must_use]
114 pub fn egress_cost_per_gb(&self) -> f64 {
115 match self {
116 Self::AwsS3 => 0.09, Self::AzureBlob => 0.087, Self::Gcs => 0.12, Self::Http => 0.0, Self::Custom => 0.0,
121 }
122 }
123
124 #[must_use]
126 pub fn storage_cost_per_gb(&self) -> f64 {
127 match self {
128 Self::AwsS3 => 0.023, Self::AzureBlob => 0.018, Self::Gcs => 0.020, Self::Http => 0.0,
132 Self::Custom => 0.0,
133 }
134 }
135}
136
137impl std::fmt::Display for CloudProvider {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "{}", self.display_name())
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
149pub enum CloudRegion {
150 UsEast1,
152 UsEast2,
154 UsWest2,
156 EuWest1,
158 EuCentral1,
160 ApNortheast1,
162 ApSoutheast1,
164 ApSoutheast2,
166 Custom(String),
168}
169
170impl CloudRegion {
171 #[must_use]
173 pub fn from_string(s: &str) -> Self {
174 match s.to_lowercase().as_str() {
175 "us-east-1" | "useast1" | "eastus" => Self::UsEast1,
176 "us-east-2" | "useast2" | "eastus2" => Self::UsEast2,
177 "us-west-2" | "uswest2" | "westus2" => Self::UsWest2,
178 "eu-west-1" | "euwest1" | "westeurope" => Self::EuWest1,
179 "eu-central-1" | "eucentral1" | "germanywestcentral" => Self::EuCentral1,
180 "ap-northeast-1" | "apnortheast1" | "japaneast" => Self::ApNortheast1,
181 "ap-southeast-1" | "apsoutheast1" | "southeastasia" => Self::ApSoutheast1,
182 "ap-southeast-2" | "apsoutheast2" | "australiaeast" => Self::ApSoutheast2,
183 _ => Self::Custom(s.to_string()),
184 }
185 }
186
187 #[must_use]
189 pub fn aws_code(&self) -> &str {
190 match self {
191 Self::UsEast1 => "us-east-1",
192 Self::UsEast2 => "us-east-2",
193 Self::UsWest2 => "us-west-2",
194 Self::EuWest1 => "eu-west-1",
195 Self::EuCentral1 => "eu-central-1",
196 Self::ApNortheast1 => "ap-northeast-1",
197 Self::ApSoutheast1 => "ap-southeast-1",
198 Self::ApSoutheast2 => "ap-southeast-2",
199 Self::Custom(s) => s.as_str(),
200 }
201 }
202
203 #[must_use]
205 pub fn azure_code(&self) -> &str {
206 match self {
207 Self::UsEast1 => "eastus",
208 Self::UsEast2 => "eastus2",
209 Self::UsWest2 => "westus2",
210 Self::EuWest1 => "westeurope",
211 Self::EuCentral1 => "germanywestcentral",
212 Self::ApNortheast1 => "japaneast",
213 Self::ApSoutheast1 => "southeastasia",
214 Self::ApSoutheast2 => "australiaeast",
215 Self::Custom(s) => s.as_str(),
216 }
217 }
218
219 #[must_use]
221 pub fn gcs_code(&self) -> &str {
222 match self {
223 Self::UsEast1 => "us-east1",
224 Self::UsEast2 => "us-east4",
225 Self::UsWest2 => "us-west1",
226 Self::EuWest1 => "europe-west1",
227 Self::EuCentral1 => "europe-west3",
228 Self::ApNortheast1 => "asia-northeast1",
229 Self::ApSoutheast1 => "asia-southeast1",
230 Self::ApSoutheast2 => "australia-southeast1",
231 Self::Custom(s) => s.as_str(),
232 }
233 }
234
235 #[must_use]
237 pub fn estimated_latency_to(&self, other: &Self) -> u32 {
238 if self == other {
239 return 1; }
241
242 match (self, other) {
244 (Self::UsEast1 | Self::UsEast2, Self::UsWest2) => 65,
246 (Self::UsWest2, Self::UsEast1 | Self::UsEast2) => 65,
247 (Self::UsEast1, Self::UsEast2) | (Self::UsEast2, Self::UsEast1) => 10,
248
249 (Self::UsEast1 | Self::UsEast2, Self::EuWest1 | Self::EuCentral1) => 80,
251 (Self::EuWest1 | Self::EuCentral1, Self::UsEast1 | Self::UsEast2) => 80,
252 (Self::UsWest2, Self::EuWest1 | Self::EuCentral1) => 140,
253 (Self::EuWest1 | Self::EuCentral1, Self::UsWest2) => 140,
254
255 (Self::EuWest1, Self::EuCentral1) | (Self::EuCentral1, Self::EuWest1) => 20,
257
258 (Self::UsWest2, Self::ApNortheast1 | Self::ApSoutheast1 | Self::ApSoutheast2) => 100,
260 (Self::ApNortheast1 | Self::ApSoutheast1 | Self::ApSoutheast2, Self::UsWest2) => 100,
261 (Self::UsEast1 | Self::UsEast2, Self::ApNortheast1 | Self::ApSoutheast1) => 180,
262 (Self::ApNortheast1 | Self::ApSoutheast1, Self::UsEast1 | Self::UsEast2) => 180,
263
264 (Self::EuWest1 | Self::EuCentral1, Self::ApNortheast1) => 220,
266 (Self::ApNortheast1, Self::EuWest1 | Self::EuCentral1) => 220,
267 (Self::EuWest1 | Self::EuCentral1, Self::ApSoutheast1 | Self::ApSoutheast2) => 180,
268 (Self::ApSoutheast1 | Self::ApSoutheast2, Self::EuWest1 | Self::EuCentral1) => 180,
269
270 (Self::ApNortheast1, Self::ApSoutheast1 | Self::ApSoutheast2) => 80,
272 (Self::ApSoutheast1 | Self::ApSoutheast2, Self::ApNortheast1) => 80,
273 (Self::ApSoutheast1, Self::ApSoutheast2) | (Self::ApSoutheast2, Self::ApSoutheast1) => {
274 60
275 }
276
277 _ => 150,
279 }
280 }
281}
282
283#[derive(Debug, Clone)]
289pub struct CloudProviderConfig {
290 pub provider: CloudProvider,
292 pub bucket: String,
294 pub prefix: String,
296 pub region: Option<CloudRegion>,
298 pub endpoint: Option<String>,
300 pub priority: u32,
302 pub weight: u32,
304 pub max_concurrent: usize,
306 pub timeout: Duration,
308 pub read_only: bool,
310 pub custom_egress_cost: Option<f64>,
312 pub options: HashMap<String, String>,
314}
315
316impl CloudProviderConfig {
317 #[must_use]
319 pub fn s3(bucket: impl Into<String>) -> Self {
320 Self {
321 provider: CloudProvider::AwsS3,
322 bucket: bucket.into(),
323 prefix: String::new(),
324 region: None,
325 endpoint: None,
326 priority: 1,
327 weight: 100,
328 max_concurrent: 100,
329 timeout: Duration::from_secs(300),
330 read_only: false,
331 custom_egress_cost: None,
332 options: HashMap::new(),
333 }
334 }
335
336 #[must_use]
338 pub fn azure(container: impl Into<String>) -> Self {
339 Self {
340 provider: CloudProvider::AzureBlob,
341 bucket: container.into(),
342 prefix: String::new(),
343 region: None,
344 endpoint: None,
345 priority: 1,
346 weight: 100,
347 max_concurrent: 100,
348 timeout: Duration::from_secs(300),
349 read_only: false,
350 custom_egress_cost: None,
351 options: HashMap::new(),
352 }
353 }
354
355 #[must_use]
357 pub fn gcs(bucket: impl Into<String>) -> Self {
358 Self {
359 provider: CloudProvider::Gcs,
360 bucket: bucket.into(),
361 prefix: String::new(),
362 region: None,
363 endpoint: None,
364 priority: 1,
365 weight: 100,
366 max_concurrent: 100,
367 timeout: Duration::from_secs(300),
368 read_only: false,
369 custom_egress_cost: None,
370 options: HashMap::new(),
371 }
372 }
373
374 #[must_use]
376 pub fn http(base_url: impl Into<String>) -> Self {
377 Self {
378 provider: CloudProvider::Http,
379 bucket: base_url.into(),
380 prefix: String::new(),
381 region: None,
382 endpoint: None,
383 priority: 1,
384 weight: 100,
385 max_concurrent: 100,
386 timeout: Duration::from_secs(300),
387 read_only: true, custom_egress_cost: None,
389 options: HashMap::new(),
390 }
391 }
392
393 #[must_use]
395 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
396 self.prefix = prefix.into();
397 self
398 }
399
400 #[must_use]
402 pub fn with_region(mut self, region: CloudRegion) -> Self {
403 self.region = Some(region);
404 self
405 }
406
407 #[must_use]
409 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
410 self.endpoint = Some(endpoint.into());
411 self
412 }
413
414 #[must_use]
416 pub fn with_priority(mut self, priority: u32) -> Self {
417 self.priority = priority;
418 self
419 }
420
421 #[must_use]
423 pub fn with_weight(mut self, weight: u32) -> Self {
424 self.weight = weight.min(100);
425 self
426 }
427
428 #[must_use]
430 pub fn with_read_only(mut self, read_only: bool) -> Self {
431 self.read_only = read_only;
432 self
433 }
434
435 #[must_use]
437 pub fn with_timeout(mut self, timeout: Duration) -> Self {
438 self.timeout = timeout;
439 self
440 }
441
442 #[must_use]
444 pub fn with_egress_cost(mut self, cost: f64) -> Self {
445 self.custom_egress_cost = Some(cost);
446 self
447 }
448
449 #[must_use]
451 pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
452 self.options.insert(key.into(), value.into());
453 self
454 }
455
456 #[must_use]
458 pub fn effective_egress_cost(&self) -> f64 {
459 self.custom_egress_cost
460 .unwrap_or_else(|| self.provider.egress_cost_per_gb())
461 }
462
463 #[must_use]
465 pub fn id(&self) -> String {
466 format!("{}:{}/{}", self.provider, self.bucket, self.prefix)
467 }
468}
469
470#[derive(Debug, Clone)]
476pub struct ProviderHealth {
477 pub provider_id: String,
479 pub healthy: bool,
481 pub last_success: Option<Instant>,
483 pub last_failure: Option<Instant>,
485 pub consecutive_failures: usize,
487 pub avg_latency_ms: f64,
489 pub success_rate: f64,
491 pub total_requests: u64,
493 pub total_bytes: u64,
495}
496
497impl ProviderHealth {
498 fn new(provider_id: String) -> Self {
500 Self {
501 provider_id,
502 healthy: true,
503 last_success: None,
504 last_failure: None,
505 consecutive_failures: 0,
506 avg_latency_ms: 0.0,
507 success_rate: 1.0,
508 total_requests: 0,
509 total_bytes: 0,
510 }
511 }
512
513 fn record_success(&mut self, latency_ms: f64, bytes: u64) {
515 self.last_success = Some(Instant::now());
516 self.consecutive_failures = 0;
517 self.healthy = true;
518 self.total_requests += 1;
519 self.total_bytes += bytes;
520
521 if self.total_requests == 1 {
523 self.avg_latency_ms = latency_ms;
524 } else {
525 self.avg_latency_ms = self.avg_latency_ms * 0.9 + latency_ms * 0.1;
526 }
527
528 self.update_success_rate(true);
530 }
531
532 fn record_failure(&mut self) {
534 self.last_failure = Some(Instant::now());
535 self.consecutive_failures += 1;
536 self.total_requests += 1;
537
538 if self.consecutive_failures >= 3 {
540 self.healthy = false;
541 }
542
543 self.update_success_rate(false);
544 }
545
546 fn update_success_rate(&mut self, success: bool) {
547 let success_value = if success { 1.0 } else { 0.0 };
548 self.success_rate = self.success_rate * 0.95 + success_value * 0.05;
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
558pub enum RoutingStrategy {
559 #[default]
561 Priority,
562 RoundRobin,
564 Weighted,
566 LatencyBased,
568 CostOptimized,
570 RegionAware,
572 Adaptive,
574}
575
576struct ProviderStats {
582 request_count: AtomicU64,
583 byte_count: AtomicU64,
584 error_count: AtomicU64,
585 latency_sum_ms: AtomicU64,
586}
587
588impl ProviderStats {
589 fn new() -> Self {
590 Self {
591 request_count: AtomicU64::new(0),
592 byte_count: AtomicU64::new(0),
593 error_count: AtomicU64::new(0),
594 latency_sum_ms: AtomicU64::new(0),
595 }
596 }
597
598 fn record_success(&self, bytes: u64, latency_ms: u64) {
599 self.request_count.fetch_add(1, Ordering::Relaxed);
600 self.byte_count.fetch_add(bytes, Ordering::Relaxed);
601 self.latency_sum_ms.fetch_add(latency_ms, Ordering::Relaxed);
602 }
603
604 fn record_error(&self) {
605 self.request_count.fetch_add(1, Ordering::Relaxed);
606 self.error_count.fetch_add(1, Ordering::Relaxed);
607 }
608
609 fn avg_latency_ms(&self) -> f64 {
610 let requests = self.request_count.load(Ordering::Relaxed);
611 let errors = self.error_count.load(Ordering::Relaxed);
612 let successful = requests.saturating_sub(errors);
613 if successful == 0 {
614 return f64::MAX;
615 }
616 self.latency_sum_ms.load(Ordering::Relaxed) as f64 / successful as f64
617 }
618
619 fn success_rate(&self) -> f64 {
620 let requests = self.request_count.load(Ordering::Relaxed);
621 if requests == 0 {
622 return 1.0;
623 }
624 let errors = self.error_count.load(Ordering::Relaxed);
625 (requests - errors) as f64 / requests as f64
626 }
627}
628
629pub struct MultiCloudManager {
631 providers: Vec<CloudProviderConfig>,
633 routing_strategy: RoutingStrategy,
635 failover_enabled: bool,
637 max_failover_attempts: usize,
639 replication_enabled: bool,
641 client_region: Option<CloudRegion>,
643 stats: HashMap<String, Arc<ProviderStats>>,
645 round_robin_counter: AtomicUsize,
647 health_check_interval: Duration,
649 #[cfg(all(feature = "async", feature = "cache"))]
652 backend_cache: dashmap::DashMap<String, Arc<dyn CloudStorageBackend>>,
653}
654
655impl MultiCloudManager {
656 #[must_use]
658 pub fn builder() -> MultiCloudManagerBuilder {
659 MultiCloudManagerBuilder::new()
660 }
661
662 #[must_use]
664 pub fn providers(&self) -> &[CloudProviderConfig] {
665 &self.providers
666 }
667
668 #[must_use]
670 pub fn get_stats(&self, provider_id: &str) -> Option<(u64, u64, f64, f64)> {
671 self.stats.get(provider_id).map(|s| {
672 (
673 s.request_count.load(Ordering::Relaxed),
674 s.byte_count.load(Ordering::Relaxed),
675 s.avg_latency_ms(),
676 s.success_rate(),
677 )
678 })
679 }
680
681 fn select_provider(&self, operation: &str) -> Result<&CloudProviderConfig> {
683 if self.providers.is_empty() {
684 return Err(CloudError::InvalidConfiguration {
685 message: "No providers configured".to_string(),
686 });
687 }
688
689 let healthy_providers: Vec<_> = self
691 .providers
692 .iter()
693 .filter(|p| {
694 let stats = self.stats.get(&p.id());
695 stats.is_none_or(|s| s.success_rate() > 0.5)
696 })
697 .collect();
698
699 let candidates = if healthy_providers.is_empty() {
700 self.providers.iter().collect::<Vec<_>>()
702 } else {
703 healthy_providers
704 };
705
706 let write_operations = ["put", "delete", "write"];
708 let candidates: Vec<_> = if write_operations.contains(&operation.to_lowercase().as_str()) {
709 candidates.into_iter().filter(|p| !p.read_only).collect()
710 } else {
711 candidates
712 };
713
714 if candidates.is_empty() {
715 return Err(CloudError::NotSupported {
716 operation: format!("No writable providers available for {operation}"),
717 });
718 }
719
720 match self.routing_strategy {
721 RoutingStrategy::Priority => {
722 candidates
724 .iter()
725 .min_by_key(|p| p.priority)
726 .copied()
727 .ok_or_else(|| CloudError::Internal {
728 message: "Failed to select provider".to_string(),
729 })
730 }
731 RoutingStrategy::RoundRobin => {
732 let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed);
734 Ok(candidates[idx % candidates.len()])
735 }
736 RoutingStrategy::Weighted => {
737 let total_weight: u32 = candidates.iter().map(|p| p.weight).sum();
739 if total_weight == 0 {
740 return Ok(candidates[0]);
741 }
742
743 let target = simple_random() % total_weight;
744 let mut cumulative = 0u32;
745
746 for provider in &candidates {
747 cumulative += provider.weight;
748 if cumulative > target {
749 return Ok(provider);
750 }
751 }
752 Ok(candidates[0])
753 }
754 RoutingStrategy::LatencyBased => {
755 candidates
757 .iter()
758 .min_by(|a, b| {
759 let lat_a = self
760 .stats
761 .get(&a.id())
762 .map_or(f64::MAX, |s| s.avg_latency_ms());
763 let lat_b = self
764 .stats
765 .get(&b.id())
766 .map_or(f64::MAX, |s| s.avg_latency_ms());
767 lat_a
768 .partial_cmp(&lat_b)
769 .unwrap_or(std::cmp::Ordering::Equal)
770 })
771 .copied()
772 .ok_or_else(|| CloudError::Internal {
773 message: "Failed to select provider".to_string(),
774 })
775 }
776 RoutingStrategy::CostOptimized => {
777 candidates
779 .iter()
780 .min_by(|a, b| {
781 let cost_a = a.effective_egress_cost();
782 let cost_b = b.effective_egress_cost();
783 cost_a
784 .partial_cmp(&cost_b)
785 .unwrap_or(std::cmp::Ordering::Equal)
786 })
787 .copied()
788 .ok_or_else(|| CloudError::Internal {
789 message: "Failed to select provider".to_string(),
790 })
791 }
792 RoutingStrategy::RegionAware => {
793 if let Some(ref client_region) = self.client_region {
795 candidates
796 .iter()
797 .min_by_key(|p| {
798 p.region
799 .as_ref()
800 .map(|r| client_region.estimated_latency_to(r))
801 .unwrap_or(500)
802 })
803 .copied()
804 .ok_or_else(|| CloudError::Internal {
805 message: "Failed to select provider".to_string(),
806 })
807 } else {
808 candidates
810 .iter()
811 .min_by_key(|p| p.priority)
812 .copied()
813 .ok_or_else(|| CloudError::Internal {
814 message: "Failed to select provider".to_string(),
815 })
816 }
817 }
818 RoutingStrategy::Adaptive => {
819 candidates
821 .iter()
822 .min_by(|a, b| {
823 let score_a = self.calculate_adaptive_score(a);
824 let score_b = self.calculate_adaptive_score(b);
825 score_a
826 .partial_cmp(&score_b)
827 .unwrap_or(std::cmp::Ordering::Equal)
828 })
829 .copied()
830 .ok_or_else(|| CloudError::Internal {
831 message: "Failed to select provider".to_string(),
832 })
833 }
834 }
835 }
836
837 fn calculate_adaptive_score(&self, provider: &CloudProviderConfig) -> f64 {
839 let stats = self.stats.get(&provider.id());
840
841 let latency_score = stats.map_or(0.5, |s| {
843 let lat = s.avg_latency_ms();
844 if lat == f64::MAX {
845 1.0
846 } else {
847 (lat / 1000.0).min(1.0) }
849 });
850
851 let cost_score = provider.effective_egress_cost() / 0.2; let health_score = stats.map_or(0.0, |s| 1.0 - s.success_rate());
856
857 let priority_score = provider.priority as f64 / 10.0;
859
860 latency_score * 0.3 + cost_score * 0.3 + health_score * 0.3 + priority_score * 0.1
862 }
863
864 fn get_failover_providers(
866 &self,
867 failed_id: &str,
868 operation: &str,
869 ) -> Vec<&CloudProviderConfig> {
870 let write_operations = ["put", "delete", "write"];
871 let is_write = write_operations.contains(&operation.to_lowercase().as_str());
872
873 let mut candidates: Vec<_> = self
874 .providers
875 .iter()
876 .filter(|p| p.id() != failed_id && (!is_write || !p.read_only))
877 .collect();
878
879 candidates.sort_by_key(|p| p.priority);
881 candidates
882 }
883
884 #[cfg(feature = "async")]
886 pub async fn get(&self, key: &str) -> Result<Bytes> {
887 let provider = self.select_provider("get")?;
888 let start = Instant::now();
889
890 match self.get_from_provider(provider, key).await {
891 Ok(data) => {
892 if let Some(stats) = self.stats.get(&provider.id()) {
893 stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
894 }
895 Ok(data)
896 }
897 Err(e) if self.failover_enabled => {
898 if let Some(stats) = self.stats.get(&provider.id()) {
899 stats.record_error();
900 }
901 tracing::warn!(
902 "Provider {} failed for get '{}': {}, attempting failover",
903 provider.id(),
904 key,
905 e
906 );
907 self.get_with_failover(key, &provider.id()).await
908 }
909 Err(e) => {
910 if let Some(stats) = self.stats.get(&provider.id()) {
911 stats.record_error();
912 }
913 Err(e)
914 }
915 }
916 }
917
918 #[cfg(feature = "async")]
924 fn build_backend(&self, p: &CloudProviderConfig) -> Result<Box<dyn CloudStorageBackend>> {
925 match p.provider {
926 CloudProvider::AwsS3 => {
927 #[cfg(feature = "s3")]
928 {
929 use crate::backends::S3Backend;
930
931 let mut backend =
932 S3Backend::new(p.bucket.clone(), p.prefix.clone()).with_timeout(p.timeout);
933 if let Some(region) = &p.region {
934 backend = backend.with_region(region.aws_code());
935 }
936 if let Some(endpoint) = &p.endpoint {
937 backend = backend.with_endpoint(endpoint.clone());
938 }
939 Ok(Box::new(backend))
940 }
941 #[cfg(not(feature = "s3"))]
942 {
943 Err(CloudError::NotSupported {
944 operation: format!(
945 "AWS S3 backend for provider '{}': enable the 's3' feature",
946 p.id()
947 ),
948 })
949 }
950 }
951 CloudProvider::Gcs => {
952 #[cfg(feature = "gcs")]
953 {
954 use crate::backends::GcsBackend;
955
956 let mut backend = GcsBackend::new(p.bucket.clone())
957 .with_prefix(p.prefix.clone())
958 .with_timeout(p.timeout);
959 if let Some(project_id) = p.options.get("project_id") {
960 backend = backend.with_project_id(project_id.clone());
961 }
962 Ok(Box::new(backend))
963 }
964 #[cfg(not(feature = "gcs"))]
965 {
966 Err(CloudError::NotSupported {
967 operation: format!(
968 "GCS backend for provider '{}': enable the 'gcs' feature",
969 p.id()
970 ),
971 })
972 }
973 }
974 CloudProvider::AzureBlob => {
975 #[cfg(feature = "azure-blob")]
976 {
977 use crate::backends::AzureBlobBackend;
978
979 let account_name = p.options.get("account_name").ok_or_else(|| {
980 CloudError::InvalidConfiguration {
981 message: format!(
982 "Azure provider '{}' is missing the required 'account_name' option",
983 p.id()
984 ),
985 }
986 })?;
987
988 let mut backend = AzureBlobBackend::new(account_name.clone(), p.bucket.clone())
989 .with_prefix(p.prefix.clone())
990 .with_timeout(p.timeout);
991 if let Some(account_key) = p.options.get("account_key") {
992 backend = backend.with_account_key(account_key.clone());
993 }
994 if let Some(sas_token) = p.options.get("sas_token") {
995 backend = backend.with_sas_token(sas_token.clone());
996 }
997 Ok(Box::new(backend))
998 }
999 #[cfg(not(feature = "azure-blob"))]
1000 {
1001 Err(CloudError::NotSupported {
1002 operation: format!(
1003 "Azure Blob backend for provider '{}': enable the 'azure-blob' feature",
1004 p.id()
1005 ),
1006 })
1007 }
1008 }
1009 CloudProvider::Http => {
1010 #[cfg(feature = "http")]
1011 {
1012 use crate::backends::HttpBackend;
1013
1014 let backend = HttpBackend::new(p.bucket.clone()).with_timeout(p.timeout);
1015 Ok(Box::new(backend))
1016 }
1017 #[cfg(not(feature = "http"))]
1018 {
1019 Err(CloudError::NotSupported {
1020 operation: format!(
1021 "HTTP backend for provider '{}': enable the 'http' feature",
1022 p.id()
1023 ),
1024 })
1025 }
1026 }
1027 CloudProvider::Custom => Err(CloudError::NotSupported {
1028 operation: format!(
1029 "Custom provider '{}' has no built-in backend implementation",
1030 p.id()
1031 ),
1032 }),
1033 }
1034 }
1035
1036 #[cfg(all(feature = "async", feature = "cache"))]
1039 fn resolve_backend(
1040 &self,
1041 provider: &CloudProviderConfig,
1042 ) -> Result<Arc<dyn CloudStorageBackend>> {
1043 let id = provider.id();
1044 if let Some(existing) = self.backend_cache.get(&id) {
1045 return Ok(Arc::clone(existing.value()));
1046 }
1047
1048 let backend: Arc<dyn CloudStorageBackend> = Arc::from(self.build_backend(provider)?);
1049 self.backend_cache.insert(id, Arc::clone(&backend));
1050 Ok(backend)
1051 }
1052
1053 #[cfg(all(feature = "async", not(feature = "cache")))]
1056 fn resolve_backend(
1057 &self,
1058 provider: &CloudProviderConfig,
1059 ) -> Result<Arc<dyn CloudStorageBackend>> {
1060 Ok(Arc::from(self.build_backend(provider)?))
1061 }
1062
1063 #[cfg(feature = "async")]
1064 async fn get_from_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<Bytes> {
1065 let backend = self.resolve_backend(provider)?;
1066 backend.get(key).await
1067 }
1068
1069 #[cfg(feature = "async")]
1070 async fn get_with_failover(&self, key: &str, failed_id: &str) -> Result<Bytes> {
1071 let failover_providers = self.get_failover_providers(failed_id, "get");
1072 let mut attempts = 0;
1073
1074 for provider in failover_providers {
1075 if attempts >= self.max_failover_attempts {
1076 break;
1077 }
1078 attempts += 1;
1079
1080 let start = Instant::now();
1081 match self.get_from_provider(provider, key).await {
1082 Ok(data) => {
1083 if let Some(stats) = self.stats.get(&provider.id()) {
1084 stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1085 }
1086 tracing::info!(
1087 "Failover successful to provider {} for key '{}'",
1088 provider.id(),
1089 key
1090 );
1091 return Ok(data);
1092 }
1093 Err(e) => {
1094 if let Some(stats) = self.stats.get(&provider.id()) {
1095 stats.record_error();
1096 }
1097 tracing::warn!(
1098 "Failover attempt {} to {} failed: {}",
1099 attempts,
1100 provider.id(),
1101 e
1102 );
1103 }
1104 }
1105 }
1106
1107 Err(CloudError::Internal {
1108 message: format!("All failover attempts exhausted for key '{key}'"),
1109 })
1110 }
1111
1112 #[cfg(feature = "async")]
1114 pub async fn put(&self, key: &str, data: &[u8]) -> Result<()> {
1115 let provider = self.select_provider("put")?;
1116 let start = Instant::now();
1117
1118 match self.put_to_provider(provider, key, data).await {
1119 Ok(()) => {
1120 if let Some(stats) = self.stats.get(&provider.id()) {
1121 stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1122 }
1123
1124 if self.replication_enabled {
1126 self.replicate_to_other_providers(key, data, &provider.id())
1127 .await;
1128 }
1129
1130 Ok(())
1131 }
1132 Err(e) if self.failover_enabled => {
1133 if let Some(stats) = self.stats.get(&provider.id()) {
1134 stats.record_error();
1135 }
1136 tracing::warn!(
1137 "Provider {} failed for put '{}': {}, attempting failover",
1138 provider.id(),
1139 key,
1140 e
1141 );
1142 self.put_with_failover(key, data, &provider.id()).await
1143 }
1144 Err(e) => {
1145 if let Some(stats) = self.stats.get(&provider.id()) {
1146 stats.record_error();
1147 }
1148 Err(e)
1149 }
1150 }
1151 }
1152
1153 #[cfg(feature = "async")]
1154 async fn put_to_provider(
1155 &self,
1156 provider: &CloudProviderConfig,
1157 key: &str,
1158 data: &[u8],
1159 ) -> Result<()> {
1160 let backend = self.resolve_backend(provider)?;
1161 backend.put(key, data).await
1162 }
1163
1164 #[cfg(feature = "async")]
1165 async fn put_with_failover(&self, key: &str, data: &[u8], failed_id: &str) -> Result<()> {
1166 let failover_providers = self.get_failover_providers(failed_id, "put");
1167 let mut attempts = 0;
1168
1169 for provider in failover_providers {
1170 if attempts >= self.max_failover_attempts {
1171 break;
1172 }
1173 attempts += 1;
1174
1175 let start = Instant::now();
1176 match self.put_to_provider(provider, key, data).await {
1177 Ok(()) => {
1178 if let Some(stats) = self.stats.get(&provider.id()) {
1179 stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1180 }
1181 tracing::info!(
1182 "Failover successful to provider {} for put '{}'",
1183 provider.id(),
1184 key
1185 );
1186 return Ok(());
1187 }
1188 Err(e) => {
1189 if let Some(stats) = self.stats.get(&provider.id()) {
1190 stats.record_error();
1191 }
1192 tracing::warn!(
1193 "Failover attempt {} to {} failed: {}",
1194 attempts,
1195 provider.id(),
1196 e
1197 );
1198 }
1199 }
1200 }
1201
1202 Err(CloudError::Internal {
1203 message: format!("All failover attempts exhausted for put '{key}'"),
1204 })
1205 }
1206
1207 #[cfg(feature = "async")]
1208 async fn replicate_to_other_providers(&self, key: &str, data: &[u8], primary_id: &str) {
1209 let replication_targets: Vec<_> = self
1210 .providers
1211 .iter()
1212 .filter(|p| p.id() != primary_id && !p.read_only)
1213 .collect();
1214
1215 for provider in replication_targets {
1216 if let Err(e) = self.put_to_provider(provider, key, data).await {
1217 tracing::warn!(
1218 "Replication to {} failed for key '{}': {}",
1219 provider.id(),
1220 key,
1221 e
1222 );
1223 }
1224 }
1225 }
1226
1227 #[cfg(feature = "async")]
1229 pub async fn exists(&self, key: &str) -> Result<bool> {
1230 let provider = self.select_provider("exists")?;
1231
1232 match self.exists_in_provider(provider, key).await {
1233 Ok(exists) => Ok(exists),
1234 Err(e) if self.failover_enabled => {
1235 tracing::warn!(
1236 "Provider {} failed for exists '{}': {}, checking other providers",
1237 provider.id(),
1238 key,
1239 e
1240 );
1241
1242 for fallback in self.get_failover_providers(&provider.id(), "exists") {
1243 if let Ok(exists) = self.exists_in_provider(fallback, key).await {
1244 return Ok(exists);
1245 }
1246 }
1247 Err(e)
1248 }
1249 Err(e) => Err(e),
1250 }
1251 }
1252
1253 #[cfg(feature = "async")]
1254 async fn exists_in_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<bool> {
1255 let backend = self.resolve_backend(provider)?;
1256 backend.exists(key).await
1257 }
1258
1259 #[cfg(feature = "async")]
1261 pub async fn delete(&self, key: &str) -> Result<()> {
1262 let mut success = false;
1263 let mut last_error = None;
1264
1265 for provider in &self.providers {
1266 if provider.read_only {
1267 continue;
1268 }
1269
1270 match self.delete_from_provider(provider, key).await {
1271 Ok(()) => success = true,
1272 Err(e) => {
1273 tracing::warn!(
1274 "Failed to delete '{}' from provider {}: {}",
1275 key,
1276 provider.id(),
1277 e
1278 );
1279 last_error = Some(e);
1280 }
1281 }
1282 }
1283
1284 if success {
1285 Ok(())
1286 } else {
1287 Err(last_error.unwrap_or_else(|| CloudError::NotSupported {
1288 operation: "No writable providers available".to_string(),
1289 }))
1290 }
1291 }
1292
1293 #[cfg(feature = "async")]
1294 async fn delete_from_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<()> {
1295 let backend = self.resolve_backend(provider)?;
1296 backend.delete(key).await
1297 }
1298
1299 #[must_use]
1301 pub fn estimate_transfer_cost(&self, bytes: u64) -> TransferCostEstimate {
1302 let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1303
1304 let mut estimates = Vec::new();
1305 for provider in &self.providers {
1306 let egress_cost = provider.effective_egress_cost() * gb;
1307 estimates.push((provider.id(), egress_cost));
1308 }
1309
1310 estimates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1311
1312 let (cheapest_id, cheapest_cost) = estimates.first().cloned().unwrap_or_default();
1313 let (most_expensive_id, most_expensive_cost) =
1314 estimates.last().cloned().unwrap_or_default();
1315
1316 TransferCostEstimate {
1317 bytes,
1318 cheapest_provider: cheapest_id,
1319 cheapest_cost,
1320 most_expensive_provider: most_expensive_id,
1321 most_expensive_cost,
1322 all_estimates: estimates,
1323 }
1324 }
1325}
1326
1327pub struct MultiCloudManagerBuilder {
1333 providers: Vec<CloudProviderConfig>,
1334 routing_strategy: RoutingStrategy,
1335 failover_enabled: bool,
1336 max_failover_attempts: usize,
1337 replication_enabled: bool,
1338 client_region: Option<CloudRegion>,
1339 health_check_interval: Duration,
1340}
1341
1342impl MultiCloudManagerBuilder {
1343 #[must_use]
1345 pub fn new() -> Self {
1346 Self {
1347 providers: Vec::new(),
1348 routing_strategy: RoutingStrategy::Priority,
1349 failover_enabled: true,
1350 max_failover_attempts: 3,
1351 replication_enabled: false,
1352 client_region: None,
1353 health_check_interval: Duration::from_secs(60),
1354 }
1355 }
1356
1357 #[must_use]
1359 pub fn add_provider(mut self, config: CloudProviderConfig) -> Self {
1360 self.providers.push(config);
1361 self
1362 }
1363
1364 #[must_use]
1366 pub fn with_routing_strategy(mut self, strategy: RoutingStrategy) -> Self {
1367 self.routing_strategy = strategy;
1368 self
1369 }
1370
1371 #[must_use]
1373 pub fn with_failover(mut self, enabled: bool) -> Self {
1374 self.failover_enabled = enabled;
1375 self
1376 }
1377
1378 #[must_use]
1380 pub fn with_max_failover_attempts(mut self, attempts: usize) -> Self {
1381 self.max_failover_attempts = attempts;
1382 self
1383 }
1384
1385 #[must_use]
1387 pub fn with_latency_routing(mut self, enabled: bool) -> Self {
1388 if enabled {
1389 self.routing_strategy = RoutingStrategy::LatencyBased;
1390 }
1391 self
1392 }
1393
1394 #[must_use]
1396 pub fn with_cost_routing(mut self, enabled: bool) -> Self {
1397 if enabled {
1398 self.routing_strategy = RoutingStrategy::CostOptimized;
1399 }
1400 self
1401 }
1402
1403 #[must_use]
1405 pub fn with_replication(mut self, enabled: bool) -> Self {
1406 self.replication_enabled = enabled;
1407 self
1408 }
1409
1410 #[must_use]
1412 pub fn with_client_region(mut self, region: CloudRegion) -> Self {
1413 self.client_region = Some(region);
1414 self
1415 }
1416
1417 #[must_use]
1419 pub fn with_health_check_interval(mut self, interval: Duration) -> Self {
1420 self.health_check_interval = interval;
1421 self
1422 }
1423
1424 pub fn build(self) -> Result<MultiCloudManager> {
1426 if self.providers.is_empty() {
1427 return Err(CloudError::InvalidConfiguration {
1428 message: "At least one provider must be configured".to_string(),
1429 });
1430 }
1431
1432 let mut stats = HashMap::new();
1433 for provider in &self.providers {
1434 stats.insert(provider.id(), Arc::new(ProviderStats::new()));
1435 }
1436
1437 Ok(MultiCloudManager {
1438 providers: self.providers,
1439 routing_strategy: self.routing_strategy,
1440 failover_enabled: self.failover_enabled,
1441 max_failover_attempts: self.max_failover_attempts,
1442 replication_enabled: self.replication_enabled,
1443 client_region: self.client_region,
1444 stats,
1445 round_robin_counter: AtomicUsize::new(0),
1446 health_check_interval: self.health_check_interval,
1447 #[cfg(all(feature = "async", feature = "cache"))]
1448 backend_cache: dashmap::DashMap::new(),
1449 })
1450 }
1451}
1452
1453impl Default for MultiCloudManagerBuilder {
1454 fn default() -> Self {
1455 Self::new()
1456 }
1457}
1458
1459#[derive(Debug, Clone)]
1465pub struct TransferCostEstimate {
1466 pub bytes: u64,
1468 pub cheapest_provider: String,
1470 pub cheapest_cost: f64,
1472 pub most_expensive_provider: String,
1474 pub most_expensive_cost: f64,
1476 pub all_estimates: Vec<(String, f64)>,
1478}
1479
1480impl Default for TransferCostEstimate {
1481 fn default() -> Self {
1482 Self {
1483 bytes: 0,
1484 cheapest_provider: String::new(),
1485 cheapest_cost: 0.0,
1486 most_expensive_provider: String::new(),
1487 most_expensive_cost: 0.0,
1488 all_estimates: Vec::new(),
1489 }
1490 }
1491}
1492
1493#[derive(Debug, Clone)]
1499pub struct CrossCloudTransferConfig {
1500 pub source_provider: String,
1502 pub dest_provider: String,
1504 pub chunk_size: usize,
1506 pub max_concurrent: usize,
1508 pub verify_integrity: bool,
1510 pub delete_source: bool,
1512}
1513
1514impl Default for CrossCloudTransferConfig {
1515 fn default() -> Self {
1516 Self {
1517 source_provider: String::new(),
1518 dest_provider: String::new(),
1519 chunk_size: 8 * 1024 * 1024, max_concurrent: 4,
1521 verify_integrity: true,
1522 delete_source: false,
1523 }
1524 }
1525}
1526
1527#[derive(Debug, Clone)]
1529pub struct CrossCloudTransferResult {
1530 pub objects_transferred: usize,
1532 pub bytes_transferred: u64,
1534 pub duration: Duration,
1536 pub failures: Vec<(String, String)>,
1538 pub estimated_cost: f64,
1540}
1541
1542fn simple_random() -> u32 {
1548 use std::sync::atomic::{AtomicU64, Ordering};
1549 static SEED: AtomicU64 = AtomicU64::new(0x5deece66d);
1550
1551 let seed = SEED.load(Ordering::Relaxed);
1552 let next = seed.wrapping_mul(0x5deece66d).wrapping_add(0xb);
1553 SEED.store(next, Ordering::Relaxed);
1554
1555 (next >> 17) as u32
1556}
1557
1558#[cfg(test)]
1563mod tests {
1564 use super::*;
1565
1566 #[test]
1567 fn test_cloud_provider_from_url() {
1568 assert_eq!(
1569 CloudProvider::from_url("s3://bucket/key"),
1570 Some(CloudProvider::AwsS3)
1571 );
1572 assert_eq!(
1573 CloudProvider::from_url("gs://bucket/object"),
1574 Some(CloudProvider::Gcs)
1575 );
1576 assert_eq!(
1577 CloudProvider::from_url("az://container/blob"),
1578 Some(CloudProvider::AzureBlob)
1579 );
1580 assert_eq!(
1581 CloudProvider::from_url("https://example.com/path"),
1582 Some(CloudProvider::Http)
1583 );
1584 assert_eq!(
1585 CloudProvider::from_url("https://mybucket.s3.amazonaws.com/key"),
1586 Some(CloudProvider::AwsS3)
1587 );
1588 assert_eq!(
1589 CloudProvider::from_url("https://account.blob.core.windows.net/container"),
1590 Some(CloudProvider::AzureBlob)
1591 );
1592 assert_eq!(
1593 CloudProvider::from_url("https://storage.googleapis.com/bucket/object"),
1594 Some(CloudProvider::Gcs)
1595 );
1596 assert_eq!(CloudProvider::from_url("invalid"), None);
1597 }
1598
1599 #[test]
1600 fn test_cloud_region_codes() {
1601 let region = CloudRegion::UsEast1;
1602 assert_eq!(region.aws_code(), "us-east-1");
1603 assert_eq!(region.azure_code(), "eastus");
1604 assert_eq!(region.gcs_code(), "us-east1");
1605
1606 let region = CloudRegion::from_string("eu-west-1");
1607 assert_eq!(region, CloudRegion::EuWest1);
1608 }
1609
1610 #[test]
1611 fn test_region_latency_estimation() {
1612 let us_east = CloudRegion::UsEast1;
1613 let us_west = CloudRegion::UsWest2;
1614 let eu_west = CloudRegion::EuWest1;
1615
1616 assert_eq!(us_east.estimated_latency_to(&us_east), 1);
1618
1619 let us_to_us = us_east.estimated_latency_to(&us_west);
1621 assert!(us_to_us > 50 && us_to_us < 100);
1622
1623 let us_to_eu = us_east.estimated_latency_to(&eu_west);
1625 assert!(us_to_eu > us_to_us);
1626 }
1627
1628 #[test]
1629 fn test_provider_config_builder() {
1630 let config = CloudProviderConfig::s3("my-bucket")
1631 .with_prefix("data/")
1632 .with_region(CloudRegion::UsWest2)
1633 .with_priority(1)
1634 .with_weight(80)
1635 .with_timeout(Duration::from_secs(60));
1636
1637 assert_eq!(config.bucket, "my-bucket");
1638 assert_eq!(config.prefix, "data/");
1639 assert_eq!(config.region, Some(CloudRegion::UsWest2));
1640 assert_eq!(config.priority, 1);
1641 assert_eq!(config.weight, 80);
1642 assert_eq!(config.timeout, Duration::from_secs(60));
1643 }
1644
1645 #[test]
1646 fn test_provider_config_id() {
1647 let config = CloudProviderConfig::s3("bucket").with_prefix("prefix");
1648 assert_eq!(config.id(), "AWS S3:bucket/prefix");
1649 }
1650
1651 #[test]
1652 fn test_egress_costs() {
1653 assert!(CloudProvider::AwsS3.egress_cost_per_gb() > 0.0);
1654 assert!(CloudProvider::AzureBlob.egress_cost_per_gb() > 0.0);
1655 assert!(CloudProvider::Gcs.egress_cost_per_gb() > 0.0);
1656 assert_eq!(CloudProvider::Http.egress_cost_per_gb(), 0.0);
1657 }
1658
1659 #[test]
1660 fn test_multicloud_manager_builder() {
1661 let manager = MultiCloudManager::builder()
1662 .add_provider(CloudProviderConfig::s3("bucket1").with_priority(1))
1663 .add_provider(CloudProviderConfig::gcs("bucket2").with_priority(2))
1664 .with_failover(true)
1665 .with_latency_routing(true)
1666 .build();
1667
1668 assert!(manager.is_ok());
1669 let manager = manager.expect("Manager should be built");
1670 assert_eq!(manager.providers.len(), 2);
1671 assert_eq!(manager.routing_strategy, RoutingStrategy::LatencyBased);
1672 }
1673
1674 #[test]
1675 fn test_multicloud_manager_empty_providers() {
1676 let manager = MultiCloudManager::builder().build();
1677 assert!(manager.is_err());
1678 }
1679
1680 #[test]
1681 fn test_transfer_cost_estimate() {
1682 let manager = MultiCloudManager::builder()
1683 .add_provider(CloudProviderConfig::s3("bucket1"))
1684 .add_provider(CloudProviderConfig::http("http://example.com"))
1685 .build()
1686 .expect("Manager should be built");
1687
1688 let estimate = manager.estimate_transfer_cost(1024 * 1024 * 1024); assert!(estimate.cheapest_cost <= estimate.most_expensive_cost);
1690 }
1691
1692 #[test]
1693 fn test_provider_health() {
1694 let mut health = ProviderHealth::new("test-provider".to_string());
1695
1696 assert!(health.healthy);
1697 assert_eq!(health.consecutive_failures, 0);
1698
1699 health.record_success(100.0, 1000);
1701 health.record_success(120.0, 2000);
1702 assert!(health.avg_latency_ms > 0.0);
1703 assert_eq!(health.total_bytes, 3000);
1704
1705 health.record_failure();
1707 health.record_failure();
1708 health.record_failure();
1709
1710 assert!(!health.healthy);
1711 assert_eq!(health.consecutive_failures, 3);
1712 }
1713
1714 #[test]
1715 fn test_provider_stats() {
1716 let stats = ProviderStats::new();
1717
1718 stats.record_success(1000, 50);
1719 stats.record_success(2000, 60);
1720
1721 assert_eq!(stats.request_count.load(Ordering::Relaxed), 2);
1722 assert_eq!(stats.byte_count.load(Ordering::Relaxed), 3000);
1723 assert!((stats.avg_latency_ms() - 55.0).abs() < 0.001);
1724 assert!((stats.success_rate() - 1.0).abs() < 0.001);
1725
1726 stats.record_error();
1727 assert!(stats.success_rate() < 1.0);
1728 }
1729
1730 #[test]
1731 fn test_cross_cloud_transfer_config() {
1732 let config = CrossCloudTransferConfig::default();
1733
1734 assert_eq!(config.chunk_size, 8 * 1024 * 1024);
1735 assert_eq!(config.max_concurrent, 4);
1736 assert!(config.verify_integrity);
1737 assert!(!config.delete_source);
1738 }
1739
1740 #[test]
1741 fn test_routing_strategy_default() {
1742 let strategy = RoutingStrategy::default();
1743 assert_eq!(strategy, RoutingStrategy::Priority);
1744 }
1745
1746 #[test]
1747 fn test_select_provider_priority() {
1748 let manager = MultiCloudManager::builder()
1749 .add_provider(CloudProviderConfig::s3("bucket1").with_priority(2))
1750 .add_provider(CloudProviderConfig::gcs("bucket2").with_priority(1))
1751 .with_routing_strategy(RoutingStrategy::Priority)
1752 .build()
1753 .expect("Manager should be built");
1754
1755 let provider = manager.select_provider("get");
1756 assert!(provider.is_ok());
1757 let provider = provider.expect("Provider should be selected");
1758 assert_eq!(provider.provider, CloudProvider::Gcs);
1759 }
1760
1761 #[test]
1762 fn test_select_provider_cost_optimized() {
1763 let manager = MultiCloudManager::builder()
1764 .add_provider(CloudProviderConfig::s3("bucket1"))
1765 .add_provider(CloudProviderConfig::http("http://example.com"))
1766 .with_routing_strategy(RoutingStrategy::CostOptimized)
1767 .build()
1768 .expect("Manager should be built");
1769
1770 let provider = manager.select_provider("get");
1771 assert!(provider.is_ok());
1772 let provider = provider.expect("Provider should be selected");
1773 assert_eq!(provider.provider, CloudProvider::Http);
1775 }
1776
1777 #[test]
1778 fn test_select_provider_write_filters_readonly() {
1779 let manager = MultiCloudManager::builder()
1780 .add_provider(CloudProviderConfig::http("http://example.com").with_priority(1))
1781 .add_provider(CloudProviderConfig::s3("bucket1").with_priority(2))
1782 .with_routing_strategy(RoutingStrategy::Priority)
1783 .build()
1784 .expect("Manager should be built");
1785
1786 let provider = manager.select_provider("put");
1788 assert!(provider.is_ok());
1789 let provider = provider.expect("Provider should be selected");
1790 assert_eq!(provider.provider, CloudProvider::AwsS3);
1791 }
1792
1793 #[cfg(feature = "async")]
1794 #[test]
1795 fn test_build_backend_custom_provider_not_supported() {
1796 let manager = MultiCloudManager::builder()
1797 .add_provider(CloudProviderConfig::http("http://example.com"))
1798 .build()
1799 .expect("Manager should be built");
1800
1801 let mut custom = CloudProviderConfig::http("http://example.com");
1802 custom.provider = CloudProvider::Custom;
1803
1804 let result = manager.build_backend(&custom);
1805 assert!(result.is_err());
1806 }
1807
1808 #[cfg(all(feature = "async", feature = "azure-blob"))]
1809 #[test]
1810 fn test_build_backend_azure_requires_account_name() {
1811 let manager = MultiCloudManager::builder()
1812 .add_provider(CloudProviderConfig::azure("container"))
1813 .build()
1814 .expect("Manager should be built");
1815
1816 let config = CloudProviderConfig::azure("container");
1817 let result = manager.build_backend(&config);
1818 assert!(matches!(
1819 result,
1820 Err(CloudError::InvalidConfiguration { .. })
1821 ));
1822
1823 let config_with_account = config.with_option("account_name", "myaccount");
1824 let result = manager.build_backend(&config_with_account);
1825 assert!(result.is_ok());
1826 }
1827
1828 #[cfg(all(feature = "async", feature = "http"))]
1829 #[test]
1830 fn test_build_backend_http() {
1831 let manager = MultiCloudManager::builder()
1832 .add_provider(CloudProviderConfig::http("http://example.com"))
1833 .build()
1834 .expect("Manager should be built");
1835
1836 let config = CloudProviderConfig::http("http://example.com");
1837 let result = manager.build_backend(&config);
1838 assert!(result.is_ok());
1839 }
1840
1841 #[cfg(all(feature = "async", feature = "s3"))]
1842 #[test]
1843 fn test_build_backend_s3_applies_region_and_endpoint() {
1844 let manager = MultiCloudManager::builder()
1845 .add_provider(CloudProviderConfig::s3("bucket"))
1846 .build()
1847 .expect("Manager should be built");
1848
1849 let config = CloudProviderConfig::s3("bucket")
1850 .with_region(CloudRegion::UsWest2)
1851 .with_endpoint("https://custom.example.com");
1852 let result = manager.build_backend(&config);
1853 assert!(result.is_ok());
1854 }
1855
1856 #[cfg(all(feature = "async", feature = "gcs"))]
1857 #[test]
1858 fn test_build_backend_gcs() {
1859 let manager = MultiCloudManager::builder()
1860 .add_provider(CloudProviderConfig::gcs("bucket"))
1861 .build()
1862 .expect("Manager should be built");
1863
1864 let config = CloudProviderConfig::gcs("bucket").with_option("project_id", "my-project");
1865 let result = manager.build_backend(&config);
1866 assert!(result.is_ok());
1867 }
1868
1869 #[cfg(all(feature = "async", feature = "http", feature = "cache"))]
1870 #[test]
1871 fn test_resolve_backend_caches_instance() {
1872 let manager = MultiCloudManager::builder()
1873 .add_provider(CloudProviderConfig::http("http://example.com"))
1874 .build()
1875 .expect("Manager should be built");
1876
1877 let config = CloudProviderConfig::http("http://example.com");
1878 let first = manager
1879 .resolve_backend(&config)
1880 .expect("first resolve should succeed");
1881 let second = manager
1882 .resolve_backend(&config)
1883 .expect("second resolve should succeed");
1884
1885 assert!(Arc::ptr_eq(&first, &second));
1886 }
1887}