Add multipart/form-data support
This commit is contained in:
		| @@ -16,6 +16,7 @@ hyper = "0.11" | |||||||
| hyper-tls = "0.1.2" | hyper-tls = "0.1.2" | ||||||
| libflate = "0.1.11" | libflate = "0.1.11" | ||||||
| log = "0.3" | log = "0.3" | ||||||
|  | mime_guess = "2.0.0-alpha.2" | ||||||
| native-tls = "0.1.3" | native-tls = "0.1.3" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| @@ -24,11 +25,12 @@ tokio-core = "0.1.6" | |||||||
| tokio-io = "0.1" | tokio-io = "0.1" | ||||||
| tokio-tls = "0.1" | tokio-tls = "0.1" | ||||||
| url = "1.2" | url = "1.2" | ||||||
|  | uuid = { version = "0.5", features = ["v4"] } | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| env_logger = "0.4" | env_logger = "0.4" | ||||||
| serde_derive = "1.0" |  | ||||||
| error-chain = "0.10" | error-chain = "0.10" | ||||||
|  | serde_derive = "1.0" | ||||||
|  |  | ||||||
| [features] | [features] | ||||||
| unstable = [] | unstable = [] | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ | |||||||
| //! to do for them. | //! to do for them. | ||||||
| //! | //! | ||||||
| //! - Uses system-native TLS | //! - Uses system-native TLS | ||||||
| //! - Plain bodies, JSON, urlencoded, (TODO: multipart) | //! - Plain bodies, JSON, urlencoded, multipart | ||||||
| //! - Customizable redirect policy | //! - Customizable redirect policy | ||||||
| //! - Proxies | //! - Proxies | ||||||
| //! - (TODO: Cookies) | //! - (TODO: Cookies) | ||||||
| @@ -127,6 +127,7 @@ extern crate hyper_tls; | |||||||
| #[macro_use] | #[macro_use] | ||||||
| extern crate log; | extern crate log; | ||||||
| extern crate libflate; | extern crate libflate; | ||||||
|  | extern crate mime_guess; | ||||||
| extern crate native_tls; | extern crate native_tls; | ||||||
| extern crate serde; | extern crate serde; | ||||||
| extern crate serde_json; | extern crate serde_json; | ||||||
| @@ -135,6 +136,7 @@ extern crate tokio_core; | |||||||
| extern crate tokio_io; | extern crate tokio_io; | ||||||
| extern crate tokio_tls; | extern crate tokio_tls; | ||||||
| extern crate url; | extern crate url; | ||||||
|  | extern crate uuid; | ||||||
|  |  | ||||||
| pub use hyper::header; | pub use hyper::header; | ||||||
| pub use hyper::mime; | pub use hyper::mime; | ||||||
| @@ -158,6 +160,21 @@ pub use self::tls::Certificate; | |||||||
| #[macro_use] | #[macro_use] | ||||||
| mod error; | mod error; | ||||||
|  |  | ||||||
|  | mod async_impl; | ||||||
|  | mod connect; | ||||||
|  | mod body; | ||||||
|  | mod client; | ||||||
|  | mod into_url; | ||||||
|  | mod multipart_; | ||||||
|  | mod proxy; | ||||||
|  | mod redirect; | ||||||
|  | mod request; | ||||||
|  | mod response; | ||||||
|  | mod tls; | ||||||
|  | mod wait; | ||||||
|  |  | ||||||
|  | pub mod multipart; | ||||||
|  |  | ||||||
| /// A set of unstable functionality. | /// A set of unstable functionality. | ||||||
| /// | /// | ||||||
| /// This module is only available when the `unstable` feature is enabled. | /// This module is only available when the `unstable` feature is enabled. | ||||||
| @@ -182,20 +199,6 @@ pub mod unstable { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| mod async_impl; |  | ||||||
| mod connect; |  | ||||||
| mod body; |  | ||||||
| mod client; |  | ||||||
| mod into_url; |  | ||||||
| mod proxy; |  | ||||||
| mod redirect; |  | ||||||
| mod request; |  | ||||||
| mod response; |  | ||||||
| mod tls; |  | ||||||
| mod wait; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /// Shortcut method to quickly make a `GET` request. | /// Shortcut method to quickly make a `GET` request. | ||||||
| /// | /// | ||||||
| /// See also the methods on the [`reqwest::Response`](./struct.Response.html) | /// See also the methods on the [`reqwest::Response`](./struct.Response.html) | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/multipart.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/multipart.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | //! # multipart/form-data | ||||||
|  | pub use ::multipart_::{Form, Part}; | ||||||
							
								
								
									
										424
									
								
								src/multipart_.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										424
									
								
								src/multipart_.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,424 @@ | |||||||
|  | use std::borrow::Cow; | ||||||
|  | use std::fmt; | ||||||
|  | use std::fs::File; | ||||||
|  | use std::io::{self, Cursor, Read}; | ||||||
|  | use std::path::Path; | ||||||
|  |  | ||||||
|  | use mime::Mime; | ||||||
|  | use mime_guess; | ||||||
|  | use url::percent_encoding; | ||||||
|  | use uuid::Uuid; | ||||||
|  |  | ||||||
|  | /// A multipart/form-data request. | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct Form { | ||||||
|  |     boundary: String, | ||||||
|  |     fields: Vec<(Cow<'static, str>, Part)>, | ||||||
|  |     headers: Vec<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Form { | ||||||
|  |     /// Creates a new Form without any content. | ||||||
|  |     pub fn new() -> Form { | ||||||
|  |         Form { | ||||||
|  |             boundary: format!("{}", Uuid::new_v4().simple()), | ||||||
|  |             fields: Vec::new(), | ||||||
|  |             headers: Vec::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<T, U>(self, name: T, value: U) -> Form | ||||||
|  |     where T: Into<Cow<'static, str>>, | ||||||
|  |           U: AsRef<[u8]> + Send + 'static | ||||||
|  |     { | ||||||
|  |         self.part(name, Part::text(value)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Adds a file field. | ||||||
|  |     /// | ||||||
|  |     /// The path will be used to try to guess the filename and mime. | ||||||
|  |     /// | ||||||
|  |     /// # Examples | ||||||
|  |     /// | ||||||
|  |     /// ```no_run | ||||||
|  |     /// # fn run() -> ::std::io::Result<()> { | ||||||
|  |     /// let files = reqwest::multipart::Form::new() | ||||||
|  |     ///     .file("key", "/path/to/file")?; | ||||||
|  |     /// # Ok(()) | ||||||
|  |     /// # } | ||||||
|  |     /// ``` | ||||||
|  |     /// | ||||||
|  |     /// # Errors | ||||||
|  |     /// | ||||||
|  |     /// Errors when the file cannot be opened. | ||||||
|  |     pub fn file<T, U>(self, name: T, path: U) -> io::Result<Form> | ||||||
|  |     where T: Into<Cow<'static, str>>, | ||||||
|  |           U: AsRef<Path> | ||||||
|  |     { | ||||||
|  |         Ok(self.part(name, Part::file(path)?)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Adds a customized Part. | ||||||
|  |     pub fn part<T>(mut self, name: T, part: Part) -> Form | ||||||
|  |     where T: Into<Cow<'static, str>>, | ||||||
|  |     { | ||||||
|  |         self.fields.push((name.into(), part)); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Form { | ||||||
|  |     fn reader(self) -> Reader { | ||||||
|  |         Reader::new(self) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn compute_length(&mut self) -> Option<u64> { | ||||||
|  |         let mut length = 0u64; | ||||||
|  |         for &(ref name, ref field) in self.fields.iter() { | ||||||
|  |             match field.value_length { | ||||||
|  |                 Some(value_length) => { | ||||||
|  |                     // We are constructing the header just to get its length. To not have to | ||||||
|  |                     // construct it again when the request is sent we cache these headers. | ||||||
|  |                     let header = header(name, field); | ||||||
|  |                     let header_length = header.len(); | ||||||
|  |                     self.headers.push(header); | ||||||
|  |                     // The additions mimick the format string out of which the field is constructed | ||||||
|  |                     // in Reader. Not the cleanest solution because if that format string is | ||||||
|  |                     // ever changed then this formula needs to be changed too which is not an | ||||||
|  |                     // obvious dependency in the code. | ||||||
|  |                     length += 2 + self.boundary.len() as u64 + 2 + header_length as u64 + 4 + value_length + 2 | ||||||
|  |                 } | ||||||
|  |                 _ => return None, | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // If there is a at least one field there is a special boundary for the very last field. | ||||||
|  |         if self.fields.len() != 0 { | ||||||
|  |             length += 2 + self.boundary.len() as u64 + 2 | ||||||
|  |         } | ||||||
|  |         Some(length) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// A field in a multipart form. | ||||||
|  | pub struct Part { | ||||||
|  |     value: Box<Read + Send>, | ||||||
|  |     value_length: Option<u64>, | ||||||
|  |     mime: Option<Mime>, | ||||||
|  |     file_name: Option<Cow<'static, str>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Part { | ||||||
|  |     /// Makes a text parameter. | ||||||
|  |     pub fn text<T: AsRef<[u8]> + Send + 'static>(value: T) -> Part { | ||||||
|  |         let value_length = Some(value.as_ref().len() as u64); | ||||||
|  |         let mut field = Part::new(Box::new(Cursor::new(value))); | ||||||
|  |         field.value_length = value_length; | ||||||
|  |         field | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Adds a generic reader. | ||||||
|  |     /// | ||||||
|  |     /// Does not set filename or mime. | ||||||
|  |     pub fn reader<T: Read + Send + 'static>(value: T) -> Part { | ||||||
|  |         Part::new(Box::from(value)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Adds a generic reader with known length. | ||||||
|  |     /// | ||||||
|  |     /// Does not set filename or mime. | ||||||
|  |     pub fn reader_with_length<T: Read + Send + 'static>(value: T, length: u64) -> Part { | ||||||
|  |         let mut field = Part::new(Box::new(value)); | ||||||
|  |         field.value_length = Some(length); | ||||||
|  |         field | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Makes a file parameter. | ||||||
|  |     /// | ||||||
|  |     /// # Errors | ||||||
|  |     /// | ||||||
|  |     /// Errors when the file cannot be opened. | ||||||
|  |     pub fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> { | ||||||
|  |         let path = path.as_ref(); | ||||||
|  |         let file_name = path.file_name().and_then(|filename| { | ||||||
|  |             Some(Cow::from(filename.to_string_lossy().into_owned())) | ||||||
|  |         }); | ||||||
|  |         let ext = path.extension() | ||||||
|  |             .and_then(|ext| ext.to_str()) | ||||||
|  |             .unwrap_or(""); | ||||||
|  |         let mime = mime_guess::get_mime_type(ext); | ||||||
|  |         let file = File::open(path)?; | ||||||
|  |         let file_length = file.metadata().ok().map(|meta| meta.len()); | ||||||
|  |         let mut field = Part::new(Box::new(file)); | ||||||
|  |         field.value_length = file_length; | ||||||
|  |         field.mime = Some(mime); | ||||||
|  |         field.file_name = file_name; | ||||||
|  |         Ok(field) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn new(value: Box<Read + Send + 'static>) -> Part { | ||||||
|  |         Part { | ||||||
|  |             value: value, | ||||||
|  |             value_length: None, | ||||||
|  |             mime: None, | ||||||
|  |             file_name: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Sets the mime, builder style. | ||||||
|  |     pub fn mime(mut self, mime: Mime) -> Part { | ||||||
|  |         self.mime = Some(mime); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Sets the filename, builder style. | ||||||
|  |     pub fn file_name<T: Into<Cow<'static, str>>>(mut self, filename: T) -> Part { | ||||||
|  |         self.file_name = Some(filename.into()); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl fmt::Debug for Part { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||||
|  |         f.debug_struct("Part") | ||||||
|  |             .field("value_length", &self.value_length) | ||||||
|  |             .field("mime", &self.mime) | ||||||
|  |             .field("file_name", &self.file_name) | ||||||
|  |             .finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // pub(crate) | ||||||
|  |  | ||||||
|  | // Turns this Form into a Reader which implements the Read trait. | ||||||
|  | pub fn reader(form: Form) -> Reader { | ||||||
|  |     form.reader() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // If predictable, computes the length the request will have | ||||||
|  | // The length should be preditable if only String and file fields have been added, | ||||||
|  | // but not if a generic reader has been added; | ||||||
|  | pub fn compute_length(form: &mut Form) -> Option<u64> { | ||||||
|  |     form.compute_length() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn boundary(form: &Form) -> &str { | ||||||
|  |     &form.boundary | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct Reader { | ||||||
|  |     form: Form, | ||||||
|  |     active_reader: Option<Box<Read + Send>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl fmt::Debug for Reader { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||||
|  |         f.debug_struct("Reader") | ||||||
|  |             .field("form", &self.form) | ||||||
|  |             .finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Reader { | ||||||
|  |     fn new(form: Form) -> Reader { | ||||||
|  |         let mut reader = Reader { | ||||||
|  |             form: form, | ||||||
|  |             active_reader: None, | ||||||
|  |         }; | ||||||
|  |         reader.next_reader(); | ||||||
|  |         reader | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn next_reader(&mut self) { | ||||||
|  |         self.active_reader = if self.form.fields.len() != 0 { | ||||||
|  |             // We need to move out of the vector here because we are consuming the field's reader | ||||||
|  |             let (name, field) = self.form.fields.remove(0); | ||||||
|  |             let reader = Cursor::new(format!( | ||||||
|  |                 "--{}\r\n{}\r\n\r\n", | ||||||
|  |                 self.form.boundary, | ||||||
|  |                 // Try to use cached headers created by compute_length | ||||||
|  |                 if self.form.headers.len() > 0 { | ||||||
|  |                     self.form.headers.remove(0) | ||||||
|  |                 } else { | ||||||
|  |                     header(&name, &field) | ||||||
|  |                 } | ||||||
|  |             )).chain(field.value) | ||||||
|  |                 .chain(Cursor::new("\r\n")); | ||||||
|  |             // According to https://tools.ietf.org/html/rfc2046#section-5.1.1 | ||||||
|  |             // the very last field has a special boundary | ||||||
|  |             if self.form.fields.len() != 0 { | ||||||
|  |                 Some(Box::new(reader)) | ||||||
|  |             } else { | ||||||
|  |                 Some(Box::new(reader.chain(Cursor::new( | ||||||
|  |                     format!("--{}--", self.form.boundary), | ||||||
|  |                 )))) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Read for Reader { | ||||||
|  |     fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { | ||||||
|  |         let mut total_bytes_read = 0usize; | ||||||
|  |         let mut last_read_bytes; | ||||||
|  |         loop { | ||||||
|  |             match self.active_reader { | ||||||
|  |                 Some(ref mut reader) => { | ||||||
|  |                     last_read_bytes = reader.read(&mut buf[total_bytes_read..])?; | ||||||
|  |                     total_bytes_read += last_read_bytes; | ||||||
|  |                     if total_bytes_read == buf.len() { | ||||||
|  |                         return Ok(total_bytes_read); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 None => return Ok(total_bytes_read), | ||||||
|  |             }; | ||||||
|  |             if last_read_bytes == 0 && buf.len() != 0 { | ||||||
|  |                 self.next_reader(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fn header(name: &str, field: &Part) -> String { | ||||||
|  |     format!( | ||||||
|  |         "Content-Disposition: form-data; {}{}{}", | ||||||
|  |         format_parameter("name", name), | ||||||
|  |         match field.file_name { | ||||||
|  |             Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)), | ||||||
|  |             None => String::new(), | ||||||
|  |         }, | ||||||
|  |         match field.mime { | ||||||
|  |             Some(ref mime) => format!("\r\nContent-Type: {}", mime), | ||||||
|  |             None => "".to_string(), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn format_parameter(name: &str, value: &str) -> String { | ||||||
|  |     let legal_value = | ||||||
|  |         percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET) | ||||||
|  |             .to_string(); | ||||||
|  |     if value.len() == legal_value.len() { | ||||||
|  |         // nothing has been percent encoded | ||||||
|  |         format!("{}=\"{}\"", name, value) | ||||||
|  |     } else { | ||||||
|  |         // something has been percent encoded | ||||||
|  |         format!("{}*=utf-8''{}", name, legal_value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn form_empty() { | ||||||
|  |         let mut output = Vec::new(); | ||||||
|  |         let mut request = Form::new(); | ||||||
|  |         let length = request.compute_length(); | ||||||
|  |         request.reader().read_to_end(&mut output).unwrap(); | ||||||
|  |         assert_eq!(output, b""); | ||||||
|  |         assert_eq!(length.unwrap(), 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn read_to_end() { | ||||||
|  |         let mut output = Vec::new(); | ||||||
|  |         let mut request = Form::new() | ||||||
|  |             .part("reader1", Part::reader(::std::io::empty())) | ||||||
|  |             .part("key1", Part::text("value1")) | ||||||
|  |             .part( | ||||||
|  |                 "key2", | ||||||
|  |                 Part::text("value2").mime(::mime::IMAGE_BMP), | ||||||
|  |             ) | ||||||
|  |             .part("reader2", Part::reader(::std::io::empty())) | ||||||
|  |             .part( | ||||||
|  |                 "key3", | ||||||
|  |                 Part::text("value3").file_name("filename"), | ||||||
|  |             ); | ||||||
|  |         request.boundary = "boundary".to_string(); | ||||||
|  |         let length = request.compute_length(); | ||||||
|  |         let expected = "--boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\ | ||||||
|  |                         \r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ | ||||||
|  |                         value1\r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key2\"\r\n\ | ||||||
|  |                         Content-Type: image/bmp\r\n\r\n\ | ||||||
|  |                         value2\r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\ | ||||||
|  |                         \r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ | ||||||
|  |                         value3\r\n--boundary--"; | ||||||
|  |         request.reader().read_to_end(&mut output).unwrap(); | ||||||
|  |         // These prints are for debug purposes in case the test fails | ||||||
|  |         println!( | ||||||
|  |             "START REAL\n{}\nEND REAL", | ||||||
|  |             ::std::str::from_utf8(&output).unwrap() | ||||||
|  |         ); | ||||||
|  |         println!("START EXPECTED\n{}\nEND EXPECTED", expected); | ||||||
|  |         assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); | ||||||
|  |         assert!(length.is_none()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn read_to_end_with_length() { | ||||||
|  |         let mut output = Vec::new(); | ||||||
|  |         let mut request = Form::new() | ||||||
|  |             .text("key1", "value1") | ||||||
|  |             .part( | ||||||
|  |                 "key2", | ||||||
|  |                 Part::text("value2").mime(::mime::IMAGE_BMP), | ||||||
|  |             ) | ||||||
|  |             .part( | ||||||
|  |                 "key3", | ||||||
|  |                 Part::text("value3").file_name("filename"), | ||||||
|  |             ); | ||||||
|  |         request.boundary = "boundary".to_string(); | ||||||
|  |         let length = request.compute_length(); | ||||||
|  |         let expected = "--boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ | ||||||
|  |                         value1\r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key2\"\r\n\ | ||||||
|  |                         Content-Type: image/bmp\r\n\r\n\ | ||||||
|  |                         value2\r\n\ | ||||||
|  |                         --boundary\r\n\ | ||||||
|  |                         Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ | ||||||
|  |                         value3\r\n--boundary--"; | ||||||
|  |         request.reader().read_to_end(&mut output).unwrap(); | ||||||
|  |         // These prints are for debug purposes in case the test fails | ||||||
|  |         println!( | ||||||
|  |             "START REAL\n{}\nEND REAL", | ||||||
|  |             ::std::str::from_utf8(&output).unwrap() | ||||||
|  |         ); | ||||||
|  |         println!("START EXPECTED\n{}\nEND EXPECTED", expected); | ||||||
|  |         assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); | ||||||
|  |         assert_eq!(length.unwrap(), expected.len() as u64); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn header_percent_encoding() { | ||||||
|  |         let name = "start%'\"\r\nßend"; | ||||||
|  |         let field = Part::text(""); | ||||||
|  |         let expected = "Content-Disposition: form-data; name*=utf-8''start%25\'%22%0D%0A%C3%9Fend"; | ||||||
|  |  | ||||||
|  |         assert_eq!(header(name, &field), expected); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -277,6 +277,42 @@ impl RequestBuilder { | |||||||
|         Ok(self) |         Ok(self) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Sends a multipart/form-data body. | ||||||
|  |     /// | ||||||
|  |     /// ``` | ||||||
|  |     /// use reqwest::mime; | ||||||
|  |     /// # use reqwest::Error; | ||||||
|  |     /// | ||||||
|  |     /// # fn run() -> Result<(), Box<std::error::Error>> { | ||||||
|  |     /// let client = reqwest::Client::new()?; | ||||||
|  |     /// let form = reqwest::multipart::Form::new() | ||||||
|  |     ///     .text("key3", "value3") | ||||||
|  |     ///     .file("file", "/path/to/field")?; | ||||||
|  |     /// | ||||||
|  |     /// let response = client.post("your url")? | ||||||
|  |     ///     .multipart(form) | ||||||
|  |     ///     .send()?; | ||||||
|  |     /// # Ok(()) | ||||||
|  |     /// # } | ||||||
|  |     /// ``` | ||||||
|  |     /// | ||||||
|  |     /// See [`multipart`](multipart/) for more examples. | ||||||
|  |     pub fn multipart(&mut self, mut multipart: ::multipart::Form) -> &mut RequestBuilder { | ||||||
|  |         { | ||||||
|  |             let req = self.request_mut(); | ||||||
|  |             req.headers_mut().set( | ||||||
|  |                 ::header::ContentType(format!("multipart/form-data; boundary={}", ::multipart_::boundary(&multipart)) | ||||||
|  |                     .parse().unwrap() | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |             *req.body_mut() = Some(match ::multipart_::compute_length(&mut multipart) { | ||||||
|  |                 Some(length) => Body::sized(::multipart_::reader(multipart), length), | ||||||
|  |                 None => Body::new(::multipart_::reader(multipart)), | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// Build a `Request`, which can be inspected, modified and executed with |     /// Build a `Request`, which can be inspected, modified and executed with | ||||||
|     /// `Client::execute()`. |     /// `Client::execute()`. | ||||||
|     /// |     /// | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user