1use crate::content_disposition::ContentDisposition;
2use crate::file_validator::Validator;
3use crate::result::{MultipartError, MultipartResult};
4use crate::{FileRules, Multipart};
5use foxtive::helpers::FileExtHelper;
6use ntex::http::HeaderMap;
7use ntex::util::Bytes;
8use std::collections::HashMap;
9use std::path::Path;
10
11#[derive(Debug, Default, Clone)]
12pub struct FileInput {
13 pub file_name: String,
14 pub field_name: String,
15 pub size: usize, pub content_type: String,
17 pub bytes: Vec<Bytes>,
18 pub extension: Option<String>,
19 pub content_disposition: ContentDisposition,
20}
21
22impl FileInput {
23 pub fn create(headers: &HeaderMap, cd: ContentDisposition) -> MultipartResult<Self> {
25 let content_type = Self::get_content_type(headers)?;
26
27 let variables = cd.get_variables();
28 let field = variables.get("name").cloned().unwrap();
29 let name = variables.get("filename").cloned().unwrap();
30
31 let extension = FileExtHelper::new().get_extension(&name);
32
33 Ok(Self {
34 extension,
35 content_type,
36 size: 0,
37 bytes: vec![],
38 file_name: name,
39 field_name: field,
40 content_disposition: cd,
41 })
42 }
43
44 pub async fn save(&self, path: impl AsRef<Path>) -> MultipartResult<()> {
46 Multipart::save_file(self, path).await
47 }
48
49 pub fn validate(&self, rules: FileRules) -> MultipartResult<()> {
50 let mut files = HashMap::new();
51 files.insert(self.field_name.clone(), vec![self.clone()]);
52
53 Validator::new()
54 .add_rule(&self.field_name, rules)
55 .validate(&files)
56 }
57
58 pub fn calculate_size(&self) -> usize {
60 self.bytes.iter().map(|b| b.len()).sum()
61 }
62
63 pub fn human_size(&self) -> String {
65 let size_in_bytes = self.calculate_size();
66 foxtive::helpers::file_size::format_size(size_in_bytes as u64)
67 }
68
69 fn get_content_type(headers: &HeaderMap) -> MultipartResult<String> {
71 match headers.get("content-type") {
72 None => Err(MultipartError::NoContentType(
73 "Empty content type".to_string(),
74 )),
75 Some(header) => header
76 .to_str()
77 .map(|v| v.to_string())
78 .map_err(|err| MultipartError::NoContentType(err.to_string())),
79 }
80 }
81
82 pub fn format_size(size_in_bytes: usize) -> String {
84 foxtive::helpers::file_size::format_size(size_in_bytes as u64)
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use ntex::http::header::{HeaderName, HeaderValue};
92 use std::str::FromStr;
93
94 fn create_headers_with_content_type(content_type: &str) -> HeaderMap {
96 let mut headers = HeaderMap::new();
97 headers.insert(
98 HeaderName::from_str("content-type").unwrap(),
99 HeaderValue::from_str(content_type).unwrap(),
100 );
101 headers
102 }
103
104 fn create_content_disposition(field_name: &str, filename: &str) -> ContentDisposition {
106 let mut variables = HashMap::new();
107 variables.insert("name".to_string(), field_name.to_string());
108 variables.insert("filename".to_string(), filename.to_string());
109
110 ContentDisposition::from(variables)
111 }
112
113 #[test]
115 fn test_calculate_size_empty() {
116 let file_input = FileInput {
117 bytes: vec![],
118 ..Default::default()
119 };
120
121 assert_eq!(file_input.calculate_size(), 0);
122 }
123
124 #[test]
125 fn test_calculate_size_single_chunk() {
126 let file_input = FileInput {
127 bytes: vec![Bytes::from_static(&[0; 1024])], ..Default::default()
129 };
130
131 assert_eq!(file_input.calculate_size(), 1024);
132 }
133
134 #[test]
135 fn test_calculate_size_multiple_chunks() {
136 let file_input = FileInput {
137 bytes: vec![
138 Bytes::from_static(&[0; 1024]), Bytes::from_static(&[0; 2048]), Bytes::from_static(&[0; 4096]), ],
142 ..Default::default()
143 };
144
145 assert_eq!(file_input.calculate_size(), 1024 + 2048 + 4096);
146 }
147
148 #[test]
149 fn test_calculate_size_various_sizes() {
150 let file_input = FileInput {
151 bytes: vec![
152 Bytes::from_static(&[1; 1]), Bytes::from_static(&[2; 10]), Bytes::from_static(&[3; 100]), Bytes::from_static(&[4; 1000]), ],
157 ..Default::default()
158 };
159
160 assert_eq!(file_input.calculate_size(), 1 + 10 + 100 + 1000);
161 }
162
163 #[test]
165 fn test_human_size_bytes() {
166 let file_input = FileInput {
167 bytes: vec![Bytes::from_static(&[0; 500])], ..Default::default()
169 };
170
171 let human_size = file_input.human_size();
172 assert!(human_size.contains("500") || human_size.contains("bytes"));
175 }
176
177 #[test]
178 fn test_human_size_kilobytes() {
179 let file_input = FileInput {
180 bytes: vec![Bytes::from_static(&[0; 2048])], ..Default::default()
182 };
183
184 let human_size = file_input.human_size();
185 assert!(human_size.contains("KB") || human_size.contains("kB"));
186 }
187
188 #[test]
189 fn test_human_size_megabytes() {
190 let file_input = FileInput {
191 bytes: vec![Bytes::from_static(&[0; 2_097_152])], ..Default::default()
193 };
194
195 let human_size = file_input.human_size();
196 assert!(human_size.contains("MB"));
197 }
198
199 #[test]
201 fn test_create_success() {
202 let headers = create_headers_with_content_type("image/jpeg");
203 let cd = create_content_disposition("upload", "test.jpg");
204
205 let result = FileInput::create(&headers, cd);
206 assert!(result.is_ok());
207
208 let file_input = result.unwrap();
209 assert_eq!(file_input.content_type, "image/jpeg");
210 assert_eq!(file_input.field_name, "upload");
211 assert_eq!(file_input.file_name, "test.jpg");
212 assert_eq!(file_input.extension, Some("jpg".to_string()));
213 assert_eq!(file_input.size, 0);
214 assert!(file_input.bytes.is_empty());
215 }
216
217 #[test]
218 fn test_create_missing_content_type() {
219 let headers = HeaderMap::new(); let cd = create_content_disposition("upload", "test.jpg");
221
222 let result = FileInput::create(&headers, cd);
223 assert!(result.is_err());
224
225 if let Err(MultipartError::NoContentType(msg)) = result {
226 assert_eq!(msg, "Empty content type");
227 } else {
228 panic!("Expected NoContentType error");
229 }
230 }
231
232 #[test]
233 fn test_create_various_content_types() {
234 let test_cases = vec![
235 ("image/png", "image.png"),
236 ("text/plain", "document.txt"),
237 ("application/pdf", "document.pdf"),
238 ("video/mp4", "video.mp4"),
239 ("audio/mpeg", "audio.mp3"),
240 ];
241
242 for (content_type, filename) in test_cases {
243 let headers = create_headers_with_content_type(content_type);
244 let cd = create_content_disposition("file", filename);
245
246 let result = FileInput::create(&headers, cd);
247 assert!(result.is_ok(), "Failed for content type: {content_type}");
248
249 let file_input = result.unwrap();
250 assert_eq!(file_input.content_type, content_type);
251 assert_eq!(file_input.file_name, filename);
252 }
253 }
254
255 #[test]
256 fn test_create_with_no_extension() {
257 let headers = create_headers_with_content_type("text/plain");
258 let cd = create_content_disposition("upload", "README");
259
260 let result = FileInput::create(&headers, cd);
261 assert!(result.is_ok());
262
263 let file_input = result.unwrap();
264 assert_eq!(file_input.extension, None);
265 assert_eq!(file_input.file_name, "README");
266 }
267
268 #[test]
269 fn test_create_with_multiple_extensions() {
270 let headers = create_headers_with_content_type("application/gzip");
271 let cd = create_content_disposition("upload", "archive.tar.gz");
272
273 let result = FileInput::create(&headers, cd);
274 assert!(result.is_ok());
275
276 let file_input = result.unwrap();
277 assert!(file_input.extension.is_some());
280 assert_eq!(file_input.file_name, "archive.tar.gz");
281 }
282
283 #[test]
285 fn test_get_content_type_success() {
286 let headers = create_headers_with_content_type("application/json");
287 let result = FileInput::get_content_type(&headers);
288
289 assert!(result.is_ok());
290 assert_eq!(result.unwrap(), "application/json");
291 }
292
293 #[test]
294 fn test_get_content_type_missing() {
295 let headers = HeaderMap::new();
296 let result = FileInput::get_content_type(&headers);
297
298 assert!(result.is_err());
299 if let Err(MultipartError::NoContentType(msg)) = result {
300 assert_eq!(msg, "Empty content type");
301 }
302 }
303
304 #[test]
305 fn test_get_content_type_invalid_header() {
306 let mut headers = HeaderMap::new();
307 headers.insert(
309 HeaderName::from_str("content-type").unwrap(),
310 HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap(),
311 );
312
313 let result = FileInput::get_content_type(&headers);
314 assert!(result.is_err());
315 }
316
317 #[test]
319 fn test_default_file_input() {
320 let file_input = FileInput::default();
321
322 assert_eq!(file_input.file_name, "");
323 assert_eq!(file_input.field_name, "");
324 assert_eq!(file_input.size, 0);
325 assert_eq!(file_input.content_type, "");
326 assert!(file_input.bytes.is_empty());
327 assert_eq!(file_input.extension, None);
328 }
329
330 #[test]
332 fn test_clone_file_input() {
333 let original = FileInput {
334 file_name: "test.txt".to_string(),
335 field_name: "upload".to_string(),
336 size: 1024,
337 content_type: "text/plain".to_string(),
338 bytes: vec![Bytes::from_static(&[0; 1024])],
339 extension: Some("txt".to_string()),
340 content_disposition: create_content_disposition("upload", "test.txt"),
341 };
342
343 let cloned = original.clone();
344
345 assert_eq!(original.file_name, cloned.file_name);
346 assert_eq!(original.field_name, cloned.field_name);
347 assert_eq!(original.size, cloned.size);
348 assert_eq!(original.content_type, cloned.content_type);
349 assert_eq!(original.bytes.len(), cloned.bytes.len());
350 assert_eq!(original.extension, cloned.extension);
351 }
352
353 #[test]
355 fn test_file_input_workflow() {
356 let headers = create_headers_with_content_type("image/jpeg");
357 let cd = create_content_disposition("photo", "vacation.jpg");
358
359 let mut file_input = FileInput::create(&headers, cd).unwrap();
361
362 file_input.bytes = vec![
364 Bytes::from_static(&[0xFF, 0xD8, 0xFF, 0xE0]), Bytes::from_static(&[0; 1020]), ];
367
368 assert_eq!(file_input.calculate_size(), 1024);
370
371 let human_size = file_input.human_size();
373 assert!(human_size.contains("1") && human_size.contains("KB"));
374
375 assert_eq!(file_input.content_type, "image/jpeg");
377 assert_eq!(file_input.field_name, "photo");
378 assert_eq!(file_input.file_name, "vacation.jpg");
379 assert_eq!(file_input.extension, Some("jpg".to_string()));
380 }
381
382 #[test]
384 fn test_calculate_size_performance() {
385 let mut bytes = Vec::new();
387 for _ in 0..1000 {
388 bytes.push(Bytes::from_static(&[0; 100])); }
390
391 let file_input = FileInput {
392 bytes,
393 ..Default::default()
394 };
395
396 let start = std::time::Instant::now();
397 let size = file_input.calculate_size();
398 let duration = start.elapsed();
399
400 assert_eq!(size, 100_000); assert!(duration.as_millis() < 10); }
403}