1use crate::error::{IoError, Result};
8use std::collections::HashMap;
9use std::path::Path;
10use std::time::{Duration, SystemTime};
11
12#[derive(Debug, Clone)]
14pub struct FileMetadata {
15 pub name: String,
17 pub size: u64,
19 pub last_modified: SystemTime,
21 pub content_type: Option<String>,
23 pub etag: Option<String>,
25 pub metadata: HashMap<String, String>,
27}
28
29#[derive(Debug, Clone)]
31pub struct S3Config {
32 pub bucket: String,
34 pub region: String,
36 pub access_key: String,
38 pub secret_key: String,
40 pub endpoint: Option<String>,
42 pub path_style: bool,
44}
45
46impl S3Config {
47 pub fn new(bucket: &str, region: &str, access_key: &str, secret_key: &str) -> Self {
49 Self {
50 bucket: bucket.to_string(),
51 region: region.to_string(),
52 access_key: access_key.to_string(),
53 secret_key: secret_key.to_string(),
54 endpoint: None,
55 path_style: false,
56 }
57 }
58
59 pub fn with_endpoint(mut self, endpoint: &str) -> Self {
61 self.endpoint = Some(endpoint.to_string());
62 self
63 }
64
65 pub fn with_path_style(mut self, pathstyle: bool) -> Self {
67 self.path_style = pathstyle;
68 self
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct GcsConfig {
75 pub bucket: String,
77 pub project_id: String,
79 pub credentials_path: Option<String>,
81 pub credentials_json: Option<String>,
83}
84
85impl GcsConfig {
86 pub fn new(bucket: &str, project_id: &str) -> Self {
88 Self {
89 bucket: bucket.to_string(),
90 project_id: project_id.to_string(),
91 credentials_path: None,
92 credentials_json: None,
93 }
94 }
95
96 pub fn with_credentials_file(mut self, path: &str) -> Self {
98 self.credentials_path = Some(path.to_string());
99 self
100 }
101
102 pub fn with_credentials_json(mut self, json: &str) -> Self {
104 self.credentials_json = Some(json.to_string());
105 self
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct AzureConfig {
112 pub account: String,
114 pub container: String,
116 pub access_key: String,
118 pub endpoint: Option<String>,
120}
121
122impl AzureConfig {
123 pub fn new(account: &str, container: &str, access_key: &str) -> Self {
125 Self {
126 account: account.to_string(),
127 container: container.to_string(),
128 access_key: access_key.to_string(),
129 endpoint: None,
130 }
131 }
132
133 pub fn with_endpoint(mut self, endpoint: &str) -> Self {
135 self.endpoint = Some(endpoint.to_string());
136 self
137 }
138}
139
140#[derive(Debug, Clone)]
142pub enum CloudProvider {
143 S3(S3Config),
145 GCS(GcsConfig),
147 Azure(AzureConfig),
149}
150
151impl CloudProvider {
152 pub async fn upload_file<P: AsRef<Path>>(
154 &self,
155 local_path: P,
156 remote_path: &str,
157 ) -> Result<()> {
158 match self {
159 CloudProvider::S3(config) => self.s3_upload(config, local_path, remote_path).await,
160 CloudProvider::GCS(config) => self.gcs_upload(config, local_path, remote_path).await,
161 CloudProvider::Azure(config) => {
162 self.azure_upload(config, local_path, remote_path).await
163 }
164 }
165 }
166
167 pub async fn download_file<P: AsRef<Path>>(
169 &self,
170 remote_path: &str,
171 local_path: P,
172 ) -> Result<()> {
173 match self {
174 CloudProvider::S3(config) => self.s3_download(config, remote_path, local_path).await,
175 CloudProvider::GCS(config) => self.gcs_download(config, remote_path, local_path).await,
176 CloudProvider::Azure(config) => {
177 self.azure_download(config, remote_path, local_path).await
178 }
179 }
180 }
181
182 pub async fn list_files(&self, path: &str) -> Result<Vec<String>> {
184 match self {
185 CloudProvider::S3(config) => CloudProvider::s3_list(config, path).await,
186 CloudProvider::GCS(config) => CloudProvider::gcs_list(config, path).await,
187 CloudProvider::Azure(config) => CloudProvider::azure_list(config, path).await,
188 }
189 }
190
191 pub async fn file_exists(&self, path: &str) -> Result<bool> {
193 match self {
194 CloudProvider::S3(config) => CloudProvider::s3_exists(config, path).await,
195 CloudProvider::GCS(config) => CloudProvider::gcs_exists(config, path).await,
196 CloudProvider::Azure(config) => CloudProvider::azure_exists(config, path).await,
197 }
198 }
199
200 pub async fn get_metadata(&self, path: &str) -> Result<FileMetadata> {
202 match self {
203 CloudProvider::S3(config) => CloudProvider::s3_metadata(config, path).await,
204 CloudProvider::GCS(config) => CloudProvider::gcs_metadata(config, path).await,
205 CloudProvider::Azure(config) => CloudProvider::azure_metadata(config, path).await,
206 }
207 }
208
209 pub async fn delete_file(&self, path: &str) -> Result<()> {
211 match self {
212 CloudProvider::S3(config) => CloudProvider::s3_delete(config, path).await,
213 CloudProvider::GCS(config) => CloudProvider::gcs_delete(config, path).await,
214 CloudProvider::Azure(config) => CloudProvider::azure_delete(config, path).await,
215 }
216 }
217
218 async fn s3_upload<P: AsRef<Path>>(
220 &self,
221 _config: &S3Config,
222 _local_path: P,
223 _remote_path: &str,
224 ) -> Result<()> {
225 let _ = (_config, _local_path, _remote_path);
230 Err(IoError::Other(
231 "AWS S3 upload is not implemented: no AWS SDK client is integrated".to_string(),
232 ))
233 }
234
235 async fn s3_download<P: AsRef<Path>>(
236 &self,
237 _config: &S3Config,
238 path: &str,
239 _local_path: P,
240 ) -> Result<()> {
241 let _ = (_config, _local_path);
242 Err(IoError::Other(format!(
243 "AWS S3 download of '{path}' is not implemented: no AWS SDK client is integrated"
244 )))
245 }
246
247 async fn s3_list(_config: &S3Config, path: &str) -> Result<Vec<String>> {
248 let _ = _config;
249 Err(IoError::Other(format!(
250 "AWS S3 listing of '{path}' is not implemented: no AWS SDK client is integrated"
251 )))
252 }
253
254 async fn s3_exists(_config: &S3Config, path: &str) -> Result<bool> {
255 let _ = _config;
256 Err(IoError::Other(format!(
257 "AWS S3 existence check for '{path}' is not implemented: no AWS SDK client is integrated"
258 )))
259 }
260
261 async fn s3_metadata(_config: &S3Config, path: &str) -> Result<FileMetadata> {
262 let _ = _config;
263 Err(IoError::Other(format!(
264 "AWS S3 metadata for '{path}' is not implemented: no AWS SDK client is integrated"
265 )))
266 }
267
268 async fn s3_delete(_config: &S3Config, path: &str) -> Result<()> {
269 let _ = _config;
270 Err(IoError::Other(format!(
271 "AWS S3 deletion of '{path}' is not implemented: no AWS SDK client is integrated"
272 )))
273 }
274
275 async fn gcs_upload<P: AsRef<Path>>(
277 &self,
278 _config: &GcsConfig,
279 _local_path: P,
280 _remote_path: &str,
281 ) -> Result<()> {
282 let _ = (_config, _local_path, _remote_path);
286 Err(IoError::Other(
287 "Google Cloud Storage upload is not implemented: no GCS client is integrated"
288 .to_string(),
289 ))
290 }
291
292 async fn gcs_download<P: AsRef<Path>>(
293 &self,
294 _config: &GcsConfig,
295 path: &str,
296 _local_path: P,
297 ) -> Result<()> {
298 let _ = (_config, _local_path);
299 Err(IoError::Other(format!(
300 "Google Cloud Storage download of '{path}' is not implemented: no GCS client is integrated"
301 )))
302 }
303
304 async fn gcs_list(_config: &GcsConfig, path: &str) -> Result<Vec<String>> {
305 let _ = _config;
306 Err(IoError::Other(format!(
307 "Google Cloud Storage listing of '{path}' is not implemented: no GCS client is integrated"
308 )))
309 }
310
311 async fn gcs_exists(_config: &GcsConfig, path: &str) -> Result<bool> {
312 let _ = _config;
313 Err(IoError::Other(format!(
314 "Google Cloud Storage existence check for '{path}' is not implemented: no GCS client is integrated"
315 )))
316 }
317
318 async fn gcs_metadata(_config: &GcsConfig, path: &str) -> Result<FileMetadata> {
319 let _ = _config;
320 Err(IoError::Other(format!(
321 "Google Cloud Storage metadata for '{path}' is not implemented: no GCS client is integrated"
322 )))
323 }
324
325 async fn gcs_delete(_config: &GcsConfig, path: &str) -> Result<()> {
326 let _ = _config;
327 Err(IoError::Other(format!(
328 "Google Cloud Storage deletion of '{path}' is not implemented: no GCS client is integrated"
329 )))
330 }
331
332 async fn azure_upload<P: AsRef<Path>>(
334 &self,
335 _config: &AzureConfig,
336 _local_path: P,
337 _remote_path: &str,
338 ) -> Result<()> {
339 let _ = (_config, _local_path, _remote_path);
343 Err(IoError::Other(
344 "Azure Blob Storage upload is not implemented: no Azure client is integrated"
345 .to_string(),
346 ))
347 }
348
349 async fn azure_download<P: AsRef<Path>>(
350 &self,
351 _config: &AzureConfig,
352 path: &str,
353 _local_path: P,
354 ) -> Result<()> {
355 let _ = (_config, _local_path);
356 Err(IoError::Other(format!(
357 "Azure Blob Storage download of '{path}' is not implemented: no Azure client is integrated"
358 )))
359 }
360
361 async fn azure_list(_config: &AzureConfig, path: &str) -> Result<Vec<String>> {
362 let _ = _config;
363 Err(IoError::Other(format!(
364 "Azure Blob Storage listing of '{path}' is not implemented: no Azure client is integrated"
365 )))
366 }
367
368 async fn azure_exists(_config: &AzureConfig, path: &str) -> Result<bool> {
369 let _ = _config;
370 Err(IoError::Other(format!(
371 "Azure Blob Storage existence check for '{path}' is not implemented: no Azure client is integrated"
372 )))
373 }
374
375 async fn azure_metadata(_config: &AzureConfig, path: &str) -> Result<FileMetadata> {
376 let _ = _config;
377 Err(IoError::Other(format!(
378 "Azure Blob Storage metadata for '{path}' is not implemented: no Azure client is integrated"
379 )))
380 }
381
382 async fn azure_delete(_config: &AzureConfig, path: &str) -> Result<()> {
383 let _ = _config;
384 Err(IoError::Other(format!(
385 "Azure Blob Storage deletion of '{path}' is not implemented: no Azure client is integrated"
386 )))
387 }
388}
389
390#[allow(dead_code)]
393pub fn create_mock_metadata(name: &str, size: u64) -> FileMetadata {
394 FileMetadata {
395 name: name.to_string(),
396 size,
397 last_modified: SystemTime::now(),
398 content_type: Some("application/octet-stream".to_string()),
399 etag: Some(format!("etag-{}", name)),
400 metadata: HashMap::new(),
401 }
402}
403
404#[allow(dead_code)]
406pub fn validate_config(provider: &CloudProvider) -> Result<()> {
407 match provider {
408 CloudProvider::S3(config) => {
409 if config.bucket.is_empty() {
410 return Err(IoError::ConfigError(
411 "S3 bucket name cannot be empty".to_string(),
412 ));
413 }
414 if config.region.is_empty() {
415 return Err(IoError::ConfigError(
416 "S3 region cannot be empty".to_string(),
417 ));
418 }
419 if config.access_key.is_empty() || config.secret_key.is_empty() {
420 return Err(IoError::ConfigError(
421 "S3 credentials cannot be empty".to_string(),
422 ));
423 }
424 }
425 CloudProvider::GCS(config) => {
426 if config.bucket.is_empty() {
427 return Err(IoError::ConfigError(
428 "GCS bucket name cannot be empty".to_string(),
429 ));
430 }
431 if config.project_id.is_empty() {
432 return Err(IoError::ConfigError(
433 "GCS project ID cannot be empty".to_string(),
434 ));
435 }
436 if config.credentials_path.is_none() && config.credentials_json.is_none() {
437 return Err(IoError::ConfigError(
438 "GCS credentials must be provided".to_string(),
439 ));
440 }
441 }
442 CloudProvider::Azure(config) => {
443 if config.account.is_empty() {
444 return Err(IoError::ConfigError(
445 "Azure account name cannot be empty".to_string(),
446 ));
447 }
448 if config.container.is_empty() {
449 return Err(IoError::ConfigError(
450 "Azure container name cannot be empty".to_string(),
451 ));
452 }
453 if config.access_key.is_empty() {
454 return Err(IoError::ConfigError(
455 "Azure access key cannot be empty".to_string(),
456 ));
457 }
458 }
459 }
460 Ok(())
461}
462
463#[allow(dead_code)]
465pub fn generate_signed_url(
466 provider: &CloudProvider,
467 path: &str,
468 expiry: Duration,
469) -> Result<String> {
470 use sha2::{Digest, Sha256};
471 use std::time::{SystemTime, UNIX_EPOCH};
472
473 let timestamp = SystemTime::now()
475 .duration_since(UNIX_EPOCH)
476 .map_err(|e| IoError::Other(format!("System time error: {}", e)))?
477 .as_secs()
478 + expiry.as_secs();
479
480 let mut hasher = Sha256::new();
482 hasher.update(path.as_bytes());
483 hasher.update(timestamp.to_string().as_bytes());
484 let signature = crate::encoding_utils::hex_encode(hasher.finalize());
485 let short_sig = &signature[0..16]; let signed_url = match provider {
488 CloudProvider::S3(config) => {
489 let bucket = &config.bucket;
490 let region = &config.region;
491 format!(
492 "https://{}.s3.{}.amazonaws.com{}?X-Amz-Expires={}&X-Amz-Signature={}",
493 bucket,
494 region,
495 path,
496 expiry.as_secs(),
497 short_sig
498 )
499 }
500 CloudProvider::GCS(config) => {
501 let bucket = &config.bucket;
502 format!(
503 "https://storage.googleapis.com/{}{}?Expires={}&Signature={}",
504 bucket, path, timestamp, short_sig
505 )
506 }
507 CloudProvider::Azure(config) => {
508 let account = &config.account;
509 let container = &config.container;
510 format!(
511 "https://{}.blob.core.windows.net/{}{}?se={}&sig={}",
512 account, container, path, timestamp, short_sig
513 )
514 }
515 };
516
517 Ok(signed_url)
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_s3_config_creation() {
526 let config = S3Config::new("my-bucket", "us-east-1", "access-key", "secret-key");
527 assert_eq!(config.bucket, "my-bucket");
528 assert_eq!(config.region, "us-east-1");
529 assert_eq!(config.access_key, "access-key");
530 assert_eq!(config.secret_key, "secret-key");
531 assert!(config.endpoint.is_none());
532 assert!(!config.path_style);
533 }
534
535 #[test]
536 fn test_s3_config_with_endpoint() {
537 let config = S3Config::new("bucket", "region", "key", "secret")
538 .with_endpoint("http://localhost:9000")
539 .with_path_style(true);
540
541 assert_eq!(config.endpoint, Some("http://localhost:9000".to_string()));
542 assert!(config.path_style);
543 }
544
545 #[test]
546 fn test_gcs_config_creation() {
547 let config = GcsConfig::new("my-bucket", "my-project");
548 assert_eq!(config.bucket, "my-bucket");
549 assert_eq!(config.project_id, "my-project");
550 assert!(config.credentials_path.is_none());
551 assert!(config.credentials_json.is_none());
552 }
553
554 #[test]
555 fn test_gcs_config_with_credentials() {
556 let config = GcsConfig::new("bucket", "project")
557 .with_credentials_file("/path/to/creds.json")
558 .with_credentials_json(r#"{"type": "service_account"}"#);
559
560 assert_eq!(
561 config.credentials_path,
562 Some("/path/to/creds.json".to_string())
563 );
564 assert_eq!(
565 config.credentials_json,
566 Some(r#"{"type": "service_account"}"#.to_string())
567 );
568 }
569
570 #[test]
571 fn test_azure_config_creation() {
572 let config = AzureConfig::new("account", "container", "access-key");
573 assert_eq!(config.account, "account");
574 assert_eq!(config.container, "container");
575 assert_eq!(config.access_key, "access-key");
576 assert!(config.endpoint.is_none());
577 }
578
579 #[test]
580 fn test_azure_config_with_endpoint() {
581 let config =
582 AzureConfig::new("account", "container", "key").with_endpoint("http://localhost:10000");
583
584 assert_eq!(config.endpoint, Some("http://localhost:10000".to_string()));
585 }
586
587 #[test]
588 fn test_validate_config() {
589 let s3_config = CloudProvider::S3(S3Config::new("bucket", "region", "key", "secret"));
591 assert!(validate_config(&s3_config).is_ok());
592
593 let invalid_s3 = CloudProvider::S3(S3Config::new("", "region", "key", "secret"));
595 assert!(validate_config(&invalid_s3).is_err());
596
597 let gcs_config = CloudProvider::GCS(
599 GcsConfig::new("bucket", "project").with_credentials_file("/path/to/creds.json"),
600 );
601 assert!(validate_config(&gcs_config).is_ok());
602
603 let invalid_gcs = CloudProvider::GCS(GcsConfig::new("bucket", "project"));
605 assert!(validate_config(&invalid_gcs).is_err());
606
607 let azure_config = CloudProvider::Azure(AzureConfig::new("account", "container", "key"));
609 assert!(validate_config(&azure_config).is_ok());
610
611 let invalid_azure = CloudProvider::Azure(AzureConfig::new("", "container", "key"));
613 assert!(validate_config(&invalid_azure).is_err());
614 }
615
616 #[test]
617 fn test_file_metadata_creation() {
618 let metadata = create_mock_metadata("test-file.txt", 1024);
619 assert_eq!(metadata.name, "test-file.txt");
620 assert_eq!(metadata.size, 1024);
621 assert_eq!(
622 metadata.content_type,
623 Some("application/octet-stream".to_string())
624 );
625 assert_eq!(metadata.etag, Some("etag-test-file.txt".to_string()));
626 }
627
628 #[test]
629 fn test_signed_url_generation() {
630 let config = CloudProvider::S3(S3Config::new("bucket", "region", "key", "secret"));
631 let url = generate_signed_url(&config, "test-file.txt", Duration::from_secs(3600));
632 assert!(url.is_ok());
633 assert!(!url.expect("Operation failed").is_empty());
634 }
635
636 #[cfg(all(
637 feature = "async",
638 not(any(
639 feature = "aws-sdk-s3",
640 feature = "google-cloud-storage",
641 feature = "azure-storage-blobs"
642 ))
643 ))]
644 #[tokio::test]
645 async fn test_cloud_provider_operations_without_features() {
646 let s3_config = CloudProvider::S3(S3Config::new("bucket", "region", "key", "secret"));
647
648 let upload_result = s3_config.upload_file("local.txt", "remote.txt").await;
650 assert!(upload_result.is_err());
651
652 let download_result = s3_config.download_file("remote.txt", "local.txt").await;
653 assert!(download_result.is_err());
654
655 let list_result = s3_config.list_files("path/").await;
656 assert!(list_result.is_err());
657
658 let exists_result = s3_config.file_exists("test.txt").await;
659 assert!(exists_result.is_err());
660
661 let metadata_result = s3_config.get_metadata("test.txt").await;
662 assert!(metadata_result.is_err());
663
664 let delete_result = s3_config.delete_file("test.txt").await;
665 assert!(delete_result.is_err());
666 }
667}