From a800202384484f60679cdd884b08ba1e320212e3 Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Wed, 8 Jul 2020 22:10:23 +0200 Subject: [PATCH] Add multipart for WASM (#966) --- Cargo.toml | 5 +- src/lib.rs | 2 +- src/wasm/body.rs | 51 ++++++-- src/wasm/client.rs | 5 +- src/wasm/mod.rs | 2 + src/wasm/multipart.rs | 275 ++++++++++++++++++++++++++++++++++++++++++ src/wasm/request.rs | 8 ++ 7 files changed, 335 insertions(+), 13 deletions(-) create mode 100644 src/wasm/multipart.rs diff --git a/Cargo.toml b/Cargo.toml index ea6df6e..3c46cd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ url = "2.1" bytes = "0.5" serde = "1.0" serde_urlencoded = "0.6.1" +mime_guess = "2.0" ## json serde_json = { version = "1.0", optional = true } @@ -79,7 +80,6 @@ hyper = { version = "0.13.4", default-features = false, features = ["tcp"] } lazy_static = "1.4" log = "0.4" mime = "0.3.7" -mime_guess = "2.0" percent-encoding = "2.1" tokio = { version = "0.2.5", default-features = false, features = ["tcp", "time"] } pin-project-lite = "0.1.1" @@ -141,6 +141,9 @@ features = [ "RequestMode", "Response", "Window", + "FormData", + "Blob", + "BlobPropertyBag", ] [[example]] diff --git a/src/lib.rs b/src/lib.rs index 8c78f44..3d1b1b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -307,5 +307,5 @@ if_hyper! { if_wasm! { mod wasm; - pub use self::wasm::{Body, Client, ClientBuilder, Request, RequestBuilder, Response}; + pub use self::wasm::{multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response}; } diff --git a/src/wasm/body.rs b/src/wasm/body.rs index 16ccaa4..43a2e82 100644 --- a/src/wasm/body.rs +++ b/src/wasm/body.rs @@ -1,6 +1,9 @@ +use super::multipart::Form; /// dox use bytes::Bytes; use std::fmt; +use js_sys::Uint8Array; +use wasm_bindgen::JsValue; /// The body of a `Request`. /// @@ -9,39 +12,73 @@ use std::fmt; /// passing many things (like a string or vector of bytes). /// /// [builder]: ./struct.RequestBuilder.html#method.body -pub struct Body(Bytes); +pub struct Body { + inner: Inner, +} + +enum Inner { + Bytes(Bytes), + Multipart(Form), +} impl Body { - pub(crate) fn bytes(&self) -> &Bytes { - &self.0 + pub(crate) fn to_js_value(&self) -> crate::Result { + match &self.inner { + Inner::Bytes(body_bytes) => { + let body_bytes: &[u8] = body_bytes.as_ref(); + let body_array: Uint8Array = body_bytes.into(); + let js_value: &JsValue = body_array.as_ref(); + Ok(js_value.to_owned()) + } + Inner::Multipart(form) => { + let form_data = form.to_form_data()?; + let js_value: &JsValue = form_data.as_ref(); + Ok(js_value.to_owned()) + } + } + } + + #[inline] + pub(crate) fn from_form(f: Form) -> Body { + Self { + inner: Inner::Multipart(f), + } } } impl From for Body { #[inline] fn from(bytes: Bytes) -> Body { - Body(bytes) + Body { + inner: Inner::Bytes(bytes), + } } } impl From> for Body { #[inline] fn from(vec: Vec) -> Body { - Body(vec.into()) + Body { + inner: Inner::Bytes(vec.into()), + } } } impl From<&'static [u8]> for Body { #[inline] fn from(s: &'static [u8]) -> Body { - Body(Bytes::from_static(s)) + Body { + inner: Inner::Bytes(Bytes::from_static(s)), + } } } impl From for Body { #[inline] fn from(s: String) -> Body { - Body(s.into()) + Body { + inner: Inner::Bytes(s.into()), + } } } diff --git a/src/wasm/client.rs b/src/wasm/client.rs index dda859f..b7bb141 100644 --- a/src/wasm/client.rs +++ b/src/wasm/client.rs @@ -1,5 +1,4 @@ use http::Method; -use js_sys::Uint8Array; use std::future::Future; use wasm_bindgen::UnwrapThrowExt as _; use url::Url; @@ -133,9 +132,7 @@ async fn fetch(req: Request) -> crate::Result { } if let Some(body) = req.body() { - let body_bytes: &[u8] = body.bytes(); - let body_array: Uint8Array = body_bytes.into(); - init.body(Some(&body_array.into())); + init.body(Some(&body.to_js_value()?.as_ref().as_ref())); } let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init) diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index 597c2bf..b0107d3 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -4,6 +4,8 @@ mod body; mod client; mod request; mod response; +/// TODO +pub mod multipart; pub use self::body::Body; pub use self::client::{Client, ClientBuilder}; diff --git a/src/wasm/multipart.rs b/src/wasm/multipart.rs new file mode 100644 index 0000000..09150cc --- /dev/null +++ b/src/wasm/multipart.rs @@ -0,0 +1,275 @@ +//! multipart/form-data +use std::borrow::Cow; +use std::fmt; + +use http::HeaderMap; +use mime_guess::Mime; +use web_sys::FormData; + +use super::Body; + +/// An async multipart/form-data request. +pub struct Form { + inner: FormParts, +} + +/// A field in a multipart form. +pub struct Part { + meta: PartMetadata, + value: Body, +} + +pub(crate) struct FormParts

{ + pub(crate) fields: Vec<(Cow<'static, str>, P)>, +} + +pub(crate) struct PartMetadata { + mime: Option, + file_name: Option>, + pub(crate) headers: HeaderMap, +} + +pub(crate) trait PartProps { + fn metadata(&self) -> &PartMetadata; +} + +// ===== impl Form ===== + +impl Default for Form { + fn default() -> Self { + Self::new() + } +} + +impl Form { + /// Creates a new async Form without any content. + pub fn new() -> Form { + Form { + inner: FormParts::new(), + } + } + + /// Add a data field with supplied name and value. + /// + /// # Examples + /// + /// ``` + /// let form = reqwest::multipart::Form::new() + /// .text("username", "seanmonstar") + /// .text("password", "secret"); + /// ``` + pub fn text(self, name: T, value: U) -> Form + where + T: Into>, + U: Into>, + { + self.part(name, Part::text(value)) + } + + /// Adds a customized Part. + pub fn part(self, name: T, part: Part) -> Form + where + T: Into>, + { + self.with_inner(move |inner| inner.part(name, part)) + } + + fn with_inner(self, func: F) -> Self + where + F: FnOnce(FormParts) -> FormParts, + { + Form { + inner: func(self.inner), + } + } + + pub(crate) fn to_form_data(&self) -> crate::Result { + + let form = FormData::new() + .map_err(crate::error::wasm) + .map_err(crate::error::builder)?; + + for (name, part) in self.inner.fields.iter() { + let blob = part.blob()?; + + if let Some(file_name) = &part.metadata().file_name { + form.append_with_blob_and_filename(name, &blob, &file_name) + } else { + form.append_with_blob(name, &blob) + } + .map_err(crate::error::wasm) + .map_err(crate::error::builder)?; + } + Ok(form) + } +} + +impl fmt::Debug for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt_fields("Form", f) + } +} + +// ===== impl Part ===== + +impl Part { + /// Makes a text parameter. + pub fn text(value: T) -> Part + where + T: Into>, + { + let body = match value.into() { + Cow::Borrowed(slice) => Body::from(slice), + Cow::Owned(string) => Body::from(string), + }; + Part::new(body) + } + + /// Makes a new parameter from arbitrary bytes. + pub fn bytes(value: T) -> Part + where + T: Into>, + { + let body = match value.into() { + Cow::Borrowed(slice) => Body::from(slice), + Cow::Owned(vec) => Body::from(vec), + }; + Part::new(body) + } + + /// Makes a new parameter from an arbitrary stream. + pub fn stream>(value: T) -> Part { + Part::new(value.into()) + } + + fn new(value: Body) -> Part { + Part { + meta: PartMetadata::new(), + value, + } + } + + /// Tries to set the mime of this part. + pub fn mime_str(self, mime: &str) -> crate::Result { + Ok(self.mime(mime.parse().map_err(crate::error::builder)?)) + } + + // Re-export when mime 0.4 is available, with split MediaType/MediaRange. + fn mime(self, mime: Mime) -> Part { + self.with_inner(move |inner| inner.mime(mime)) + } + + /// Sets the filename, builder style. + pub fn file_name(self, filename: T) -> Part + where + T: Into>, + { + self.with_inner(move |inner| inner.file_name(filename)) + } + + fn with_inner(self, func: F) -> Self + where + F: FnOnce(PartMetadata) -> PartMetadata, + { + Part { + meta: func(self.meta), + value: self.value, + } + } + + fn blob(&self) -> crate::Result { + use web_sys::Blob; + use web_sys::BlobPropertyBag; + let mut properties = BlobPropertyBag::new(); + if let Some(mime) = &self.meta.mime { + properties.type_(mime.as_ref()); + } + + // BUG: the return value of to_js_value() is not valid if + // it is a Multipart variant. + let js_value = self.value.to_js_value()?; + Blob::new_with_u8_array_sequence_and_options(&js_value, &properties) + .map_err(crate::error::wasm) + .map_err(crate::error::builder) + } +} + +impl fmt::Debug for Part { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut dbg = f.debug_struct("Part"); + dbg.field("value", &self.value); + self.meta.fmt_fields(&mut dbg); + dbg.finish() + } +} + +impl PartProps for Part { + fn metadata(&self) -> &PartMetadata { + &self.meta + } +} + +// ===== impl FormParts ===== + +impl FormParts

{ + pub(crate) fn new() -> Self { + FormParts { fields: Vec::new() } + } + + /// Adds a customized Part. + pub(crate) fn part(mut self, name: T, part: P) -> Self + where + T: Into>, + { + self.fields.push((name.into(), part)); + self + } +} + +impl FormParts

{ + pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct(ty_name) + .field("parts", &self.fields) + .finish() + } +} + +// ===== impl PartMetadata ===== + +impl PartMetadata { + pub(crate) fn new() -> Self { + PartMetadata { + mime: None, + file_name: None, + headers: HeaderMap::default(), + } + } + + pub(crate) fn mime(mut self, mime: Mime) -> Self { + self.mime = Some(mime); + self + } + + pub(crate) fn file_name(mut self, filename: T) -> Self + where + T: Into>, + { + self.file_name = Some(filename.into()); + self + } +} + +impl PartMetadata { + pub(crate) fn fmt_fields<'f, 'fa, 'fb>( + &self, + debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>, + ) -> &'f mut fmt::DebugStruct<'fa, 'fb> { + debug_struct + .field("mime", &self.mime) + .field("file_name", &self.file_name) + .field("headers", &self.headers) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/wasm/request.rs b/src/wasm/request.rs index 3e362e1..cce8747 100644 --- a/src/wasm/request.rs +++ b/src/wasm/request.rs @@ -190,6 +190,14 @@ impl RequestBuilder { self } + /// TODO + pub fn multipart(mut self, multipart: super::multipart::Form) -> RequestBuilder { + if let Ok(ref mut req) = self.request { + *req.body_mut() = Some(Body::from_form(multipart)) + } + self + } + /// Add a `Header` to this Request. pub fn header(mut self, key: K, value: V) -> RequestBuilder where