use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum StorageBackend {
#[default]
Local,
S3,
MinIO,
AzureBlob,
GCS,
}
impl std::fmt::Display for StorageBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Local => write!(f, "local"),
Self::S3 => write!(f, "s3"),
Self::MinIO => write!(f, "minio"),
Self::AzureBlob => write!(f, "azure_blob"),
Self::GCS => write!(f, "gcs"),
}
}
}
impl std::str::FromStr for StorageBackend {
type Err = crate::error::StorageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"s3" | "aws" => Ok(Self::S3),
"minio" => Ok(Self::MinIO),
"azure_blob" | "azure" => Ok(Self::AzureBlob),
"gcs" | "google" => Ok(Self::GCS),
_ => Err(crate::error::StorageError::InvalidConfiguration(
format!("Invalid storage backend: {}", s)
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub backend: StorageBackend,
pub bucket: String,
pub base_path: Option<String>,
pub multipart_threshold: u64,
pub chunk_size: usize,
pub versioning: bool,
pub compression: bool,
pub encryption: Option<EncryptionConfig>,
#[cfg(feature = "compression")]
pub compression_config: Option<crate::CompressionConfig>,
pub retention: Option<RetentionConfig>,
pub access_control: Option<AccessControlConfig>,
pub backend_config: HashMap<String, serde_json::Value>,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: StorageBackend::Local,
bucket: "default".to_string(),
base_path: None,
multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
chunk_size: crate::DEFAULT_CHUNK_SIZE,
versioning: false,
compression: false,
encryption: None,
#[cfg(feature = "compression")]
compression_config: None,
retention: None,
access_control: None,
backend_config: HashMap::new(),
}
}
}
impl StorageConfig {
pub fn from_env() -> crate::StorageResult<Self> {
let backend_str = std::env::var("STORAGE_BACKEND")
.unwrap_or_else(|_| "local".to_string());
let backend = backend_str.parse()?;
let mut config = Self {
backend,
bucket: std::env::var("STORAGE_BUCKET")
.unwrap_or_else(|_| "default".to_string()),
base_path: std::env::var("STORAGE_BASE_PATH").ok(),
..Default::default()
};
if config.backend == StorageBackend::Local {
config.backend_config.insert("base_dir".to_string(),
serde_json::Value::String(
std::env::var("LOCAL_STORAGE_DIR").unwrap_or_else(|_| "./storage".to_string())
)
);
}
Ok(config)
}
pub fn into_local_config(self) -> Option<LocalStorageConfig> {
if self.backend != StorageBackend::Local {
return None;
}
Some(LocalStorageConfig {
base_dir: self.backend_config.get("base_dir")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "./storage".to_string()),
base_path: self.base_path,
compression: self.compression,
encryption: self.encryption,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalStorageConfig {
pub base_dir: String,
pub base_path: Option<String>,
pub compression: bool,
pub encryption: Option<EncryptionConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S3StorageConfig {
pub region: String,
pub access_key_id: String,
pub secret_access_key: String,
pub bucket: String,
pub endpoint: Option<String>,
pub base_path: Option<String>,
pub multipart_threshold: u64,
pub chunk_size: usize,
pub encryption: Option<EncryptionConfig>,
pub path_style: bool,
pub versioning: bool,
}
impl Default for S3StorageConfig {
fn default() -> Self {
Self {
region: "us-east-1".to_string(),
access_key_id: String::new(),
secret_access_key: String::new(),
bucket: String::new(),
endpoint: None,
base_path: None,
multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
chunk_size: crate::DEFAULT_CHUNK_SIZE,
encryption: None,
path_style: false,
versioning: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinIOStorageConfig {
pub endpoint: String,
pub access_key: String,
pub secret_key: String,
pub bucket: String,
pub region: String,
pub base_path: Option<String>,
pub use_ssl: bool,
pub multipart_threshold: u64,
pub chunk_size: usize,
pub encryption: Option<EncryptionConfig>,
pub versioning: bool,
}
impl Default for MinIOStorageConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:9000".to_string(),
access_key: String::new(),
secret_key: String::new(),
bucket: String::new(),
region: "us-east-1".to_string(),
base_path: None,
use_ssl: false,
multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
chunk_size: crate::DEFAULT_CHUNK_SIZE,
encryption: None,
versioning: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GcsStorageConfig {
pub project_id: String,
pub bucket: String,
pub credentials_json: Option<String>,
pub base_path: Option<String>,
}
impl Default for GcsStorageConfig {
fn default() -> Self {
Self {
project_id: String::new(),
bucket: String::new(),
credentials_json: None,
base_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionConfig {
pub algorithm: EncryptionAlgorithm,
pub key: Option<String>,
pub server_side: bool,
pub parameters: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageCompressionConfig {
pub enabled: bool,
pub quality: u8,
pub max_width: Option<u32>,
pub max_height: Option<u32>,
pub max_file_size: Option<u64>,
pub preferred_formats: Vec<String>,
pub preserve_metadata: bool,
pub progressive_jpeg: bool,
pub webp_method: u8,
}
impl Default for ImageCompressionConfig {
fn default() -> Self {
Self {
enabled: true,
quality: 80, max_width: Some(2048),
max_height: Some(2048),
max_file_size: Some(2 * 1024 * 1024), preferred_formats: vec!["webp".to_string(), "jpeg".to_string()],
preserve_metadata: false,
progressive_jpeg: true,
webp_method: 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EncryptionAlgorithm {
AES256GCM,
AES256CBC,
ChaCha20Poly1305,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetentionConfig {
pub mode: RetentionMode,
pub period_days: u32,
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RetentionMode {
KeepForDays,
DeleteAfterDays,
GlacierAfterDays,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessControlConfig {
pub default_policy: AccessPolicy,
pub cors: Option<CorsConfig>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessPolicy {
Private,
PublicRead,
PublicReadWrite,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorsConfig {
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<String>,
pub allowed_headers: Vec<String>,
pub max_age: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageFile {
pub id: String,
pub name: String,
pub size: u64,
pub content_type: String,
pub checksum: Option<String>,
pub checksum_algorithm: Option<String>,
pub metadata: HashMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
pub version: Option<String>,
pub backend: StorageBackend,
pub bucket: String,
pub storage_path: String,
pub encrypted: bool,
pub compressed: bool,
pub expires_at: Option<DateTime<Utc>>,
}
impl StorageFile {
pub fn new(
name: String,
size: u64,
content_type: String,
bucket: String,
storage_path: String,
backend: StorageBackend,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
size,
content_type,
checksum: None,
checksum_algorithm: None,
metadata: HashMap::new(),
created_at: Utc::now(),
updated_at: None,
version: None,
backend,
bucket,
storage_path,
encrypted: false,
compressed: false,
expires_at: None,
}
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
pub fn with_checksum(mut self, checksum: String, algorithm: String) -> Self {
self.checksum = Some(checksum);
self.checksum_algorithm = Some(algorithm);
self
}
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
pub fn with_encryption(mut self, encrypted: bool) -> Self {
self.encrypted = encrypted;
self
}
pub fn with_compression(mut self, compressed: bool) -> Self {
self.compressed = compressed;
self
}
pub fn with_expiration(mut self, expires_at: DateTime<Utc>) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn with_created_at(mut self, created_at: DateTime<Utc>) -> Self {
self.created_at = created_at;
self
}
pub fn with_updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
self.updated_at = Some(updated_at);
self
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Utc::now() > expires_at
} else {
false
}
}
pub fn extension(&self) -> Option<&str> {
std::path::Path::new(&self.name)
.extension()
.and_then(|ext| ext.to_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct UploadOptions {
pub content_type: Option<String>,
pub metadata: HashMap<String, String>,
pub multipart: Option<bool>,
pub chunk_size: Option<usize>,
pub calculate_checksum: bool,
pub checksum_algorithm: Option<String>,
pub encrypt: bool,
pub compress: bool,
pub retry_config: Option<RetryConfig>,
}
#[derive(Debug, Clone, Default)]
pub struct DownloadOptions {
pub range: Option<ByteRange>,
pub verify_checksum: bool,
pub expected_checksum: Option<String>,
pub retry_config: Option<RetryConfig>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteRange {
pub start: u64,
pub end: Option<u64>,
}
impl ByteRange {
pub fn new(start: u64, end: Option<u64>) -> Self {
Self { start, end }
}
pub fn from_start(start: u64) -> Self {
Self { start, end: None }
}
pub fn last_bytes(bytes: u64) -> Self {
Self {
start: 0,
end: Some(bytes - 1)
}
}
pub fn to_header_value(&self) -> String {
match self.end {
Some(end) => format!("bytes={}-{}", self.start, end),
None => format!("bytes={}-", self.start),
}
}
}
#[derive(Debug, Clone)]
pub struct PresignedUrlOptions {
pub expiry_seconds: u64,
pub method: HttpMethod,
pub headers: HashMap<String, String>,
pub query_params: HashMap<String, String>,
}
impl Default for PresignedUrlOptions {
fn default() -> Self {
Self {
expiry_seconds: crate::DEFAULT_PRESIGNED_EXPIRY,
method: HttpMethod::GET,
headers: HashMap::new(),
query_params: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
GET,
PUT,
DELETE,
HEAD,
}
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GET => write!(f, "GET"),
Self::PUT => write!(f, "PUT"),
Self::DELETE => write!(f, "DELETE"),
Self::HEAD => write!(f, "HEAD"),
}
}
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay_ms: u64,
pub backoff_multiplier: f64,
pub max_delay_ms: u64,
pub retry_on_error: Option<Vec<String>>,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay_ms: 1000,
backoff_multiplier: 2.0,
max_delay_ms: 30000,
retry_on_error: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub size: u64,
pub last_modified: DateTime<Utc>,
pub content_type: String,
pub etag: Option<String>,
pub storage_class: Option<String>,
pub custom_metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageStats {
pub file_count: u64,
pub total_bytes: u64,
pub backend_usage: HashMap<String, u64>,
pub content_type_counts: HashMap<String, u64>,
pub average_file_size: f64,
pub largest_file_size: u64,
pub last_updated: DateTime<Utc>,
}