Skip to main content

multer_derive/
multipart_form.rs

1use encoding_rs::Encoding;
2use http::HeaderMap;
3use mime::Mime;
4use multer::{bytes::Bytes, Multipart};
5use std::{borrow::Cow, ops::Index};
6
7/// A field in a multipart form.
8#[derive(Clone)]
9pub struct MultipartField {
10    name: Option<String>,
11    file_name: Option<String>,
12    content_type: Option<Mime>,
13    headers: HeaderMap,
14    bytes: Bytes,
15    index: usize,
16}
17
18impl MultipartField {
19    /// Returns the index of this field in the form.
20    pub fn index(&self) -> usize {
21        self.index
22    }
23
24    /// Returns the name of this field.
25    pub fn name(&self) -> Option<&str> {
26        self.name.as_deref()
27    }
28
29    /// Returns the file name of this field.
30    pub fn file_name(&self) -> Option<&str> {
31        self.file_name.as_deref()
32    }
33
34    /// Returns the content type of this field.
35    pub fn content_type(&self) -> Option<&Mime> {
36        self.content_type.as_ref()
37    }
38
39    /// Returns the headers of this field.
40    pub fn headers(&self) -> &HeaderMap {
41        &self.headers
42    }
43
44    /// Returns the bytes content of this field.
45    pub fn bytes(&self) -> &Bytes {
46        &self.bytes
47    }
48
49    /// Converts the bytes of this field to a `utf-8` string.
50    pub fn text(&self) -> String {
51        self.text_with_charset("utf-8")
52    }
53
54    /// Converts this field to a string using the given encoding.
55    ///
56    /// Checkout: <https://docs.rs/encoding_rs/latest/encoding_rs/struct.Encoding.html>
57    pub fn text_with_charset(&self, default_encoding: &str) -> String {
58        let encoding_name = self
59            .content_type()
60            .and_then(|mime| mime.get_param(mime::CHARSET))
61            .map(|charset| charset.as_str())
62            .unwrap_or(default_encoding);
63
64        let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(encoding_rs::UTF_8);
65        let bytes = self.bytes();
66        let (text, ..) = encoding.decode(bytes);
67
68        match text {
69            Cow::Owned(s) => s,
70            Cow::Borrowed(s) => String::from(s),
71        }
72    }
73}
74
75/// A multipart form.
76#[derive(Clone)]
77pub struct MultipartForm {
78    fields: Vec<MultipartField>,
79}
80
81impl MultipartForm {
82    /// Creates a multipart form by caching all the fields in the [`multer::Multipart`].
83    pub async fn with_multipart(mut multipart: Multipart<'_>) -> multer::Result<MultipartForm> {
84        let mut fields = vec![];
85
86        while let Some((index, field)) = multipart.next_field_with_idx().await? {
87            let name = field.name().map(|s| s.to_owned());
88            let file_name = field.file_name().map(|s| s.to_owned());
89            let content_type = field.content_type().cloned();
90            let headers = field.headers().clone();
91            let bytes = field.bytes().await?;
92
93            fields.push(MultipartField {
94                name,
95                file_name,
96                content_type,
97                headers,
98                bytes,
99                index,
100            })
101        }
102
103        Ok(MultipartForm { fields })
104    }
105
106    /// Returns the field in the given index.
107    pub fn get(&self, index: usize) -> Option<&MultipartField> {
108        self.fields.get(index)
109    }
110
111    /// Returns the field with the given name
112    pub fn get_by_name(&self, name: &str) -> Option<&MultipartField> {
113        self.fields.iter().find(|x| x.name() == Some(name))
114    }
115
116    /// Returns all the fields.
117    pub fn fields(&self) -> &[MultipartField] {
118        self.fields.as_slice()
119    }
120
121    /// Returns the number of fields.
122    pub fn len(&self) -> usize {
123        self.fields.len()
124    }
125
126    /// Returns true if this form had no fields.
127    pub fn is_empty(&self) -> bool {
128        self.len() == 0
129    }
130}
131
132impl Index<usize> for MultipartForm {
133    type Output = MultipartField;
134
135    fn index(&self, index: usize) -> &Self::Output {
136        &self.fields[index]
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use http::HeaderValue;
143    use multer::Multipart;
144
145    use crate::multipart_form::MultipartForm;
146
147    const MULTI_PART_STR: &str = "--MyBoundary\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nJohn Doe\r\n--MyBoundary\r\nContent-Disposition: form-data; name=\"email\"\r\n\r\njohndoe@example.com\r\n--MyBoundary\r\nContent-Disposition: form-data; name=\"age\"\r\n\r\n25\r\n--MyBoundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example.txt\"\r\nContent-Type: text/plain\r\n\r\nThis is an example file.\r\n--MyBoundary--\r\n";
148
149    #[tokio::test]
150    async fn with_multipart_test() {
151        let reader = MULTI_PART_STR.as_bytes();
152        let multipart = Multipart::with_reader(reader, "MyBoundary");
153
154        let form = MultipartForm::with_multipart(multipart).await.unwrap();
155
156        assert_eq!(form.len(), 4);
157        assert_eq!(form[0].name(), Some("name"));
158        assert_eq!(form[1].name(), Some("email"));
159        assert_eq!(form[2].name(), Some("age"));
160        assert_eq!(form[3].name(), Some("file"));
161    }
162
163    #[tokio::test]
164    async fn with_multipart_get_test() {
165        let reader = MULTI_PART_STR.as_bytes();
166        let multipart = Multipart::with_reader(reader, "MyBoundary");
167
168        let form = MultipartForm::with_multipart(multipart).await.unwrap();
169
170        assert_eq!(form.get(0).unwrap().name(), Some("name"));
171        assert_eq!(form.get(1).unwrap().name(), Some("email"));
172        assert_eq!(form.get(2).unwrap().name(), Some("age"));
173        assert_eq!(form.get(3).unwrap().name(), Some("file"));
174        assert!(form.get(4).is_none());
175    }
176
177    #[tokio::test]
178    async fn form_field_text_test() {
179        let reader = MULTI_PART_STR.as_bytes();
180        let multipart = Multipart::with_reader(reader, "MyBoundary");
181
182        let form = MultipartForm::with_multipart(multipart).await.unwrap();
183
184        assert_eq!(form.get(0).unwrap().text(), String::from("John Doe"));
185        assert_eq!(
186            form.get(1).unwrap().text(),
187            String::from("johndoe@example.com")
188        );
189        assert_eq!(form.get(2).unwrap().text(), String::from("25"));
190        assert_eq!(
191            form.get(3).unwrap().text(),
192            String::from("This is an example file.")
193        );
194        assert!(form.get(4).is_none());
195    }
196
197    #[tokio::test]
198    async fn form_field_file_test() {
199        let reader = MULTI_PART_STR.as_bytes();
200        let multipart = Multipart::with_reader(reader, "MyBoundary");
201
202        let form = MultipartForm::with_multipart(multipart).await.unwrap();
203
204        let file = &form[3];
205        assert_eq!(file.text(), "This is an example file.");
206        assert_eq!(file.file_name(), Some("example.txt"));
207        assert_eq!(file.content_type(), Some(&mime::TEXT_PLAIN));
208
209        let headers = file.headers();
210        assert_eq!(headers.len(), 2);
211
212        assert_eq!(
213            headers.get("content-disposition"),
214            Some(&HeaderValue::from_static(
215                "form-data; name=\"file\"; filename=\"example.txt\""
216            ))
217        );
218
219        assert_eq!(
220            headers.get("content-type"),
221            Some(&HeaderValue::from_static("text/plain"))
222        );
223    }
224}