elif_storage/
lib.rs

1//! # elif-storage
2//! 
3//! A comprehensive multi-backend file storage system for the elif.rs framework.
4//! 
5//! ## Features
6//! 
7//! - **Multi-backend support**: Local filesystem, AWS S3, and Google Cloud Storage
8//! - **File upload validation**: Size, type, and content validation
9//! - **Image processing**: Resize, crop, optimize, and watermark images
10//! - **CDN integration**: Signed URLs and CDN support
11//! - **Access control**: File permissions and access control
12//! - **Temporary file management**: Automatic cleanup of temporary files
13//! - **Streaming support**: Handle large files efficiently
14//! - **Async-first**: Built for modern async Rust applications
15//! 
16//! ## Quick Start
17//! 
18//! ```rust
19//! use elif_storage::{Storage, LocalBackend, LocalStorageConfig};
20//! 
21//! # tokio_test::block_on(async {
22//! // Create a local filesystem storage
23//! let config = LocalStorageConfig::default().with_root_path("./storage");
24//! let storage = Storage::new(LocalBackend::new(config));
25//! 
26//! // Store a file
27//! let file_data = b"Hello, World!";
28//! let file_info = storage.put("documents/hello.txt", file_data, None).await.unwrap();
29//! 
30//! // Retrieve a file
31//! let retrieved = storage.get("documents/hello.txt").await.unwrap();
32//! assert_eq!(retrieved.unwrap().as_ref(), file_data);
33//! # });
34//! ```
35
36use std::time::Duration;
37use std::path::PathBuf;
38use std::collections::HashMap;
39use bytes::Bytes;
40use serde::{Serialize, Deserialize};
41use async_trait::async_trait;
42use thiserror::Error;
43use chrono::{DateTime, Utc};
44use uuid::Uuid;
45
46pub mod backends;
47pub mod config;
48pub mod upload;
49pub mod validation;
50pub mod permissions;
51pub mod cleanup;
52
53#[cfg(feature = "image-processing")]
54pub mod image_processing;
55
56pub use backends::*;
57pub use config::*;
58pub use upload::*;
59pub use validation::*;
60pub use permissions::*;
61pub use cleanup::*;
62
63#[cfg(feature = "image-processing")]
64pub use image_processing::*;
65
66/// Storage operation errors
67#[derive(Error, Debug)]
68pub enum StorageError {
69    #[error("IO error: {0}")]
70    Io(#[from] std::io::Error),
71    
72    #[error("Backend error: {0}")]
73    Backend(String),
74    
75    #[error("File not found: {0}")]
76    FileNotFound(String),
77    
78    #[error("Permission denied: {0}")]
79    PermissionDenied(String),
80    
81    #[error("Validation error: {0}")]
82    Validation(String),
83    
84    #[error("Configuration error: {0}")]
85    Configuration(String),
86    
87    #[error("Network error: {0}")]
88    Network(String),
89    
90    #[error("Timeout error")]
91    Timeout,
92    
93    #[error("File too large: {0} bytes, max allowed: {1} bytes")]
94    FileTooLarge(u64, u64),
95    
96    #[error("Unsupported file type: {0}")]
97    UnsupportedFileType(String),
98    
99    #[error("Image processing error: {0}")]
100    ImageProcessing(String),
101    
102    #[error("CDN error: {0}")]
103    Cdn(String),
104}
105
106/// Result type for storage operations
107pub type StorageResult<T> = Result<T, StorageError>;
108
109/// File path type
110pub type FilePath = String;
111
112/// File metadata
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct FileMetadata {
115    /// File path/key
116    pub path: FilePath,
117    
118    /// File size in bytes
119    pub size: u64,
120    
121    /// MIME type
122    pub content_type: String,
123    
124    /// File creation timestamp
125    pub created_at: DateTime<Utc>,
126    
127    /// File modification timestamp
128    pub modified_at: DateTime<Utc>,
129    
130    /// ETag/version identifier
131    pub etag: Option<String>,
132    
133    /// Custom metadata
134    pub metadata: HashMap<String, String>,
135    
136    /// File permissions
137    #[cfg(feature = "access-control")]
138    pub permissions: Option<FilePermissions>,
139}
140
141impl FileMetadata {
142    /// Create new file metadata
143    pub fn new(path: FilePath, size: u64, content_type: String) -> Self {
144        let now = Utc::now();
145        Self {
146            path,
147            size,
148            content_type,
149            created_at: now,
150            modified_at: now,
151            etag: None,
152            metadata: HashMap::new(),
153            #[cfg(feature = "access-control")]
154            permissions: None,
155        }
156    }
157    
158    /// Set custom metadata
159    pub fn with_metadata(mut self, key: String, value: String) -> Self {
160        self.metadata.insert(key, value);
161        self
162    }
163    
164    /// Set ETag
165    pub fn with_etag(mut self, etag: String) -> Self {
166        self.etag = Some(etag);
167        self
168    }
169    
170    /// Set permissions
171    #[cfg(feature = "access-control")]
172    pub fn with_permissions(mut self, permissions: FilePermissions) -> Self {
173        self.permissions = Some(permissions);
174        self
175    }
176}
177
178/// File upload options
179#[derive(Debug, Clone)]
180pub struct UploadOptions {
181    /// Content type override
182    pub content_type: Option<String>,
183    
184    /// Custom metadata
185    pub metadata: HashMap<String, String>,
186    
187    /// File permissions
188    #[cfg(feature = "access-control")]
189    pub permissions: Option<FilePermissions>,
190    
191    /// Cache control headers
192    pub cache_control: Option<String>,
193    
194    /// Content disposition
195    pub content_disposition: Option<String>,
196    
197    /// Whether to overwrite existing file
198    pub overwrite: bool,
199}
200
201impl Default for UploadOptions {
202    fn default() -> Self {
203        Self {
204            content_type: None,
205            metadata: HashMap::new(),
206            #[cfg(feature = "access-control")]
207            permissions: None,
208            cache_control: None,
209            content_disposition: None,
210            overwrite: false,
211        }
212    }
213}
214
215impl UploadOptions {
216    /// Create new upload options
217    pub fn new() -> Self {
218        Self::default()
219    }
220    
221    /// Set content type
222    pub fn content_type(mut self, content_type: String) -> Self {
223        self.content_type = Some(content_type);
224        self
225    }
226    
227    /// Add metadata
228    pub fn metadata(mut self, key: String, value: String) -> Self {
229        self.metadata.insert(key, value);
230        self
231    }
232    
233    /// Set permissions
234    #[cfg(feature = "access-control")]
235    pub fn permissions(mut self, permissions: FilePermissions) -> Self {
236        self.permissions = Some(permissions);
237        self
238    }
239    
240    /// Set cache control
241    pub fn cache_control(mut self, cache_control: String) -> Self {
242        self.cache_control = Some(cache_control);
243        self
244    }
245    
246    /// Set content disposition
247    pub fn content_disposition(mut self, content_disposition: String) -> Self {
248        self.content_disposition = Some(content_disposition);
249        self
250    }
251    
252    /// Allow overwriting existing files
253    pub fn overwrite(mut self) -> Self {
254        self.overwrite = true;
255        self
256    }
257}
258
259/// Storage statistics
260#[derive(Debug, Clone, Default)]
261pub struct StorageStats {
262    pub total_files: u64,
263    pub total_size: u64,
264    pub available_space: Option<u64>,
265    pub used_space: Option<u64>,
266}
267
268/// Core storage backend trait that all storage implementations must implement
269#[async_trait]
270pub trait StorageBackend: Send + Sync {
271    /// Store a file
272    async fn put(&self, path: &str, data: &[u8], options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
273    
274    /// Store a file from a stream (for large files)
275    async fn put_stream<S>(&self, path: &str, stream: S, options: Option<UploadOptions>) -> StorageResult<FileMetadata>
276    where
277        S: futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin;
278    
279    /// Retrieve a file
280    async fn get(&self, path: &str) -> StorageResult<Option<Bytes>>;
281    
282    /// Get a file as a stream (for large files)
283    async fn get_stream(&self, path: &str) -> StorageResult<Option<Box<dyn futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin>>>;
284    
285    /// Check if a file exists
286    async fn exists(&self, path: &str) -> StorageResult<bool>;
287    
288    /// Get file metadata
289    async fn metadata(&self, path: &str) -> StorageResult<Option<FileMetadata>>;
290    
291    /// Delete a file
292    async fn delete(&self, path: &str) -> StorageResult<bool>;
293    
294    /// List files in a directory/prefix
295    async fn list(&self, prefix: Option<&str>, limit: Option<u32>) -> StorageResult<Vec<FileMetadata>>;
296    
297    /// Copy a file
298    async fn copy(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
299    
300    /// Move/rename a file
301    async fn move_file(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
302    
303    /// Generate a signed URL (if supported)
304    async fn signed_url(&self, path: &str, expires_in: Duration) -> StorageResult<String>;
305    
306    /// Generate a public URL (if supported)
307    async fn public_url(&self, path: &str) -> StorageResult<String>;
308    
309    /// Get storage statistics
310    async fn stats(&self) -> StorageResult<StorageStats> {
311        Ok(StorageStats::default())
312    }
313    
314    /// Delete multiple files
315    async fn delete_many(&self, paths: &[&str]) -> StorageResult<Vec<String>> {
316        let mut deleted = Vec::new();
317        for path in paths {
318            if self.delete(path).await? {
319                deleted.push(path.to_string());
320            }
321        }
322        Ok(deleted)
323    }
324}
325
326/// High-level storage interface with additional features
327pub struct Storage<B: StorageBackend> {
328    backend: B,
329    config: StorageConfig,
330}
331
332impl<B: StorageBackend> Storage<B> {
333    /// Create a new storage instance
334    pub fn new(backend: B) -> Self {
335        Self {
336            backend,
337            config: StorageConfig::default(),
338        }
339    }
340    
341    /// Create a storage instance with custom configuration
342    pub fn with_config(backend: B, config: StorageConfig) -> Self {
343        Self { backend, config }
344    }
345    
346    /// Store a file with validation
347    pub async fn put(&self, path: &str, data: &[u8], options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
348        // Validate file
349        if let Err(e) = validate_file_size(data.len() as u64, self.config.max_file_size) {
350            return Err(e);
351        }
352        
353        let content_type = options.as_ref()
354            .and_then(|o| o.content_type.clone())
355            .unwrap_or_else(|| detect_content_type(path, data));
356            
357        if let Err(e) = validate_file_type(&content_type, &self.config.allowed_types) {
358            return Err(e);
359        }
360        
361        // Store the file
362        self.backend.put(path, data, options).await
363    }
364    
365    /// Store a file from a stream with validation
366    pub async fn put_stream<S>(&self, path: &str, stream: S, options: Option<UploadOptions>) -> StorageResult<FileMetadata>
367    where
368        S: futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin,
369    {
370        self.backend.put_stream(path, stream, options).await
371    }
372    
373    /// Retrieve a file
374    pub async fn get(&self, path: &str) -> StorageResult<Option<Bytes>> {
375        #[cfg(feature = "access-control")]
376        if let Some(permissions) = self.get_file_permissions(path).await? {
377            // Check access permissions here
378            // This would integrate with the auth system
379        }
380        
381        self.backend.get(path).await
382    }
383    
384    /// Get a file as a stream
385    pub async fn get_stream(&self, path: &str) -> StorageResult<Option<Box<dyn futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin>>> {
386        self.backend.get_stream(path).await
387    }
388    
389    /// Check if file exists
390    pub async fn exists(&self, path: &str) -> StorageResult<bool> {
391        self.backend.exists(path).await
392    }
393    
394    /// Get file metadata
395    pub async fn metadata(&self, path: &str) -> StorageResult<Option<FileMetadata>> {
396        self.backend.metadata(path).await
397    }
398    
399    /// Delete a file
400    pub async fn delete(&self, path: &str) -> StorageResult<bool> {
401        self.backend.delete(path).await
402    }
403    
404    /// List files
405    pub async fn list(&self, prefix: Option<&str>, limit: Option<u32>) -> StorageResult<Vec<FileMetadata>> {
406        self.backend.list(prefix, limit).await
407    }
408    
409    /// Copy a file
410    pub async fn copy(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
411        self.backend.copy(from, to, options).await
412    }
413    
414    /// Move a file
415    pub async fn move_file(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
416        self.backend.move_file(from, to, options).await
417    }
418    
419    /// Generate signed URL
420    pub async fn signed_url(&self, path: &str, expires_in: Duration) -> StorageResult<String> {
421        self.backend.signed_url(path, expires_in).await
422    }
423    
424    /// Generate public URL
425    pub async fn public_url(&self, path: &str) -> StorageResult<String> {
426        self.backend.public_url(path).await
427    }
428    
429    /// Get storage statistics
430    pub async fn stats(&self) -> StorageResult<StorageStats> {
431        self.backend.stats().await
432    }
433    
434    /// Delete multiple files
435    pub async fn delete_many(&self, paths: &[&str]) -> StorageResult<Vec<String>> {
436        self.backend.delete_many(paths).await
437    }
438    
439    #[cfg(feature = "access-control")]
440    async fn get_file_permissions(&self, _path: &str) -> StorageResult<Option<FilePermissions>> {
441        // This would check permissions from the metadata or a separate permissions system
442        Ok(None)
443    }
444}
445
446/// Helper function to detect content type from path and data
447fn detect_content_type(path: &str, data: &[u8]) -> String {
448    // Try to detect from file extension first
449    if let Some(ext) = std::path::Path::new(path).extension().and_then(|e| e.to_str()) {
450        match ext.to_lowercase().as_str() {
451            "jpg" | "jpeg" => return "image/jpeg".to_string(),
452            "png" => return "image/png".to_string(),
453            "gif" => return "image/gif".to_string(),
454            "webp" => return "image/webp".to_string(),
455            "pdf" => return "application/pdf".to_string(),
456            "txt" => return "text/plain".to_string(),
457            "json" => return "application/json".to_string(),
458            "xml" => return "application/xml".to_string(),
459            "html" => return "text/html".to_string(),
460            "css" => return "text/css".to_string(),
461            "js" => return "application/javascript".to_string(),
462            _ => {}
463        }
464    }
465    
466    // Try to detect from content (simple magic number detection)
467    if data.len() >= 4 {
468        match &data[..4] {
469            [0xFF, 0xD8, 0xFF, _] => return "image/jpeg".to_string(),
470            [0x89, 0x50, 0x4E, 0x47] => return "image/png".to_string(),
471            [0x47, 0x49, 0x46, 0x38] => return "image/gif".to_string(),
472            [0x52, 0x49, 0x46, 0x46] if data.len() >= 12 && &data[8..12] == b"WEBP" => return "image/webp".to_string(),
473            [0x25, 0x50, 0x44, 0x46] => return "application/pdf".to_string(),
474            _ => {}
475        }
476    }
477    
478    "application/octet-stream".to_string()
479}
480
481/// Validate file size
482fn validate_file_size(size: u64, max_size: Option<u64>) -> StorageResult<()> {
483    if let Some(max) = max_size {
484        if size > max {
485            return Err(StorageError::FileTooLarge(size, max));
486        }
487    }
488    Ok(())
489}
490
491/// Validate file type
492fn validate_file_type(content_type: &str, allowed_types: &Option<Vec<String>>) -> StorageResult<()> {
493    if let Some(allowed) = allowed_types {
494        if !allowed.iter().any(|t| content_type.starts_with(t)) {
495            return Err(StorageError::UnsupportedFileType(content_type.to_string()));
496        }
497    }
498    Ok(())
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    
505    #[test]
506    fn test_detect_content_type() {
507        // Test extension detection
508        assert_eq!(detect_content_type("test.jpg", &[]), "image/jpeg");
509        assert_eq!(detect_content_type("test.png", &[]), "image/png");
510        assert_eq!(detect_content_type("test.pdf", &[]), "application/pdf");
511        
512        // Test magic number detection
513        let jpeg_data = [0xFF, 0xD8, 0xFF, 0xE0];
514        assert_eq!(detect_content_type("unknown", &jpeg_data), "image/jpeg");
515        
516        let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
517        assert_eq!(detect_content_type("unknown", &png_data), "image/png");
518        
519        // Test fallback
520        assert_eq!(detect_content_type("unknown", &[0x00, 0x01, 0x02, 0x03]), "application/octet-stream");
521    }
522    
523    #[test]
524    fn test_validate_file_size() {
525        assert!(validate_file_size(1000, Some(2000)).is_ok());
526        assert!(validate_file_size(2000, Some(2000)).is_ok());
527        assert!(validate_file_size(3000, Some(2000)).is_err());
528        assert!(validate_file_size(1000, None).is_ok());
529    }
530    
531    #[test]
532    fn test_validate_file_type() {
533        let allowed = Some(vec!["image/".to_string(), "text/plain".to_string()]);
534        
535        assert!(validate_file_type("image/jpeg", &allowed).is_ok());
536        assert!(validate_file_type("image/png", &allowed).is_ok());
537        assert!(validate_file_type("text/plain", &allowed).is_ok());
538        assert!(validate_file_type("application/pdf", &allowed).is_err());
539        assert!(validate_file_type("application/pdf", &None).is_ok());
540    }
541}