multer_derive/
multipart_form.rs1use encoding_rs::Encoding;
2use http::HeaderMap;
3use mime::Mime;
4use multer::{bytes::Bytes, Multipart};
5use std::{borrow::Cow, ops::Index};
6
7#[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 pub fn index(&self) -> usize {
21 self.index
22 }
23
24 pub fn name(&self) -> Option<&str> {
26 self.name.as_deref()
27 }
28
29 pub fn file_name(&self) -> Option<&str> {
31 self.file_name.as_deref()
32 }
33
34 pub fn content_type(&self) -> Option<&Mime> {
36 self.content_type.as_ref()
37 }
38
39 pub fn headers(&self) -> &HeaderMap {
41 &self.headers
42 }
43
44 pub fn bytes(&self) -> &Bytes {
46 &self.bytes
47 }
48
49 pub fn text(&self) -> String {
51 self.text_with_charset("utf-8")
52 }
53
54 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#[derive(Clone)]
77pub struct MultipartForm {
78 fields: Vec<MultipartField>,
79}
80
81impl MultipartForm {
82 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 pub fn get(&self, index: usize) -> Option<&MultipartField> {
108 self.fields.get(index)
109 }
110
111 pub fn get_by_name(&self, name: &str) -> Option<&MultipartField> {
113 self.fields.iter().find(|x| x.name() == Some(name))
114 }
115
116 pub fn fields(&self) -> &[MultipartField] {
118 self.fields.as_slice()
119 }
120
121 pub fn len(&self) -> usize {
123 self.fields.len()
124 }
125
126 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}