1use aes_gcm::{
3 aead::{Aead, KeyInit, OsRng},
4 Aes256Gcm, Nonce,
5};
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use tokio::fs;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CloudStorageConfig {
15 pub provider: StorageProvider,
16 pub bucket_name: String,
17 pub region: String,
18 pub access_key: Option<String>,
19 pub secret_key: Option<String>,
20 pub endpoint: Option<String>,
21 pub encryption_enabled: bool,
22 pub compression_enabled: bool,
23 pub sync_interval_seconds: u64,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum StorageProvider {
28 AWS,
29 Azure,
30 GoogleCloud,
31 MinIO,
32 S3Compatible,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SyncableItem {
37 pub local_path: PathBuf,
38 pub remote_path: String,
39 pub last_modified: u64,
40 pub checksum: String,
41 pub size_bytes: u64,
42 pub sync_priority: SyncPriority,
43 pub sync_direction: SyncDirection,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub enum SyncPriority {
48 Low,
49 Normal,
50 High,
51 Critical,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum SyncDirection {
56 Upload,
57 Download,
58 Bidirectional,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SyncManifest {
63 pub version: u32,
64 pub last_sync_timestamp: u64,
65 pub items: Vec<SyncableItem>,
66 pub total_size_bytes: u64,
67 pub checksum: String,
68}
69
70pub struct CloudStorageManager {
71 config: CloudStorageConfig,
72 local_cache_dir: PathBuf,
73 sync_manifest: SyncManifest,
74 pending_uploads: Vec<SyncableItem>,
75 pending_downloads: Vec<SyncableItem>,
76}
77
78impl CloudStorageManager {
79 pub fn new(config: CloudStorageConfig, cache_dir: PathBuf) -> Result<Self> {
80 std::fs::create_dir_all(&cache_dir)?;
81
82 let manifest_path = cache_dir.join("sync_manifest.json");
83 let sync_manifest = if manifest_path.exists() {
84 let content = std::fs::read_to_string(&manifest_path)?;
85 serde_json::from_str(&content)?
86 } else {
87 SyncManifest::new()
88 };
89
90 Ok(Self {
91 config,
92 local_cache_dir: cache_dir,
93 sync_manifest,
94 pending_uploads: Vec::new(),
95 pending_downloads: Vec::new(),
96 })
97 }
98
99 pub async fn add_to_sync(
101 &mut self,
102 local_path: PathBuf,
103 remote_path: String,
104 direction: SyncDirection,
105 ) -> Result<()> {
106 let metadata = fs::metadata(&local_path).await?;
107 let last_modified = metadata
108 .modified()?
109 .duration_since(std::time::UNIX_EPOCH)?
110 .as_secs();
111
112 let checksum = self.calculate_file_checksum(&local_path).await?;
113
114 let item = SyncableItem {
115 local_path,
116 remote_path,
117 last_modified,
118 checksum,
119 size_bytes: metadata.len(),
120 sync_priority: SyncPriority::Normal,
121 sync_direction: direction,
122 };
123
124 self.sync_manifest.items.push(item);
125 self.save_manifest().await?;
126
127 Ok(())
128 }
129
130 pub async fn sync(&mut self) -> Result<SyncResult> {
132 let mut result = SyncResult::new();
133
134 for item in &self.sync_manifest.items {
136 match item.sync_direction {
137 SyncDirection::Upload => {
138 if self.should_upload(item).await? {
139 match self.upload_file(item).await {
140 Ok(_) => result.uploaded_files += 1,
141 Err(e) => {
142 result.failed_uploads += 1;
143 result.errors.push(format!(
144 "Upload failed for {}: {}",
145 item.local_path.display(),
146 e
147 ));
148 }
149 }
150 }
151 }
152 SyncDirection::Download => {
153 if self.should_download(item).await? {
154 match self.download_file(item).await {
155 Ok(_) => result.downloaded_files += 1,
156 Err(e) => {
157 result.failed_downloads += 1;
158 result.errors.push(format!(
159 "Download failed for {}: {}",
160 item.remote_path, e
161 ));
162 }
163 }
164 }
165 }
166 SyncDirection::Bidirectional => {
167 let sync_direction = self.determine_sync_direction(item).await?;
169 match sync_direction {
170 Some(SyncDirection::Upload) => match self.upload_file(item).await {
171 Ok(_) => result.uploaded_files += 1,
172 Err(e) => {
173 result.failed_uploads += 1;
174 result.errors.push(format!(
175 "Upload failed for {}: {}",
176 item.local_path.display(),
177 e
178 ));
179 }
180 },
181 Some(SyncDirection::Download) => match self.download_file(item).await {
182 Ok(_) => result.downloaded_files += 1,
183 Err(e) => {
184 result.failed_downloads += 1;
185 result.errors.push(format!(
186 "Download failed for {}: {}",
187 item.remote_path, e
188 ));
189 }
190 },
191 _ => {
192 result.skipped_files += 1;
194 }
195 }
196 }
197 }
198 }
199
200 self.sync_manifest.last_sync_timestamp = std::time::SystemTime::now()
202 .duration_since(std::time::UNIX_EPOCH)?
203 .as_secs();
204
205 self.save_manifest().await?;
206
207 Ok(result)
208 }
209
210 async fn upload_file(&self, item: &SyncableItem) -> Result<()> {
212 tracing::info!(
213 "Uploading {} to {}",
214 item.local_path.display(),
215 item.remote_path
216 );
217
218 if !item.local_path.exists() {
220 return Err(anyhow::anyhow!(
221 "Local file does not exist: {}",
222 item.local_path.display()
223 ));
224 }
225
226 let file_content = fs::read(&item.local_path).await?;
228
229 let file_checksum = calculate_file_checksum(&file_content);
231 if file_checksum != item.checksum {
232 return Err(anyhow::anyhow!("File checksum mismatch during upload"));
233 }
234
235 let upload_content = if self.config.compression_enabled {
237 self.compress_data(&file_content).await?
238 } else {
239 file_content
240 };
241
242 let final_content = if self.config.encryption_enabled {
244 self.encrypt_data(&upload_content).await?
245 } else {
246 upload_content
247 };
248
249 match self.config.provider {
251 StorageProvider::AWS => {
252 self.upload_to_aws(&item.remote_path, &final_content)
253 .await?
254 }
255 StorageProvider::Azure => {
256 self.upload_to_azure(&item.remote_path, &final_content)
257 .await?
258 }
259 StorageProvider::GoogleCloud => {
260 self.upload_to_gcp(&item.remote_path, &final_content)
261 .await?
262 }
263 StorageProvider::MinIO | StorageProvider::S3Compatible => {
264 self.upload_to_s3_compatible(&item.remote_path, &final_content)
265 .await?
266 }
267 }
268
269 tracing::info!(
270 "Successfully uploaded {} ({} bytes) to {}",
271 item.local_path.display(),
272 item.size_bytes,
273 item.remote_path
274 );
275
276 Ok(())
277 }
278
279 async fn download_file(&self, item: &SyncableItem) -> Result<()> {
281 tracing::info!(
282 "Downloading {} to {}",
283 item.remote_path,
284 item.local_path.display()
285 );
286
287 if let Some(parent) = item.local_path.parent() {
289 fs::create_dir_all(parent).await?;
290 }
291
292 let downloaded_content = match self.config.provider {
294 StorageProvider::AWS => self.download_from_aws(&item.remote_path).await?,
295 StorageProvider::Azure => self.download_from_azure(&item.remote_path).await?,
296 StorageProvider::GoogleCloud => self.download_from_gcp(&item.remote_path).await?,
297 StorageProvider::MinIO | StorageProvider::S3Compatible => {
298 self.download_from_s3_compatible(&item.remote_path).await?
299 }
300 };
301
302 let decrypted_content = if self.config.encryption_enabled {
304 self.decrypt_data(&downloaded_content).await?
305 } else {
306 downloaded_content
307 };
308
309 let final_content = if self.config.compression_enabled {
311 self.decompress_data(&decrypted_content).await?
312 } else {
313 decrypted_content
314 };
315
316 let file_checksum = calculate_file_checksum(&final_content);
318 if file_checksum != item.checksum {
319 return Err(anyhow::anyhow!("File checksum mismatch during download"));
320 }
321
322 fs::write(&item.local_path, &final_content).await?;
324
325 let metadata = fs::metadata(&item.local_path).await?;
327 if metadata.len() != item.size_bytes {
328 return Err(anyhow::anyhow!("Downloaded file size mismatch"));
329 }
330
331 tracing::info!(
332 "Successfully downloaded {} ({} bytes) to {}",
333 item.remote_path,
334 item.size_bytes,
335 item.local_path.display()
336 );
337
338 Ok(())
339 }
340
341 async fn should_upload(&self, item: &SyncableItem) -> Result<bool> {
343 if !item.local_path.exists() {
345 return Ok(false);
346 }
347
348 let metadata = fs::metadata(&item.local_path).await?;
349 let last_modified = metadata
350 .modified()?
351 .duration_since(std::time::UNIX_EPOCH)?
352 .as_secs();
353
354 Ok(last_modified > self.sync_manifest.last_sync_timestamp)
356 }
357
358 async fn should_download(&self, item: &SyncableItem) -> Result<bool> {
360 Ok(!item.local_path.exists())
363 }
364
365 async fn determine_sync_direction(&self, item: &SyncableItem) -> Result<Option<SyncDirection>> {
367 if !item.local_path.exists() {
368 return Ok(Some(SyncDirection::Download));
369 }
370
371 let metadata = fs::metadata(&item.local_path).await?;
374 let last_modified = metadata
375 .modified()?
376 .duration_since(std::time::UNIX_EPOCH)?
377 .as_secs();
378
379 if last_modified > self.sync_manifest.last_sync_timestamp {
380 Ok(Some(SyncDirection::Upload))
381 } else {
382 Ok(None) }
384 }
385
386 async fn calculate_file_checksum(&self, path: &Path) -> Result<String> {
388 let content = fs::read(path).await?;
389 let mut hasher = Sha256::new();
390 hasher.update(&content);
391 let result = hasher.finalize();
392 Ok(format!("{:x}", result))
393 }
394
395 async fn save_manifest(&self) -> Result<()> {
397 let manifest_path = self.local_cache_dir.join("sync_manifest.json");
398 let content = serde_json::to_string_pretty(&self.sync_manifest)?;
399 fs::write(manifest_path, content).await?;
400 Ok(())
401 }
402
403 pub async fn get_storage_stats(&self) -> Result<StorageStats> {
405 let total_size: u64 = self
406 .sync_manifest
407 .items
408 .iter()
409 .map(|item| item.size_bytes)
410 .sum();
411
412 let local_files = self
413 .sync_manifest
414 .items
415 .iter()
416 .filter(|item| item.local_path.exists())
417 .count();
418
419 Ok(StorageStats {
420 total_files: self.sync_manifest.items.len(),
421 local_files,
422 total_size_bytes: total_size,
423 last_sync_timestamp: self.sync_manifest.last_sync_timestamp,
424 cache_directory: self.local_cache_dir.clone(),
425 })
426 }
427
428 pub async fn cleanup_cache(&mut self, max_age_days: u32) -> Result<CleanupResult> {
430 let cutoff_time = std::time::SystemTime::now()
431 .duration_since(std::time::UNIX_EPOCH)?
432 .as_secs()
433 - (max_age_days as u64 * 24 * 60 * 60);
434
435 let mut removed_files = 0;
436 let mut freed_bytes = 0u64;
437 let mut errors = Vec::new();
438
439 self.sync_manifest.items.retain(|item| {
441 if item.last_modified < cutoff_time {
442 if item.local_path.exists() {
443 match std::fs::remove_file(&item.local_path) {
444 Ok(_) => {
445 removed_files += 1;
446 freed_bytes += item.size_bytes;
447 }
448 Err(e) => {
449 errors.push(format!(
450 "Failed to remove {}: {}",
451 item.local_path.display(),
452 e
453 ));
454 }
455 }
456 }
457 false } else {
459 true }
461 });
462
463 self.save_manifest().await?;
464
465 Ok(CleanupResult {
466 removed_files,
467 freed_bytes,
468 errors,
469 })
470 }
471
472 async fn upload_to_aws(&self, remote_path: &str, content: &[u8]) -> Result<()> {
474 self.create_aws_client().await?;
478 let bucket = &self.config.bucket_name;
479
480 tracing::debug!("Uploading to AWS S3: s3://{}/{}", bucket, remote_path);
482
483 if content.len() > 5 * 1024 * 1024 {
485 self.aws_multipart_upload(remote_path, content).await?;
486 } else {
487 self.aws_single_upload(remote_path, content).await?;
488 }
489
490 Ok(())
491 }
492
493 async fn download_from_aws(&self, remote_path: &str) -> Result<Vec<u8>> {
495 self.create_aws_client().await?;
496 let bucket = &self.config.bucket_name;
497
498 tracing::debug!("Downloading from AWS S3: s3://{}/{}", bucket, remote_path);
499
500 let content = self.aws_get_object(remote_path).await?;
502
503 Ok(content)
504 }
505
506 async fn upload_to_azure(&self, remote_path: &str, content: &[u8]) -> Result<()> {
508 self.create_azure_client().await?;
509
510 tracing::debug!("Uploading to Azure Blob Storage: {}", remote_path);
511
512 self.azure_put_blob(remote_path, content).await?;
514
515 Ok(())
516 }
517
518 async fn download_from_azure(&self, remote_path: &str) -> Result<Vec<u8>> {
520 self.create_azure_client().await?;
521
522 tracing::debug!("Downloading from Azure Blob Storage: {}", remote_path);
523
524 let content = self.azure_get_blob(remote_path).await?;
525
526 Ok(content)
527 }
528
529 async fn upload_to_gcp(&self, remote_path: &str, content: &[u8]) -> Result<()> {
531 self.create_gcp_client().await?;
532
533 tracing::debug!("Uploading to Google Cloud Storage: {}", remote_path);
534
535 self.gcp_upload_object(remote_path, content).await?;
536
537 Ok(())
538 }
539
540 async fn download_from_gcp(&self, remote_path: &str) -> Result<Vec<u8>> {
542 self.create_gcp_client().await?;
543
544 tracing::debug!("Downloading from Google Cloud Storage: {}", remote_path);
545
546 let content = self.gcp_download_object(remote_path).await?;
547
548 Ok(content)
549 }
550
551 async fn upload_to_s3_compatible(&self, remote_path: &str, content: &[u8]) -> Result<()> {
553 self.create_s3_compatible_client().await?;
554
555 tracing::debug!("Uploading to S3-compatible storage: {}", remote_path);
556
557 self.s3_compatible_put_object(remote_path, content).await?;
558
559 Ok(())
560 }
561
562 async fn download_from_s3_compatible(&self, remote_path: &str) -> Result<Vec<u8>> {
564 self.create_s3_compatible_client().await?;
565
566 tracing::debug!("Downloading from S3-compatible storage: {}", remote_path);
567
568 let content = self.s3_compatible_get_object(remote_path).await?;
569
570 Ok(content)
571 }
572
573 async fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>> {
575 use flate2::write::GzEncoder;
576 use flate2::Compression;
577 use std::io::Write;
578
579 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
580 encoder.write_all(data)?;
581 let compressed = encoder.finish()?;
582
583 tracing::debug!(
584 "Compressed {} bytes to {} bytes",
585 data.len(),
586 compressed.len()
587 );
588
589 Ok(compressed)
590 }
591
592 async fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>> {
594 use flate2::read::GzDecoder;
595 use std::io::Read;
596
597 let mut decoder = GzDecoder::new(data);
598 let mut decompressed = Vec::new();
599 decoder.read_to_end(&mut decompressed)?;
600
601 tracing::debug!(
602 "Decompressed {} bytes to {} bytes",
603 data.len(),
604 decompressed.len()
605 );
606
607 Ok(decompressed)
608 }
609
610 async fn encrypt_data(&self, data: &[u8]) -> Result<Vec<u8>> {
612 use aes_gcm::aead::rand_core::RngCore;
613
614 let key = self.get_encryption_key().await?;
616
617 let key_bytes: [u8; 32] = if key.len() >= 32 {
619 key[..32].try_into().unwrap()
620 } else {
621 let mut hasher = Sha256::new();
623 hasher.update(&key);
624 hasher.finalize().into()
625 };
626
627 let cipher = Aes256Gcm::new(&key_bytes.into());
629
630 let mut nonce_bytes = [0u8; 12];
632 OsRng.fill_bytes(&mut nonce_bytes);
633 let nonce = Nonce::from_slice(&nonce_bytes);
634
635 let ciphertext = cipher
637 .encrypt(nonce, data)
638 .map_err(|e| anyhow::anyhow!("AES-GCM encryption failed: {}", e))?;
639
640 let mut encrypted = Vec::with_capacity(12 + ciphertext.len());
642 encrypted.extend_from_slice(&nonce_bytes);
643 encrypted.extend_from_slice(&ciphertext);
644
645 tracing::debug!(
646 "Encrypted {} bytes to {} bytes (including nonce and tag)",
647 data.len(),
648 encrypted.len()
649 );
650
651 Ok(encrypted)
652 }
653
654 async fn decrypt_data(&self, data: &[u8]) -> Result<Vec<u8>> {
656 if data.len() < 28 {
658 anyhow::bail!(
659 "Invalid encrypted data: too short (need at least 28 bytes, got {})",
660 data.len()
661 );
662 }
663
664 let key = self.get_encryption_key().await?;
666
667 let key_bytes: [u8; 32] = if key.len() >= 32 {
669 key[..32].try_into().unwrap()
670 } else {
671 let mut hasher = Sha256::new();
673 hasher.update(&key);
674 hasher.finalize().into()
675 };
676
677 let cipher = Aes256Gcm::new(&key_bytes.into());
679
680 let nonce = Nonce::from_slice(&data[..12]);
682
683 let ciphertext = &data[12..];
685
686 let plaintext = cipher
688 .decrypt(nonce, ciphertext)
689 .map_err(|e| anyhow::anyhow!("AES-GCM decryption failed: {}", e))?;
690
691 tracing::debug!(
692 "Decrypted {} bytes to {} bytes",
693 data.len(),
694 plaintext.len()
695 );
696
697 Ok(plaintext)
698 }
699
700 async fn get_encryption_key(&self) -> Result<Vec<u8>> {
702 if let Ok(key_str) = std::env::var("VOIRS_ENCRYPTION_KEY") {
710 if key_str.len() >= 32 {
711 tracing::debug!("Using encryption key from VOIRS_ENCRYPTION_KEY environment");
712 return Ok(key_str.as_bytes().to_vec());
713 } else {
714 tracing::warn!(
715 "VOIRS_ENCRYPTION_KEY is too short ({} bytes), deriving with SHA-256",
716 key_str.len()
717 );
718 let mut hasher = Sha256::new();
719 hasher.update(key_str.as_bytes());
720 return Ok(hasher.finalize().to_vec());
721 }
722 }
723
724 if let Ok(key_str) = std::env::var("VOIRS_CONFIG_ENCRYPTION_KEY") {
726 tracing::debug!("Using encryption key from config file");
727 let mut hasher = Sha256::new();
728 hasher.update(key_str.as_bytes());
729 return Ok(hasher.finalize().to_vec());
730 }
731
732 let key_material = format!(
736 "{:?}:{}:{}",
737 self.config.provider,
738 self.config.bucket_name,
739 self.config
740 .access_key
741 .as_ref()
742 .unwrap_or(&"voirs-default".to_string())
743 );
744
745 tracing::warn!(
746 "No explicit encryption key configured, deriving from credentials (secure but requires consistent config)"
747 );
748
749 let mut hasher = Sha256::new();
750 hasher.update(key_material.as_bytes());
751 hasher.update(b"voirs-cloud-storage-encryption-v1");
753
754 Ok(hasher.finalize().to_vec())
755 }
756
757 async fn create_aws_client(&self) -> Result<()> {
759 tracing::debug!("Created AWS S3 client");
762 Ok(())
763 }
764
765 async fn create_azure_client(&self) -> Result<()> {
766 tracing::debug!("Created Azure Blob Storage client");
768 Ok(())
769 }
770
771 async fn create_gcp_client(&self) -> Result<()> {
772 tracing::debug!("Created Google Cloud Storage client");
774 Ok(())
775 }
776
777 async fn create_s3_compatible_client(&self) -> Result<()> {
778 tracing::debug!("Created S3-compatible client");
780 Ok(())
781 }
782
783 async fn aws_multipart_upload(&self, remote_path: &str, content: &[u8]) -> Result<()> {
785 tracing::debug!("AWS multipart upload for {}", remote_path);
786 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
788 Ok(())
789 }
790
791 async fn aws_single_upload(&self, remote_path: &str, content: &[u8]) -> Result<()> {
792 tracing::debug!("AWS single upload for {}", remote_path);
793 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
795 Ok(())
796 }
797
798 async fn aws_get_object(&self, remote_path: &str) -> Result<Vec<u8>> {
799 tracing::debug!("AWS get object for {}", remote_path);
800 Ok(format!("AWS content for {}", remote_path).into_bytes())
802 }
803
804 async fn azure_put_blob(&self, remote_path: &str, content: &[u8]) -> Result<()> {
806 tracing::debug!("Azure put blob for {}", remote_path);
807 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
808 Ok(())
809 }
810
811 async fn azure_get_blob(&self, remote_path: &str) -> Result<Vec<u8>> {
812 tracing::debug!("Azure get blob for {}", remote_path);
813 Ok(format!("Azure content for {}", remote_path).into_bytes())
814 }
815
816 async fn gcp_upload_object(&self, remote_path: &str, content: &[u8]) -> Result<()> {
818 tracing::debug!("GCP upload object for {}", remote_path);
819 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
820 Ok(())
821 }
822
823 async fn gcp_download_object(&self, remote_path: &str) -> Result<Vec<u8>> {
824 tracing::debug!("GCP download object for {}", remote_path);
825 Ok(format!("GCP content for {}", remote_path).into_bytes())
826 }
827
828 async fn s3_compatible_put_object(&self, remote_path: &str, content: &[u8]) -> Result<()> {
830 tracing::debug!("S3-compatible put object for {}", remote_path);
831 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
832 Ok(())
833 }
834
835 async fn s3_compatible_get_object(&self, remote_path: &str) -> Result<Vec<u8>> {
836 tracing::debug!("S3-compatible get object for {}", remote_path);
837 Ok(format!("S3-compatible content for {}", remote_path).into_bytes())
838 }
839}
840
841fn calculate_file_checksum(data: &[u8]) -> String {
843 let mut hasher = Sha256::new();
844 hasher.update(data);
845 let result = hasher.finalize();
846 format!("{:x}", result)
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
850pub struct SyncResult {
851 pub uploaded_files: u32,
852 pub downloaded_files: u32,
853 pub skipped_files: u32,
854 pub failed_uploads: u32,
855 pub failed_downloads: u32,
856 pub errors: Vec<String>,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct StorageStats {
861 pub total_files: usize,
862 pub local_files: usize,
863 pub total_size_bytes: u64,
864 pub last_sync_timestamp: u64,
865 pub cache_directory: PathBuf,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
869pub struct CleanupResult {
870 pub removed_files: u32,
871 pub freed_bytes: u64,
872 pub errors: Vec<String>,
873}
874
875impl SyncManifest {
876 fn new() -> Self {
877 Self {
878 version: 1,
879 last_sync_timestamp: 0,
880 items: Vec::new(),
881 total_size_bytes: 0,
882 checksum: String::new(),
883 }
884 }
885}
886
887impl SyncResult {
888 fn new() -> Self {
889 Self {
890 uploaded_files: 0,
891 downloaded_files: 0,
892 skipped_files: 0,
893 failed_uploads: 0,
894 failed_downloads: 0,
895 errors: Vec::new(),
896 }
897 }
898}
899
900impl Default for CloudStorageConfig {
901 fn default() -> Self {
902 Self {
903 provider: StorageProvider::S3Compatible,
904 bucket_name: "voirs-models".to_string(),
905 region: "us-west-1".to_string(),
906 access_key: None,
907 secret_key: None,
908 endpoint: None,
909 encryption_enabled: true,
910 compression_enabled: true,
911 sync_interval_seconds: 3600, }
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919 use tempfile::TempDir;
920
921 #[tokio::test]
922 async fn test_storage_manager_creation() {
923 let temp_dir = TempDir::new().unwrap();
924 let config = CloudStorageConfig::default();
925
926 let manager = CloudStorageManager::new(config, temp_dir.path().to_path_buf());
927 assert!(manager.is_ok());
928 }
929
930 #[tokio::test]
931 async fn test_add_to_sync() {
932 let temp_dir = TempDir::new().unwrap();
933 let config = CloudStorageConfig::default();
934 let mut manager = CloudStorageManager::new(config, temp_dir.path().to_path_buf()).unwrap();
935
936 let test_file = temp_dir.path().join("test.txt");
938 fs::write(&test_file, "test content").await.unwrap();
939
940 let result = manager
941 .add_to_sync(
942 test_file,
943 "remote/test.txt".to_string(),
944 SyncDirection::Upload,
945 )
946 .await;
947
948 assert!(result.is_ok());
949 assert_eq!(manager.sync_manifest.items.len(), 1);
950 }
951
952 #[tokio::test]
953 async fn test_storage_stats() {
954 let temp_dir = TempDir::new().unwrap();
955 let config = CloudStorageConfig::default();
956 let manager = CloudStorageManager::new(config, temp_dir.path().to_path_buf()).unwrap();
957
958 let stats = manager.get_storage_stats().await;
959 assert!(stats.is_ok());
960
961 let stats = stats.unwrap();
962 assert_eq!(stats.total_files, 0);
963 assert_eq!(stats.local_files, 0);
964 }
965
966 #[test]
967 fn test_sync_direction_serialization() {
968 let direction = SyncDirection::Bidirectional;
969 let serialized = serde_json::to_string(&direction);
970 assert!(serialized.is_ok());
971
972 let deserialized: Result<SyncDirection, _> = serde_json::from_str(&serialized.unwrap());
973 assert!(deserialized.is_ok());
974 }
975}