reqwest_h3/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            part.append_to_form(name, &form)
99                .map_err(crate::error::wasm)
100                .map_err(crate::error::builder)?;
101        }
102        Ok(form)
103    }
104}
105
106impl fmt::Debug for Form {
107    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108        self.inner.fmt_fields("Form", f)
109    }
110}
111
112// ===== impl Part =====
113
114impl Part {
115    /// Makes a text parameter.
116    pub fn text<T>(value: T) -> Part
117    where
118        T: Into<Cow<'static, str>>,
119    {
120        let body = match value.into() {
121            Cow::Borrowed(slice) => Body::from(slice),
122            Cow::Owned(string) => Body::from(string),
123        };
124        Part::new(body)
125    }
126
127    /// Makes a new parameter from arbitrary bytes.
128    pub fn bytes<T>(value: T) -> Part
129    where
130        T: Into<Cow<'static, [u8]>>,
131    {
132        let body = match value.into() {
133            Cow::Borrowed(slice) => Body::from(slice),
134            Cow::Owned(vec) => Body::from(vec),
135        };
136        Part::new(body)
137    }
138
139    /// Makes a new parameter from an arbitrary stream.
140    pub fn stream<T: Into<Body>>(value: T) -> Part {
141        Part::new(value.into())
142    }
143
144    fn new(value: Body) -> Part {
145        Part {
146            meta: PartMetadata::new(),
147            value: value.into_part(),
148        }
149    }
150
151    /// Tries to set the mime of this part.
152    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
153        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
154    }
155
156    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
157    fn mime(self, mime: Mime) -> Part {
158        self.with_inner(move |inner| inner.mime(mime))
159    }
160
161    /// Sets the filename, builder style.
162    pub fn file_name<T>(self, filename: T) -> Part
163    where
164        T: Into<Cow<'static, str>>,
165    {
166        self.with_inner(move |inner| inner.file_name(filename))
167    }
168
169    /// Sets custom headers for the part.
170    pub fn headers(self, headers: HeaderMap) -> Part {
171        self.with_inner(move |inner| inner.headers(headers))
172    }
173
174    fn with_inner<F>(self, func: F) -> Self
175    where
176        F: FnOnce(PartMetadata) -> PartMetadata,
177    {
178        Part {
179            meta: func(self.meta),
180            value: self.value,
181        }
182    }
183
184    fn append_to_form(
185        &self,
186        name: &str,
187        form: &web_sys::FormData,
188    ) -> Result<(), wasm_bindgen::JsValue> {
189        let single = self
190            .value
191            .as_single()
192            .expect("A part's body can't be multipart itself");
193
194        let mut mime_type = self.metadata().mime.as_ref();
195
196        // The JS fetch API doesn't support file names and mime types for strings. So we do our best
197        // effort to use `append_with_str` and fallback to `append_with_blob_*` if that's not
198        // possible.
199        if let super::body::Single::Text(text) = single {
200            if mime_type.is_none() || mime_type == Some(&mime_guess::mime::TEXT_PLAIN) {
201                if self.metadata().file_name.is_none() {
202                    return form.append_with_str(name, text);
203                }
204            } else {
205                mime_type = Some(&mime_guess::mime::TEXT_PLAIN);
206            }
207        }
208
209        let blob = self.blob(mime_type)?;
210
211        if let Some(file_name) = &self.metadata().file_name {
212            form.append_with_blob_and_filename(name, &blob, file_name)
213        } else {
214            form.append_with_blob(name, &blob)
215        }
216    }
217
218    fn blob(&self, mime_type: Option<&Mime>) -> crate::Result<web_sys::Blob> {
219        use web_sys::Blob;
220        use web_sys::BlobPropertyBag;
221        let mut properties = BlobPropertyBag::new();
222        if let Some(mime) = mime_type {
223            properties.type_(mime.as_ref());
224        }
225
226        let js_value = self
227            .value
228            .as_single()
229            .expect("A part's body can't be set to a multipart body")
230            .to_js_value();
231
232        let body_array = js_sys::Array::new();
233        body_array.push(&js_value);
234
235        Blob::new_with_u8_array_sequence_and_options(body_array.as_ref(), &properties)
236            .map_err(crate::error::wasm)
237            .map_err(crate::error::builder)
238    }
239}
240
241impl fmt::Debug for Part {
242    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
243        let mut dbg = f.debug_struct("Part");
244        dbg.field("value", &self.value);
245        self.meta.fmt_fields(&mut dbg);
246        dbg.finish()
247    }
248}
249
250impl PartProps for Part {
251    fn metadata(&self) -> &PartMetadata {
252        &self.meta
253    }
254}
255
256// ===== impl FormParts =====
257
258impl<P: PartProps> FormParts<P> {
259    pub(crate) fn new() -> Self {
260        FormParts { fields: Vec::new() }
261    }
262
263    /// Adds a customized Part.
264    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
265    where
266        T: Into<Cow<'static, str>>,
267    {
268        self.fields.push((name.into(), part));
269        self
270    }
271}
272
273impl<P: fmt::Debug> FormParts<P> {
274    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
275        f.debug_struct(ty_name)
276            .field("parts", &self.fields)
277            .finish()
278    }
279}
280
281// ===== impl PartMetadata =====
282
283impl PartMetadata {
284    pub(crate) fn new() -> Self {
285        PartMetadata {
286            mime: None,
287            file_name: None,
288            headers: HeaderMap::default(),
289        }
290    }
291
292    pub(crate) fn mime(mut self, mime: Mime) -> Self {
293        self.mime = Some(mime);
294        self
295    }
296
297    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
298    where
299        T: Into<Cow<'static, str>>,
300    {
301        self.file_name = Some(filename.into());
302        self
303    }
304
305    pub(crate) fn headers<T>(mut self, headers: T) -> Self
306    where
307        T: Into<HeaderMap>,
308    {
309        self.headers = headers.into();
310        self
311    }
312}
313
314impl PartMetadata {
315    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
316        &self,
317        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
318    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
319        debug_struct
320            .field("mime", &self.mime)
321            .field("file_name", &self.file_name)
322            .field("headers", &self.headers)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328
329    use wasm_bindgen_test::*;
330
331    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
332
333    #[wasm_bindgen_test]
334    async fn test_multipart_js() {
335        use super::{Form, Part};
336        use js_sys::Uint8Array;
337        use wasm_bindgen::JsValue;
338        use web_sys::{File, FormData};
339
340        let text_file_name = "test.txt";
341        let text_file_type = "text/plain";
342        let text_content = "TEST";
343        let text_part = Part::text(text_content)
344            .file_name(text_file_name)
345            .mime_str(text_file_type)
346            .expect("invalid mime type");
347
348        let binary_file_name = "binary.bin";
349        let binary_file_type = "application/octet-stream";
350        let binary_content = vec![0u8, 42];
351        let binary_part = Part::bytes(binary_content.clone())
352            .file_name(binary_file_name)
353            .mime_str(binary_file_type)
354            .expect("invalid mime type");
355
356        let string_name = "string";
357        let string_content = "CONTENT";
358        let string_part = Part::text(string_content);
359
360        let text_name = "text part";
361        let binary_name = "binary part";
362        let form = Form::new()
363            .part(text_name, text_part)
364            .part(binary_name, binary_part)
365            .part(string_name, string_part);
366
367        let mut init = web_sys::RequestInit::new();
368        init.method("POST");
369        init.body(Some(
370            form.to_form_data()
371                .expect("could not convert to FormData")
372                .as_ref(),
373        ));
374
375        let js_req = web_sys::Request::new_with_str_and_init("", &init)
376            .expect("could not create JS request");
377
378        let form_data_promise = js_req.form_data().expect("could not get form_data promise");
379
380        let form_data = crate::wasm::promise::<FormData>(form_data_promise)
381            .await
382            .expect("could not get body as form data");
383
384        // check text part
385        let text_file = File::from(form_data.get(text_name));
386        assert_eq!(text_file.name(), text_file_name);
387        assert_eq!(text_file.type_(), text_file_type);
388
389        let text_promise = text_file.text();
390        let text = crate::wasm::promise::<JsValue>(text_promise)
391            .await
392            .expect("could not get text body as text");
393        assert_eq!(
394            text.as_string().expect("text is not a string"),
395            text_content
396        );
397
398        // check binary part
399        let binary_file = File::from(form_data.get(binary_name));
400        assert_eq!(binary_file.name(), binary_file_name);
401        assert_eq!(binary_file.type_(), binary_file_type);
402
403        // check string part
404        let string = form_data
405            .get(string_name)
406            .as_string()
407            .expect("content is not a string");
408        assert_eq!(string, string_content);
409
410        let binary_array_buffer_promise = binary_file.array_buffer();
411        let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
412            .await
413            .expect("could not get request body as array buffer");
414
415        let binary = Uint8Array::new(&array_buffer).to_vec();
416
417        assert_eq!(binary, binary_content);
418    }
419}