Skip to main content

rust_serv/file_upload/
config.rs

1//! Upload configuration
2
3use std::path::PathBuf;
4
5/// Upload configuration
6#[derive(Debug, Clone)]
7pub struct UploadConfig {
8    /// Upload directory (where files will be stored)
9    pub upload_dir: PathBuf,
10    /// Maximum file size in bytes (default: 100MB)
11    pub max_file_size: usize,
12    /// Allowed file extensions (empty = all allowed)
13    pub allowed_extensions: Vec<String>,
14    /// Overwrite existing files
15    pub overwrite: bool,
16    /// Generate unique filenames
17    pub unique_names: bool,
18}
19
20impl UploadConfig {
21    /// Create a new upload config
22    pub fn new(upload_dir: impl Into<PathBuf>) -> Self {
23        Self {
24            upload_dir: upload_dir.into(),
25            max_file_size: 100 * 1024 * 1024, // 100MB
26            allowed_extensions: vec![],
27            overwrite: false,
28            unique_names: false,
29        }
30    }
31
32    /// Set max file size
33    pub fn with_max_size(mut self, size: usize) -> Self {
34        self.max_file_size = size;
35        self
36    }
37
38    /// Set allowed extensions
39    pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
40        self.allowed_extensions = extensions;
41        self
42    }
43
44    /// Set overwrite mode
45    pub fn with_overwrite(mut self, overwrite: bool) -> Self {
46        self.overwrite = overwrite;
47        self
48    }
49
50    /// Set unique names mode
51    pub fn with_unique_names(mut self, unique: bool) -> Self {
52        self.unique_names = unique;
53        self
54    }
55
56    /// Check if file extension is allowed
57    pub fn is_extension_allowed(&self, filename: &str) -> bool {
58        if self.allowed_extensions.is_empty() {
59            return true;
60        }
61        
62        let ext = std::path::Path::new(filename)
63            .extension()
64            .and_then(|e| e.to_str())
65            .map(|e| e.to_lowercase());
66        
67        match ext {
68            Some(e) => self.allowed_extensions.iter().any(|a| a.to_lowercase() == e),
69            None => false,
70        }
71    }
72
73    /// Check if file size is within limit
74    pub fn is_size_allowed(&self, size: usize) -> bool {
75        size <= self.max_file_size
76    }
77
78    /// Generate unique filename
79    pub fn generate_unique_filename(&self, original: &str) -> String {
80        if !self.unique_names {
81            return original.to_string();
82        }
83        
84        use std::time::{SystemTime, UNIX_EPOCH};
85        let timestamp = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .map(|d| d.as_nanos())
88            .unwrap_or(0);
89        
90        let path = std::path::Path::new(original);
91        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
92        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("file");
93        
94        if ext.is_empty() {
95            format!("{}_{}", stem, timestamp)
96        } else {
97            format!("{}_{}.{}", stem, timestamp, ext)
98        }
99    }
100}
101
102impl Default for UploadConfig {
103    fn default() -> Self {
104        Self::new("./uploads")
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_config_creation() {
114        let config = UploadConfig::new("/var/uploads");
115        assert_eq!(config.upload_dir, PathBuf::from("/var/uploads"));
116        assert_eq!(config.max_file_size, 100 * 1024 * 1024);
117    }
118
119    #[test]
120    fn test_config_with_max_size() {
121        let config = UploadConfig::new("/uploads").with_max_size(1024);
122        assert_eq!(config.max_file_size, 1024);
123    }
124
125    #[test]
126    fn test_config_with_extensions() {
127        let config = UploadConfig::new("/uploads")
128            .with_extensions(vec!["txt".to_string(), "pdf".to_string()]);
129        
130        assert_eq!(config.allowed_extensions.len(), 2);
131    }
132
133    #[test]
134    fn test_config_with_overwrite() {
135        let config = UploadConfig::new("/uploads").with_overwrite(true);
136        assert!(config.overwrite);
137    }
138
139    #[test]
140    fn test_config_with_unique_names() {
141        let config = UploadConfig::new("/uploads").with_unique_names(true);
142        assert!(config.unique_names);
143    }
144
145    #[test]
146    fn test_is_extension_allowed_all() {
147        let config = UploadConfig::new("/uploads");
148        
149        assert!(config.is_extension_allowed("test.txt"));
150        assert!(config.is_extension_allowed("test.pdf"));
151        assert!(config.is_extension_allowed("test.jpg"));
152    }
153
154    #[test]
155    fn test_is_extension_allowed_specific() {
156        let config = UploadConfig::new("/uploads")
157            .with_extensions(vec!["txt".to_string(), "PDF".to_string()]);
158        
159        assert!(config.is_extension_allowed("test.txt"));
160        assert!(config.is_extension_allowed("test.pdf"));
161        assert!(!config.is_extension_allowed("test.jpg"));
162        assert!(!config.is_extension_allowed("noextension"));
163    }
164
165    #[test]
166    fn test_is_extension_allowed_case_insensitive() {
167        let config = UploadConfig::new("/uploads")
168            .with_extensions(vec!["TXT".to_string()]);
169        
170        assert!(config.is_extension_allowed("test.TXT"));
171        assert!(config.is_extension_allowed("test.txt"));
172    }
173
174    #[test]
175    fn test_is_size_allowed() {
176        let config = UploadConfig::new("/uploads").with_max_size(1000);
177        
178        assert!(config.is_size_allowed(500));
179        assert!(config.is_size_allowed(1000));
180        assert!(!config.is_size_allowed(1001));
181    }
182
183    #[test]
184    fn test_generate_unique_filename_disabled() {
185        let config = UploadConfig::new("/uploads");
186        
187        let result = config.generate_unique_filename("test.txt");
188        assert_eq!(result, "test.txt");
189    }
190
191    #[test]
192    fn test_generate_unique_filename_enabled() {
193        let config = UploadConfig::new("/uploads").with_unique_names(true);
194        
195        let result = config.generate_unique_filename("test.txt");
196        
197        // Should have format: test_{timestamp}.txt
198        assert!(result.starts_with("test_"));
199        assert!(result.ends_with(".txt"));
200        assert_ne!(result, "test.txt");
201        // Should contain a number (timestamp)
202        assert!(result.chars().any(|c| c.is_numeric()));
203    }
204
205    #[test]
206    fn test_generate_unique_filename_no_ext() {
207        let config = UploadConfig::new("/uploads").with_unique_names(true);
208        
209        let result = config.generate_unique_filename("README");
210        
211        // Should have format: README_{timestamp}
212        assert!(result.starts_with("README_"));
213        assert!(!result.contains('.'));
214    }
215
216    #[test]
217    fn test_default() {
218        let config = UploadConfig::default();
219        assert_eq!(config.upload_dir, PathBuf::from("./uploads"));
220    }
221}