rust_serv/file_upload/
config.rs1use std::path::PathBuf;
4
5#[derive(Debug, Clone)]
7pub struct UploadConfig {
8 pub upload_dir: PathBuf,
10 pub max_file_size: usize,
12 pub allowed_extensions: Vec<String>,
14 pub overwrite: bool,
16 pub unique_names: bool,
18}
19
20impl UploadConfig {
21 pub fn new(upload_dir: impl Into<PathBuf>) -> Self {
23 Self {
24 upload_dir: upload_dir.into(),
25 max_file_size: 100 * 1024 * 1024, allowed_extensions: vec![],
27 overwrite: false,
28 unique_names: false,
29 }
30 }
31
32 pub fn with_max_size(mut self, size: usize) -> Self {
34 self.max_file_size = size;
35 self
36 }
37
38 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
40 self.allowed_extensions = extensions;
41 self
42 }
43
44 pub fn with_overwrite(mut self, overwrite: bool) -> Self {
46 self.overwrite = overwrite;
47 self
48 }
49
50 pub fn with_unique_names(mut self, unique: bool) -> Self {
52 self.unique_names = unique;
53 self
54 }
55
56 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 pub fn is_size_allowed(&self, size: usize) -> bool {
75 size <= self.max_file_size
76 }
77
78 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 assert!(result.starts_with("test_"));
199 assert!(result.ends_with(".txt"));
200 assert_ne!(result, "test.txt");
201 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 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}