medullah_multipart/
file_input.rs

1use crate::content_disposition::ContentDisposition;
2use crate::file_validator::Validator;
3use crate::result::{MultipartError, MultipartResult};
4use crate::{FileRules, Multipart};
5use ntex::http::HeaderMap;
6use ntex::util::Bytes;
7use std::collections::HashMap;
8use std::path::Path;
9
10#[derive(Debug, Default, Clone)]
11pub struct FileInput {
12    pub file_name: String,
13    pub field_name: String,
14    pub size: usize, // Size in bytes
15    pub content_type: String,
16    pub bytes: Vec<Bytes>,
17    pub extension: Option<String>,
18    pub content_disposition: ContentDisposition,
19}
20
21impl FileInput {
22    // Create a new FileInput instance from headers and content disposition
23    pub fn create(headers: &HeaderMap, cd: ContentDisposition) -> MultipartResult<Self> {
24        let content_type = Self::get_content_type(headers)?;
25
26        let variables = cd.get_variables();
27        let field = variables.get("name").cloned().unwrap();
28        let name = variables.get("filename").cloned().unwrap();
29
30        let binding = name.clone();
31        let split_name: Vec<&str> = binding.split('.').collect();
32
33        Ok(Self {
34            content_type,
35            size: 0,
36            bytes: vec![],
37            file_name: name,
38            field_name: field,
39            extension: split_name.last().map(|e| e.to_string()),
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        Self::format_size(size_in_bytes)
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        const KILOBYTE: usize = 1024;
85        const MEGABYTE: usize = KILOBYTE * 1024;
86        const GIGABYTE: usize = MEGABYTE * 1024;
87
88        if size_in_bytes >= GIGABYTE {
89            format!("{:.2} GB", size_in_bytes as f64 / GIGABYTE as f64)
90        } else if size_in_bytes >= MEGABYTE {
91            format!("{:.2} MB", size_in_bytes as f64 / MEGABYTE as f64)
92        } else if size_in_bytes >= KILOBYTE {
93            format!("{:.2} KB", size_in_bytes as f64 / KILOBYTE as f64)
94        } else {
95            format!("{} bytes", size_in_bytes)
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    // Test for `human_size`
105    #[test]
106    fn test_human_readable_size() {
107        let file_input = FileInput {
108            size: 1048576,                                  // 1 MB in bytes
109            bytes: vec![Bytes::from_static(&[0; 1048576])], // Mock 1MB data
110            ..Default::default()
111        };
112
113        // Test for 1 MB
114        assert_eq!(file_input.human_size(), "1.00 MB");
115
116        let file_input = FileInput {
117            size: 1572864,                                  // 1.5 MB in bytes
118            bytes: vec![Bytes::from_static(&[0; 1572864])], // Mock 1MB data
119            ..Default::default()
120        };
121
122        // Test for 1.5 MB
123        assert_eq!(file_input.human_size(), "1.50 MB");
124
125        let file_input = FileInput {
126            size: 102400,                                  // 100 KB in bytes
127            bytes: vec![Bytes::from_static(&[0; 102400])], // Mock 100KB data
128            ..Default::default()
129        };
130
131        // Test for 100 KB (100.00 KB)
132        assert_eq!(file_input.human_size(), "100.00 KB");
133
134        let file_input = FileInput {
135            size: 1014,                                  // 1234 bytes
136            bytes: vec![Bytes::from_static(&[0; 1014])], // Mock 1014 bytes
137            ..Default::default()
138        };
139
140        // Test for bytes (1014 bytes)
141        assert_eq!(file_input.human_size(), "1014 bytes");
142    }
143
144    // Test for `calculate_size`
145    #[test]
146    fn test_calculate_size() {
147        let file_input = FileInput {
148            bytes: vec![
149                Bytes::from_static(&[0; 1024]), // 1 KB
150                Bytes::from_static(&[0; 2048]), // 2 KB
151                Bytes::from_static(&[0; 4096]), // 4 KB
152            ],
153            ..Default::default()
154        };
155
156        assert_eq!(file_input.calculate_size(), 1024 + 2048 + 4096);
157    }
158}