Skip to main content

foxtive_ntex_multipart/
file_input.rs

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, // Size in bytes
16    pub content_type: String,
17    pub bytes: Vec<Bytes>,
18    pub extension: Option<String>,
19    pub content_disposition: ContentDisposition,
20}
21
22impl FileInput {
23    // Create a new FileInput instance from headers and content disposition
24    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    // Save the file to the specified path
45    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    /// Calculate the file size from bytes collected
59    pub fn calculate_size(&self) -> usize {
60        self.bytes.iter().map(|b| b.len()).sum()
61    }
62
63    /// Get the human-readable file size (e.g., "1.2 MB", "300 KB")
64    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    // Get the content type from headers
70    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    // Helper function to format size in bytes to a human-readable string
83    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    // Helper function to create a basic HeaderMap with content-type
95    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    // Helper function to create a basic ContentDisposition
105    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 for `calculate_size` with various byte combinations
114    #[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])], // 1 KB
128            ..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]), // 1 KB
139                Bytes::from_static(&[0; 2048]), // 2 KB
140                Bytes::from_static(&[0; 4096]), // 4 KB
141            ],
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]),    // 1 byte
153                Bytes::from_static(&[2; 10]),   // 10 bytes
154                Bytes::from_static(&[3; 100]),  // 100 bytes
155                Bytes::from_static(&[4; 1000]), // 1000 bytes
156            ],
157            ..Default::default()
158        };
159
160        assert_eq!(file_input.calculate_size(), 1 + 10 + 100 + 1000);
161    }
162
163    // Test for `human_size` method
164    #[test]
165    fn test_human_size_bytes() {
166        let file_input = FileInput {
167            bytes: vec![Bytes::from_static(&[0; 500])], // 500 bytes
168            ..Default::default()
169        };
170
171        let human_size = file_input.human_size();
172        // This depends on your foxtive::helpers::file_size::format_size implementation
173        // Adjust the expected value based on your actual implementation
174        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])], // 2 KB
181            ..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])], // 2 MB
192            ..Default::default()
193        };
194
195        let human_size = file_input.human_size();
196        assert!(human_size.contains("MB"));
197    }
198
199    // Test for `create` method
200    #[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(); // Empty headers
220        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        // This depends on your FileExtHelper implementation
278        // Adjust based on whether it returns "gz" or "tar.gz"
279        assert!(file_input.extension.is_some());
280        assert_eq!(file_input.file_name, "archive.tar.gz");
281    }
282
283    // Test for `get_content_type` method
284    #[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        // Insert invalid UTF-8 bytes
308        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 for Default implementation
318    #[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 for Clone implementation
331    #[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    // Integration test combining multiple operations
354    #[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        // Create FileInput
360        let mut file_input = FileInput::create(&headers, cd).unwrap();
361
362        // Add some data
363        file_input.bytes = vec![
364            Bytes::from_static(&[0xFF, 0xD8, 0xFF, 0xE0]), // JPEG header
365            Bytes::from_static(&[0; 1020]),                // Rest of 1KB
366        ];
367
368        // Test size calculation
369        assert_eq!(file_input.calculate_size(), 1024);
370
371        // Test human readable size
372        let human_size = file_input.human_size();
373        assert!(human_size.contains("1") && human_size.contains("KB"));
374
375        // Verify other properties
376        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    // Benchmark-style test for performance
383    #[test]
384    fn test_calculate_size_performance() {
385        // Create a file input with many small chunks
386        let mut bytes = Vec::new();
387        for _ in 0..1000 {
388            bytes.push(Bytes::from_static(&[0; 100])); // 100 bytes each
389        }
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); // 1000 * 100 bytes
401        assert!(duration.as_millis() < 10); // Should be very fast
402    }
403}