reqwest_wasm/wasm/
multipart.rs

1//! multipart/form-data
2use std::borrow::Cow;
3use std::fmt;
4
5use http::HeaderMap;
6use mime_guess::Mime;
7use web_sys::FormData;
8
9use super::Body;
10
11/// An async multipart/form-data request.
12pub struct Form {
13    inner: FormParts<Part>,
14}
15
16impl Form {
17    pub(crate) fn is_empty(&self) -> bool {
18        self.inner.fields.is_empty()
19    }
20}
21
22/// A field in a multipart form.
23pub struct Part {
24    meta: PartMetadata,
25    value: Body,
26}
27
28pub(crate) struct FormParts<P> {
29    pub(crate) fields: Vec<(Cow<'static, str>, P)>,
30}
31
32pub(crate) struct PartMetadata {
33    mime: Option<Mime>,
34    file_name: Option<Cow<'static, str>>,
35    pub(crate) headers: HeaderMap,
36}
37
38pub(crate) trait PartProps {
39    fn metadata(&self) -> &PartMetadata;
40}
41
42// ===== impl Form =====
43
44impl Default for Form {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl Form {
51    /// Creates a new async Form without any content.
52    pub fn new() -> Form {
53        Form {
54            inner: FormParts::new(),
55        }
56    }
57
58    /// Add a data field with supplied name and value.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// let form = reqwest::multipart::Form::new()
64    ///     .text("username", "seanmonstar")
65    ///     .text("password", "secret");
66    /// ```
67    pub fn text<T, U>(self, name: T, value: U) -> Form
68    where
69        T: Into<Cow<'static, str>>,
70        U: Into<Cow<'static, str>>,
71    {
72        self.part(name, Part::text(value))
73    }
74
75    /// Adds a customized Part.
76    pub fn part<T>(self, name: T, part: Part) -> Form
77    where
78        T: Into<Cow<'static, str>>,
79    {
80        self.with_inner(move |inner| inner.part(name, part))
81    }
82
83    fn with_inner<F>(self, func: F) -> Self
84    where
85        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
86    {
87        Form {
88            inner: func(self.inner),
89        }
90    }
91
92    pub(crate) fn to_form_data(&self) -> crate::Result<FormData> {
93        let form = FormData::new()
94            .map_err(crate::error::wasm)
95            .map_err(crate::error::builder)?;
96
97        for (name, part) in self.inner.fields.iter() {
98            let blob = part.blob()?;
99
100            if let Some(file_name) = &part.metadata().file_name {
101                form.append_with_blob_and_filename(name, &blob, &file_name)
102            } else {
103                form.append_with_blob(name, &blob)
104            }
105            .map_err(crate::error::wasm)
106            .map_err(crate::error::builder)?;
107        }
108        Ok(form)
109    }
110}
111
112impl fmt::Debug for Form {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        self.inner.fmt_fields("Form", f)
115    }
116}
117
118// ===== impl Part =====
119
120impl Part {
121    /// Makes a text parameter.
122    pub fn text<T>(value: T) -> Part
123    where
124        T: Into<Cow<'static, str>>,
125    {
126        let body = match value.into() {
127            Cow::Borrowed(slice) => Body::from(slice),
128            Cow::Owned(string) => Body::from(string),
129        };
130        Part::new(body)
131    }
132
133    /// Makes a new parameter from arbitrary bytes.
134    pub fn bytes<T>(value: T) -> Part
135    where
136        T: Into<Cow<'static, [u8]>>,
137    {
138        let body = match value.into() {
139            Cow::Borrowed(slice) => Body::from(slice),
140            Cow::Owned(vec) => Body::from(vec),
141        };
142        Part::new(body)
143    }
144
145    /// Makes a new parameter from an arbitrary stream.
146    pub fn stream<T: Into<Body>>(value: T) -> Part {
147        Part::new(value.into())
148    }
149
150    fn new(value: Body) -> Part {
151        Part {
152            meta: PartMetadata::new(),
153            value: value.into_part(),
154        }
155    }
156
157    /// Tries to set the mime of this part.
158    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
159        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
160    }
161
162    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
163    fn mime(self, mime: Mime) -> Part {
164        self.with_inner(move |inner| inner.mime(mime))
165    }
166
167    /// Sets the filename, builder style.
168    pub fn file_name<T>(self, filename: T) -> Part
169    where
170        T: Into<Cow<'static, str>>,
171    {
172        self.with_inner(move |inner| inner.file_name(filename))
173    }
174
175    fn with_inner<F>(self, func: F) -> Self
176    where
177        F: FnOnce(PartMetadata) -> PartMetadata,
178    {
179        Part {
180            meta: func(self.meta),
181            value: self.value,
182        }
183    }
184
185    fn blob(&self) -> crate::Result<web_sys::Blob> {
186        use web_sys::Blob;
187        use web_sys::BlobPropertyBag;
188        let mut properties = BlobPropertyBag::new();
189        if let Some(mime) = &self.meta.mime {
190            properties.type_(mime.as_ref());
191        }
192
193        // BUG: the return value of to_js_value() is not valid if
194        // it is a MultipartForm variant.
195        let js_value = self.value.to_js_value()?;
196        Blob::new_with_u8_array_sequence_and_options(&js_value, &properties)
197            .map_err(crate::error::wasm)
198            .map_err(crate::error::builder)
199    }
200}
201
202impl fmt::Debug for Part {
203    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
204        let mut dbg = f.debug_struct("Part");
205        dbg.field("value", &self.value);
206        self.meta.fmt_fields(&mut dbg);
207        dbg.finish()
208    }
209}
210
211impl PartProps for Part {
212    fn metadata(&self) -> &PartMetadata {
213        &self.meta
214    }
215}
216
217// ===== impl FormParts =====
218
219impl<P: PartProps> FormParts<P> {
220    pub(crate) fn new() -> Self {
221        FormParts { fields: Vec::new() }
222    }
223
224    /// Adds a customized Part.
225    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
226    where
227        T: Into<Cow<'static, str>>,
228    {
229        self.fields.push((name.into(), part));
230        self
231    }
232}
233
234impl<P: fmt::Debug> FormParts<P> {
235    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
236        f.debug_struct(ty_name)
237            .field("parts", &self.fields)
238            .finish()
239    }
240}
241
242// ===== impl PartMetadata =====
243
244impl PartMetadata {
245    pub(crate) fn new() -> Self {
246        PartMetadata {
247            mime: None,
248            file_name: None,
249            headers: HeaderMap::default(),
250        }
251    }
252
253    pub(crate) fn mime(mut self, mime: Mime) -> Self {
254        self.mime = Some(mime);
255        self
256    }
257
258    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
259    where
260        T: Into<Cow<'static, str>>,
261    {
262        self.file_name = Some(filename.into());
263        self
264    }
265}
266
267impl PartMetadata {
268    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
269        &self,
270        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
271    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
272        debug_struct
273            .field("mime", &self.mime)
274            .field("file_name", &self.file_name)
275            .field("headers", &self.headers)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281
282    use wasm_bindgen_test::*;
283
284    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
285
286    #[wasm_bindgen_test]
287    async fn test_multipart_js() {
288        use super::{Form, Part};
289        use js_sys::Uint8Array;
290        use wasm_bindgen::JsValue;
291        use web_sys::{File, FormData};
292
293        let text_file_name = "test.txt";
294        let text_file_type = "text/plain";
295        let text_content = "TEST";
296        let text_part = Part::text(text_content)
297            .file_name(text_file_name)
298            .mime_str(text_file_type)
299            .expect("invalid mime type");
300
301        let binary_file_name = "binary.bin";
302        let binary_file_type = "application/octet-stream";
303        let binary_content = vec![0u8, 42];
304        let binary_part = Part::bytes(binary_content.clone())
305            .file_name(binary_file_name)
306            .mime_str(binary_file_type)
307            .expect("invalid mime type");
308
309        let text_name = "text part";
310        let binary_name = "binary part";
311        let form = Form::new()
312            .part(text_name, text_part)
313            .part(binary_name, binary_part);
314
315        let mut init = web_sys::RequestInit::new();
316        init.method("POST");
317        init.body(Some(
318            form.to_form_data()
319                .expect("could not convert to FormData")
320                .as_ref(),
321        ));
322
323        let js_req = web_sys::Request::new_with_str_and_init("", &init)
324            .expect("could not create JS request");
325
326        let form_data_promise = js_req.form_data().expect("could not get form_data promise");
327
328        let form_data = crate::wasm::promise::<FormData>(form_data_promise)
329            .await
330            .expect("could not get body as form data");
331
332        // check text part
333        let text_file = File::from(form_data.get(text_name));
334        assert_eq!(text_file.name(), text_file_name);
335        assert_eq!(text_file.type_(), text_file_type);
336
337        let text_promise = text_file.text();
338        let text = crate::wasm::promise::<JsValue>(text_promise)
339            .await
340            .expect("could not get text body as text");
341        assert_eq!(
342            text.as_string().expect("text is not a string"),
343            text_content
344        );
345
346        // check binary part
347        let binary_file = File::from(form_data.get(binary_name));
348        assert_eq!(binary_file.name(), binary_file_name);
349        assert_eq!(binary_file.type_(), binary_file_type);
350
351        let binary_array_buffer_promise = binary_file.array_buffer();
352        let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
353            .await
354            .expect("could not get request body as array buffer");
355
356        let binary = Uint8Array::new(&array_buffer).to_vec();
357
358        assert_eq!(binary, binary_content);
359    }
360}