1use serde::{Serialize, Deserialize};
4use std::collections::HashMap;
5use chrono::{DateTime, Utc};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum StorageBackend {
11 #[default]
13 Local,
14 S3,
16 MinIO,
18 AzureBlob,
20 GCS,
22}
23
24impl std::fmt::Display for StorageBackend {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::Local => write!(f, "local"),
28 Self::S3 => write!(f, "s3"),
29 Self::MinIO => write!(f, "minio"),
30 Self::AzureBlob => write!(f, "azure_blob"),
31 Self::GCS => write!(f, "gcs"),
32 }
33 }
34}
35
36impl std::str::FromStr for StorageBackend {
37 type Err = crate::error::StorageError;
38
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 match s.to_lowercase().as_str() {
41 "local" => Ok(Self::Local),
42 "s3" | "aws" => Ok(Self::S3),
43 "minio" => Ok(Self::MinIO),
44 "azure_blob" | "azure" => Ok(Self::AzureBlob),
45 "gcs" | "google" => Ok(Self::GCS),
46 _ => Err(crate::error::StorageError::InvalidConfiguration(
47 format!("Invalid storage backend: {}", s)
48 )),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct StorageConfig {
56 pub backend: StorageBackend,
58
59 pub bucket: String,
61
62 pub base_path: Option<String>,
64
65 pub multipart_threshold: u64,
67
68 pub chunk_size: usize,
70
71 pub versioning: bool,
73
74 pub compression: bool,
76
77 pub encryption: Option<EncryptionConfig>,
79
80 #[cfg(feature = "compression")]
82 pub compression_config: Option<crate::CompressionConfig>,
83
84 pub retention: Option<RetentionConfig>,
86
87 pub access_control: Option<AccessControlConfig>,
89
90 pub backend_config: HashMap<String, serde_json::Value>,
92}
93
94impl Default for StorageConfig {
95 fn default() -> Self {
96 Self {
97 backend: StorageBackend::Local,
98 bucket: "default".to_string(),
99 base_path: None,
100 multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
101 chunk_size: crate::DEFAULT_CHUNK_SIZE,
102 versioning: false,
103 compression: false,
104 encryption: None,
105 #[cfg(feature = "compression")]
106 compression_config: None,
107 retention: None,
108 access_control: None,
109 backend_config: HashMap::new(),
110 }
111 }
112}
113
114impl StorageConfig {
115 pub fn from_env() -> crate::StorageResult<Self> {
117 let backend_str = std::env::var("STORAGE_BACKEND")
118 .unwrap_or_else(|_| "local".to_string());
119 let backend = backend_str.parse()?;
120
121 let mut config = Self {
122 backend,
123 bucket: std::env::var("STORAGE_BUCKET")
124 .unwrap_or_else(|_| "default".to_string()),
125 base_path: std::env::var("STORAGE_BASE_PATH").ok(),
126 ..Default::default()
127 };
128
129 if config.backend == StorageBackend::Local {
131 config.backend_config.insert("base_dir".to_string(),
132 serde_json::Value::String(
133 std::env::var("LOCAL_STORAGE_DIR").unwrap_or_else(|_| "./storage".to_string())
134 )
135 );
136 }
137
138 Ok(config)
139 }
140
141
142 pub fn into_local_config(self) -> Option<LocalStorageConfig> {
144 if self.backend != StorageBackend::Local {
145 return None;
146 }
147
148 Some(LocalStorageConfig {
149 base_dir: self.backend_config.get("base_dir")
150 .and_then(|v| v.as_str())
151 .map(|s| s.to_string())
152 .unwrap_or_else(|| "./storage".to_string()),
153 base_path: self.base_path,
154 compression: self.compression,
155 encryption: self.encryption,
156 })
157 }
158}
159
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct LocalStorageConfig {
164 pub base_dir: String,
165 pub base_path: Option<String>,
166 pub compression: bool,
167 pub encryption: Option<EncryptionConfig>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct S3StorageConfig {
173 pub region: String,
175 pub access_key_id: String,
177 pub secret_access_key: String,
179 pub bucket: String,
181 pub endpoint: Option<String>,
183 pub base_path: Option<String>,
185 pub multipart_threshold: u64,
187 pub chunk_size: usize,
189 pub encryption: Option<EncryptionConfig>,
191 pub path_style: bool,
193 pub versioning: bool,
195}
196
197impl Default for S3StorageConfig {
198 fn default() -> Self {
199 Self {
200 region: "us-east-1".to_string(),
201 access_key_id: String::new(),
202 secret_access_key: String::new(),
203 bucket: String::new(),
204 endpoint: None,
205 base_path: None,
206 multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
207 chunk_size: crate::DEFAULT_CHUNK_SIZE,
208 encryption: None,
209 path_style: false,
210 versioning: false,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct MinIOStorageConfig {
218 pub endpoint: String,
220 pub access_key: String,
222 pub secret_key: String,
224 pub bucket: String,
226 pub region: String,
228 pub base_path: Option<String>,
230 pub use_ssl: bool,
232 pub multipart_threshold: u64,
234 pub chunk_size: usize,
236 pub encryption: Option<EncryptionConfig>,
238 pub versioning: bool,
240}
241
242impl Default for MinIOStorageConfig {
243 fn default() -> Self {
244 Self {
245 endpoint: "http://localhost:9000".to_string(),
246 access_key: String::new(),
247 secret_key: String::new(),
248 bucket: String::new(),
249 region: "us-east-1".to_string(),
250 base_path: None,
251 use_ssl: false,
252 multipart_threshold: crate::DEFAULT_MULTIPART_THRESHOLD,
253 chunk_size: crate::DEFAULT_CHUNK_SIZE,
254 encryption: None,
255 versioning: false,
256 }
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct GcsStorageConfig {
263 pub project_id: String,
265 pub bucket: String,
267 pub credentials_json: Option<String>,
269 pub base_path: Option<String>,
271}
272
273impl Default for GcsStorageConfig {
274 fn default() -> Self {
275 Self {
276 project_id: String::new(),
277 bucket: String::new(),
278 credentials_json: None,
279 base_path: None,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct EncryptionConfig {
287 pub algorithm: EncryptionAlgorithm,
289
290 pub key: Option<String>,
292
293 pub server_side: bool,
295
296 pub parameters: HashMap<String, String>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct ImageCompressionConfig {
303 pub enabled: bool,
305
306 pub quality: u8, pub max_width: Option<u32>,
311
312 pub max_height: Option<u32>,
314
315 pub max_file_size: Option<u64>,
317
318 pub preferred_formats: Vec<String>,
320
321 pub preserve_metadata: bool,
323
324 pub progressive_jpeg: bool,
326
327 pub webp_method: u8,
329}
330
331impl Default for ImageCompressionConfig {
332 fn default() -> Self {
333 Self {
334 enabled: true,
335 quality: 80, max_width: Some(2048),
337 max_height: Some(2048),
338 max_file_size: Some(2 * 1024 * 1024), preferred_formats: vec!["webp".to_string(), "jpeg".to_string()],
340 preserve_metadata: false,
341 progressive_jpeg: true,
342 webp_method: 4,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
349pub enum EncryptionAlgorithm {
350 AES256GCM,
352 AES256CBC,
354 ChaCha20Poly1305,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct RetentionConfig {
361 pub mode: RetentionMode,
363
364 pub period_days: u32,
366
367 pub patterns: Vec<String>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373pub enum RetentionMode {
374 KeepForDays,
376 DeleteAfterDays,
378 GlacierAfterDays,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct AccessControlConfig {
385 pub default_policy: AccessPolicy,
387
388 pub cors: Option<CorsConfig>,
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
394pub enum AccessPolicy {
395 Private,
397 PublicRead,
399 PublicReadWrite,
401 Custom,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct CorsConfig {
408 pub allowed_origins: Vec<String>,
410
411 pub allowed_methods: Vec<String>,
413
414 pub allowed_headers: Vec<String>,
416
417 pub max_age: Option<u32>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct StorageFile {
424 pub id: String,
426
427 pub name: String,
429
430 pub size: u64,
432
433 pub content_type: String,
435
436 pub checksum: Option<String>,
438
439 pub checksum_algorithm: Option<String>,
441
442 pub metadata: HashMap<String, String>,
444
445 pub created_at: DateTime<Utc>,
447
448 pub updated_at: Option<DateTime<Utc>>,
450
451 pub version: Option<String>,
453
454 pub backend: StorageBackend,
456
457 pub bucket: String,
459
460 pub storage_path: String,
462
463 pub encrypted: bool,
465
466 pub compressed: bool,
468
469 pub expires_at: Option<DateTime<Utc>>,
471}
472
473impl StorageFile {
474 pub fn new(
476 name: String,
477 size: u64,
478 content_type: String,
479 bucket: String,
480 storage_path: String,
481 backend: StorageBackend,
482 ) -> Self {
483 Self {
484 id: Uuid::new_v4().to_string(),
485 name,
486 size,
487 content_type,
488 checksum: None,
489 checksum_algorithm: None,
490 metadata: HashMap::new(),
491 created_at: Utc::now(),
492 updated_at: None,
493 version: None,
494 backend,
495 bucket,
496 storage_path,
497 encrypted: false,
498 compressed: false,
499 expires_at: None,
500 }
501 }
502
503 pub fn with_metadata(mut self, key: String, value: String) -> Self {
505 self.metadata.insert(key, value);
506 self
507 }
508
509 pub fn with_checksum(mut self, checksum: String, algorithm: String) -> Self {
511 self.checksum = Some(checksum);
512 self.checksum_algorithm = Some(algorithm);
513 self
514 }
515
516 pub fn with_version(mut self, version: String) -> Self {
518 self.version = Some(version);
519 self
520 }
521
522 pub fn with_encryption(mut self, encrypted: bool) -> Self {
524 self.encrypted = encrypted;
525 self
526 }
527
528 pub fn with_compression(mut self, compressed: bool) -> Self {
530 self.compressed = compressed;
531 self
532 }
533
534 pub fn with_expiration(mut self, expires_at: DateTime<Utc>) -> Self {
536 self.expires_at = Some(expires_at);
537 self
538 }
539
540 pub fn with_created_at(mut self, created_at: DateTime<Utc>) -> Self {
542 self.created_at = created_at;
543 self
544 }
545
546 pub fn with_updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
548 self.updated_at = Some(updated_at);
549 self
550 }
551
552 pub fn is_expired(&self) -> bool {
554 if let Some(expires_at) = self.expires_at {
555 Utc::now() > expires_at
556 } else {
557 false
558 }
559 }
560
561 pub fn extension(&self) -> Option<&str> {
563 std::path::Path::new(&self.name)
564 .extension()
565 .and_then(|ext| ext.to_str())
566 }
567}
568
569#[derive(Debug, Clone, Default)]
571pub struct UploadOptions {
572 pub content_type: Option<String>,
574
575 pub metadata: HashMap<String, String>,
577
578 pub multipart: Option<bool>,
580
581 pub chunk_size: Option<usize>,
583
584 pub calculate_checksum: bool,
586
587 pub checksum_algorithm: Option<String>,
589
590 pub encrypt: bool,
592
593 pub compress: bool,
595
596 pub retry_config: Option<RetryConfig>,
601}
602
603#[derive(Debug, Clone, Default)]
605pub struct DownloadOptions {
606 pub range: Option<ByteRange>,
608
609 pub verify_checksum: bool,
611
612 pub expected_checksum: Option<String>,
614
615 pub retry_config: Option<RetryConfig>,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub struct ByteRange {
625 pub start: u64,
627
628 pub end: Option<u64>,
630}
631
632impl ByteRange {
633 pub fn new(start: u64, end: Option<u64>) -> Self {
635 Self { start, end }
636 }
637
638 pub fn from_start(start: u64) -> Self {
640 Self { start, end: None }
641 }
642
643 pub fn last_bytes(bytes: u64) -> Self {
645 Self {
646 start: 0,
647 end: Some(bytes - 1)
648 }
649 }
650
651 pub fn to_header_value(&self) -> String {
653 match self.end {
654 Some(end) => format!("bytes={}-{}", self.start, end),
655 None => format!("bytes={}-", self.start),
656 }
657 }
658}
659
660#[derive(Debug, Clone)]
662pub struct PresignedUrlOptions {
663 pub expiry_seconds: u64,
665
666 pub method: HttpMethod,
668
669 pub headers: HashMap<String, String>,
671
672 pub query_params: HashMap<String, String>,
674}
675
676impl Default for PresignedUrlOptions {
677 fn default() -> Self {
678 Self {
679 expiry_seconds: crate::DEFAULT_PRESIGNED_EXPIRY,
680 method: HttpMethod::GET,
681 headers: HashMap::new(),
682 query_params: HashMap::new(),
683 }
684 }
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq)]
689pub enum HttpMethod {
690 GET,
691 PUT,
692 DELETE,
693 HEAD,
694}
695
696impl std::fmt::Display for HttpMethod {
697 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698 match self {
699 Self::GET => write!(f, "GET"),
700 Self::PUT => write!(f, "PUT"),
701 Self::DELETE => write!(f, "DELETE"),
702 Self::HEAD => write!(f, "HEAD"),
703 }
704 }
705}
706
707#[derive(Debug, Clone)]
709pub struct RetryConfig {
710 pub max_attempts: u32,
712
713 pub initial_delay_ms: u64,
715
716 pub backoff_multiplier: f64,
718
719 pub max_delay_ms: u64,
721
722 pub retry_on_error: Option<Vec<String>>,
724}
725
726impl Default for RetryConfig {
727 fn default() -> Self {
728 Self {
729 max_attempts: 3,
730 initial_delay_ms: 1000,
731 backoff_multiplier: 2.0,
732 max_delay_ms: 30000,
733 retry_on_error: None,
734 }
735 }
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct FileMetadata {
741 pub size: u64,
743
744 pub last_modified: DateTime<Utc>,
746
747 pub content_type: String,
749
750 pub etag: Option<String>,
752
753 pub storage_class: Option<String>,
755
756 pub custom_metadata: HashMap<String, String>,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct StorageStats {
763 pub file_count: u64,
765
766 pub total_bytes: u64,
768
769 pub backend_usage: HashMap<String, u64>,
771
772 pub content_type_counts: HashMap<String, u64>,
774
775 pub average_file_size: f64,
777
778 pub largest_file_size: u64,
780
781 pub last_updated: DateTime<Utc>,
783}