use crate::{UtilsError, UtilsResult};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub struct CloudStorageConfig {
pub provider: CloudProvider,
pub endpoint: Option<String>,
pub region: Option<String>,
pub access_key: Option<String>,
pub secret_key: Option<String>,
pub bucket: String,
pub timeout_seconds: Option<u64>,
pub use_ssl: bool,
pub custom_headers: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CloudProvider {
AWS,
GoogleCloud,
Azure,
MinIO,
Custom(String),
}
impl fmt::Display for CloudProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CloudProvider::AWS => write!(f, "aws"),
CloudProvider::GoogleCloud => write!(f, "gcp"),
CloudProvider::Azure => write!(f, "azure"),
CloudProvider::MinIO => write!(f, "minio"),
CloudProvider::Custom(name) => write!(f, "{name}"),
}
}
}
impl Default for CloudStorageConfig {
fn default() -> Self {
Self {
provider: CloudProvider::AWS,
endpoint: None,
region: Some("us-east-1".to_string()),
access_key: None,
secret_key: None,
bucket: String::new(),
timeout_seconds: Some(30),
use_ssl: true,
custom_headers: HashMap::new(),
}
}
}
pub trait CloudStorageClient {
fn upload(&self, key: &str, data: &[u8]) -> UtilsResult<String>;
fn download(&self, key: &str) -> UtilsResult<Vec<u8>>;
fn delete(&self, key: &str) -> UtilsResult<()>;
fn list_objects(&self, prefix: &str) -> UtilsResult<Vec<String>>;
fn exists(&self, key: &str) -> UtilsResult<bool>;
fn get_metadata(&self, key: &str) -> UtilsResult<ObjectMetadata>;
fn upload_file(&self, key: &str, local_path: &str) -> UtilsResult<String>;
fn download_file(&self, key: &str, local_path: &str) -> UtilsResult<()>;
}
#[derive(Debug, Clone)]
pub struct ObjectMetadata {
pub size: u64,
pub etag: Option<String>,
pub content_type: Option<String>,
pub last_modified: Option<String>,
pub custom_metadata: HashMap<String, String>,
}
pub struct MockCloudStorageClient {
storage: std::sync::Arc<std::sync::Mutex<HashMap<String, Vec<u8>>>>,
metadata: std::sync::Arc<std::sync::Mutex<HashMap<String, ObjectMetadata>>>,
}
impl Default for MockCloudStorageClient {
fn default() -> Self {
Self::new()
}
}
impl MockCloudStorageClient {
pub fn new() -> Self {
Self {
storage: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
metadata: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
}
}
}
impl CloudStorageClient for MockCloudStorageClient {
fn upload(&self, key: &str, data: &[u8]) -> UtilsResult<String> {
let mut storage = self.storage.lock().expect("operation should succeed");
let mut metadata = self.metadata.lock().expect("operation should succeed");
storage.insert(key.to_string(), data.to_vec());
metadata.insert(
key.to_string(),
ObjectMetadata {
size: data.len() as u64,
etag: Some(format!("mock-etag-{key}")),
content_type: Some("application/octet-stream".to_string()),
last_modified: Some(chrono::Utc::now().to_rfc3339()),
custom_metadata: HashMap::new(),
},
);
Ok(format!("mock://bucket/{key}"))
}
fn download(&self, key: &str) -> UtilsResult<Vec<u8>> {
let storage = self.storage.lock().expect("operation should succeed");
storage
.get(key)
.cloned()
.ok_or_else(|| UtilsError::InvalidParameter(format!("Object not found: {key}")))
}
fn delete(&self, key: &str) -> UtilsResult<()> {
let mut storage = self.storage.lock().expect("operation should succeed");
let mut metadata = self.metadata.lock().expect("operation should succeed");
storage.remove(key);
metadata.remove(key);
Ok(())
}
fn list_objects(&self, prefix: &str) -> UtilsResult<Vec<String>> {
let storage = self.storage.lock().expect("operation should succeed");
let objects: Vec<String> = storage
.keys()
.filter(|key| key.starts_with(prefix))
.cloned()
.collect();
Ok(objects)
}
fn exists(&self, key: &str) -> UtilsResult<bool> {
let storage = self.storage.lock().expect("operation should succeed");
Ok(storage.contains_key(key))
}
fn get_metadata(&self, key: &str) -> UtilsResult<ObjectMetadata> {
let metadata = self.metadata.lock().expect("operation should succeed");
metadata
.get(key)
.cloned()
.ok_or_else(|| UtilsError::InvalidParameter(format!("Object not found: {key}")))
}
fn upload_file(&self, key: &str, local_path: &str) -> UtilsResult<String> {
let data = std::fs::read(local_path)
.map_err(|e| UtilsError::InvalidParameter(format!("Failed to read file: {e}")))?;
self.upload(key, &data)
}
fn download_file(&self, key: &str, local_path: &str) -> UtilsResult<()> {
let data = self.download(key)?;
std::fs::write(local_path, data)
.map_err(|e| UtilsError::InvalidParameter(format!("Failed to write file: {e}")))?;
Ok(())
}
}
pub struct CloudStorageFactory;
impl CloudStorageFactory {
pub fn create_client(config: &CloudStorageConfig) -> UtilsResult<Box<dyn CloudStorageClient>> {
match config.provider {
CloudProvider::AWS => {
Ok(Box::new(MockCloudStorageClient::new()))
}
CloudProvider::GoogleCloud => {
Ok(Box::new(MockCloudStorageClient::new()))
}
CloudProvider::Azure => {
Ok(Box::new(MockCloudStorageClient::new()))
}
CloudProvider::MinIO => {
Ok(Box::new(MockCloudStorageClient::new()))
}
CloudProvider::Custom(_) => {
Ok(Box::new(MockCloudStorageClient::new()))
}
}
}
}
pub struct CloudStorageUtils;
impl CloudStorageUtils {
pub fn upload_dataset(
client: &dyn CloudStorageClient,
dataset_path: &str,
key_prefix: &str,
) -> UtilsResult<Vec<String>> {
let mut uploaded_keys = Vec::new();
let entries = std::fs::read_dir(dataset_path)
.map_err(|e| UtilsError::InvalidParameter(format!("Failed to read directory: {e}")))?;
for entry in entries {
let entry = entry
.map_err(|e| UtilsError::InvalidParameter(format!("Failed to read entry: {e}")))?;
let path = entry.path();
if path.is_file() {
let filename = path
.file_name()
.expect("operation should succeed")
.to_str()
.expect("operation should succeed");
let key = format!("{key_prefix}/{filename}");
let local_path = path.to_str().expect("operation should succeed");
client.upload_file(&key, local_path)?;
uploaded_keys.push(key);
}
}
Ok(uploaded_keys)
}
pub fn download_dataset(
client: &dyn CloudStorageClient,
key_prefix: &str,
local_path: &str,
) -> UtilsResult<Vec<String>> {
let objects = client.list_objects(key_prefix)?;
let mut downloaded_files = Vec::new();
std::fs::create_dir_all(local_path).map_err(|e| {
UtilsError::InvalidParameter(format!("Failed to create directory: {e}"))
})?;
for object_key in objects {
let filename = object_key.split('/').next_back().unwrap_or(&object_key);
let local_file_path = format!("{local_path}/{filename}");
client.download_file(&object_key, &local_file_path)?;
downloaded_files.push(local_file_path);
}
Ok(downloaded_files)
}
pub fn sync_dataset(
client: &dyn CloudStorageClient,
local_path: &str,
key_prefix: &str,
sync_mode: SyncMode,
) -> UtilsResult<SyncResult> {
let mut result = SyncResult::default();
match sync_mode {
SyncMode::Upload => {
let uploaded = Self::upload_dataset(client, local_path, key_prefix)?;
result.uploaded = uploaded;
}
SyncMode::Download => {
let downloaded = Self::download_dataset(client, key_prefix, local_path)?;
result.downloaded = downloaded;
}
SyncMode::Bidirectional => {
let uploaded = Self::upload_dataset(client, local_path, key_prefix)?;
let downloaded = Self::download_dataset(client, key_prefix, local_path)?;
result.uploaded = uploaded;
result.downloaded = downloaded;
}
}
Ok(result)
}
pub fn batch_upload(
client: &dyn CloudStorageClient,
files: &[(String, String)], ) -> UtilsResult<Vec<String>> {
let mut uploaded_keys = Vec::new();
for (local_path, key) in files {
let result = client.upload_file(key, local_path)?;
uploaded_keys.push(result);
}
Ok(uploaded_keys)
}
pub fn calculate_storage_metrics(
client: &dyn CloudStorageClient,
key_prefix: &str,
) -> UtilsResult<StorageMetrics> {
let objects = client.list_objects(key_prefix)?;
let mut total_size = 0;
let mut total_objects = 0;
let mut file_types = HashMap::new();
for object_key in objects {
if let Ok(metadata) = client.get_metadata(&object_key) {
total_size += metadata.size;
total_objects += 1;
if let Some(ext) = object_key.split('.').next_back() {
*file_types.entry(ext.to_string()).or_insert(0) += 1;
}
}
}
Ok(StorageMetrics {
total_size_bytes: total_size,
total_objects,
file_types,
average_file_size: total_size.checked_div(total_objects).unwrap_or(0),
})
}
}
#[derive(Debug, Clone)]
pub enum SyncMode {
Upload,
Download,
Bidirectional,
}
#[derive(Debug, Clone, Default)]
pub struct SyncResult {
pub uploaded: Vec<String>,
pub downloaded: Vec<String>,
pub errors: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct StorageMetrics {
pub total_size_bytes: u64,
pub total_objects: u64,
pub file_types: HashMap<String, usize>,
pub average_file_size: u64,
}
impl fmt::Display for StorageMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Storage Metrics:")?;
writeln!(
f,
" Total Size: {:.2} MB",
self.total_size_bytes as f64 / 1024.0 / 1024.0
)?;
writeln!(f, " Total Objects: {}", self.total_objects)?;
writeln!(
f,
" Average File Size: {:.2} KB",
self.average_file_size as f64 / 1024.0
)?;
writeln!(f, " File Types:")?;
for (ext, count) in &self.file_types {
writeln!(f, " .{ext}: {count}")?;
}
Ok(())
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_cloud_storage_config() {
let config = CloudStorageConfig {
provider: CloudProvider::AWS,
bucket: "test-bucket".to_string(),
..Default::default()
};
assert_eq!(config.provider, CloudProvider::AWS);
assert_eq!(config.bucket, "test-bucket");
assert_eq!(config.region, Some("us-east-1".to_string()));
assert!(config.use_ssl);
}
#[test]
fn test_cloud_provider_display() {
assert_eq!(CloudProvider::AWS.to_string(), "aws");
assert_eq!(CloudProvider::GoogleCloud.to_string(), "gcp");
assert_eq!(CloudProvider::Azure.to_string(), "azure");
assert_eq!(CloudProvider::MinIO.to_string(), "minio");
assert_eq!(
CloudProvider::Custom("test".to_string()).to_string(),
"test"
);
}
#[test]
fn test_mock_client_upload_download() {
let client = MockCloudStorageClient::new();
let test_data = b"hello world";
let url = client
.upload("test-key", test_data)
.expect("operation should succeed");
assert_eq!(url, "mock://bucket/test-key");
let downloaded = client
.download("test-key")
.expect("operation should succeed");
assert_eq!(downloaded, test_data);
assert!(client.exists("test-key").expect("operation should succeed"));
assert!(!client
.exists("nonexistent-key")
.expect("operation should succeed"));
}
#[test]
fn test_mock_client_metadata() {
let client = MockCloudStorageClient::new();
let test_data = b"hello world";
client
.upload("test-key", test_data)
.expect("operation should succeed");
let metadata = client
.get_metadata("test-key")
.expect("operation should succeed");
assert_eq!(metadata.size, test_data.len() as u64);
assert_eq!(metadata.etag, Some("mock-etag-test-key".to_string()));
assert_eq!(
metadata.content_type,
Some("application/octet-stream".to_string())
);
}
#[test]
fn test_mock_client_list_objects() {
let client = MockCloudStorageClient::new();
client
.upload("data/file1.txt", b"content1")
.expect("operation should succeed");
client
.upload("data/file2.txt", b"content2")
.expect("operation should succeed");
client
.upload("other/file3.txt", b"content3")
.expect("operation should succeed");
let objects = client
.list_objects("data/")
.expect("operation should succeed");
assert_eq!(objects.len(), 2);
assert!(objects.contains(&"data/file1.txt".to_string()));
assert!(objects.contains(&"data/file2.txt".to_string()));
}
#[test]
fn test_mock_client_delete() {
let client = MockCloudStorageClient::new();
client
.upload("test-key", b"hello")
.expect("operation should succeed");
assert!(client.exists("test-key").expect("operation should succeed"));
client.delete("test-key").expect("operation should succeed");
assert!(!client.exists("test-key").expect("operation should succeed"));
}
#[test]
fn test_cloud_storage_factory() {
let config = CloudStorageConfig {
provider: CloudProvider::AWS,
bucket: "test-bucket".to_string(),
..Default::default()
};
let client = CloudStorageFactory::create_client(&config).expect("operation should succeed");
client
.upload("test", b"data")
.expect("operation should succeed");
let downloaded = client.download("test").expect("operation should succeed");
assert_eq!(downloaded, b"data");
}
#[test]
fn test_storage_metrics_display() {
let mut file_types = HashMap::new();
file_types.insert("txt".to_string(), 5);
file_types.insert("csv".to_string(), 3);
let metrics = StorageMetrics {
total_size_bytes: 1_048_576, total_objects: 8,
file_types,
average_file_size: 131_072, };
let display = metrics.to_string();
assert!(display.contains("Total Size: 1.00 MB"));
assert!(display.contains("Total Objects: 8"));
assert!(display.contains("Average File Size: 128.00 KB"));
assert!(display.contains(".txt: 5"));
assert!(display.contains(".csv: 3"));
}
#[test]
fn test_sync_result_default() {
let result = SyncResult::default();
assert!(result.uploaded.is_empty());
assert!(result.downloaded.is_empty());
assert!(result.errors.is_empty());
}
#[test]
fn test_file_upload_download() {
let client = MockCloudStorageClient::new();
let temp_dir = tempfile::tempdir().expect("operation should succeed");
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, b"test content").expect("operation should succeed");
let url = client
.upload_file(
"test.txt",
file_path.to_str().expect("operation should succeed"),
)
.expect("operation should succeed");
assert_eq!(url, "mock://bucket/test.txt");
let download_path = temp_dir.path().join("downloaded.txt");
client
.download_file(
"test.txt",
download_path.to_str().expect("operation should succeed"),
)
.expect("operation should succeed");
let downloaded_content = fs::read(&download_path).expect("operation should succeed");
assert_eq!(downloaded_content, b"test content");
}
#[test]
fn test_calculate_storage_metrics() {
let client = MockCloudStorageClient::new();
client
.upload("data/file1.txt", b"hello")
.expect("operation should succeed");
client
.upload("data/file2.csv", b"world")
.expect("operation should succeed");
client
.upload("data/file3.txt", b"test")
.expect("operation should succeed");
let metrics = CloudStorageUtils::calculate_storage_metrics(&client, "data/")
.expect("operation should succeed");
assert_eq!(metrics.total_objects, 3);
assert_eq!(metrics.total_size_bytes, 14); assert_eq!(metrics.file_types.get("txt"), Some(&2));
assert_eq!(metrics.file_types.get("csv"), Some(&1));
}
#[test]
fn test_batch_upload() {
let client = MockCloudStorageClient::new();
let temp_dir = tempfile::tempdir().expect("operation should succeed");
let file1_path = temp_dir.path().join("file1.txt");
let file2_path = temp_dir.path().join("file2.txt");
fs::write(&file1_path, b"content1").expect("operation should succeed");
fs::write(&file2_path, b"content2").expect("operation should succeed");
let files = vec![
(
file1_path
.to_str()
.expect("operation should succeed")
.to_string(),
"batch/file1.txt".to_string(),
),
(
file2_path
.to_str()
.expect("operation should succeed")
.to_string(),
"batch/file2.txt".to_string(),
),
];
let results =
CloudStorageUtils::batch_upload(&client, &files).expect("operation should succeed");
assert_eq!(results.len(), 2);
assert_eq!(results[0], "mock://bucket/batch/file1.txt");
assert_eq!(results[1], "mock://bucket/batch/file2.txt");
let content1 = client
.download("batch/file1.txt")
.expect("operation should succeed");
let content2 = client
.download("batch/file2.txt")
.expect("operation should succeed");
assert_eq!(content1, b"content1");
assert_eq!(content2, b"content2");
}
}