Add multipart/form-data support
This commit is contained in:
		
							
								
								
									
										33
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ | ||||
| //! to do for them. | ||||
| //! | ||||
| //! - Uses system-native TLS | ||||
| //! - Plain bodies, JSON, urlencoded, (TODO: multipart) | ||||
| //! - Plain bodies, JSON, urlencoded, multipart | ||||
| //! - Customizable redirect policy | ||||
| //! - Proxies | ||||
| //! - (TODO: Cookies) | ||||
| @@ -127,6 +127,7 @@ extern crate hyper_tls; | ||||
| #[macro_use] | ||||
| extern crate log; | ||||
| extern crate libflate; | ||||
| extern crate mime_guess; | ||||
| extern crate native_tls; | ||||
| extern crate serde; | ||||
| extern crate serde_json; | ||||
| @@ -135,6 +136,7 @@ extern crate tokio_core; | ||||
| extern crate tokio_io; | ||||
| extern crate tokio_tls; | ||||
| extern crate url; | ||||
| extern crate uuid; | ||||
|  | ||||
| pub use hyper::header; | ||||
| pub use hyper::mime; | ||||
| @@ -158,6 +160,21 @@ pub use self::tls::Certificate; | ||||
| #[macro_use] | ||||
| 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. | ||||
| /// | ||||
| /// 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. | ||||
| /// | ||||
| /// 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) | ||||
|     } | ||||
|  | ||||
|     /// 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 | ||||
|     /// `Client::execute()`. | ||||
|     /// | ||||
|   | ||||
		Reference in New Issue
	
	Block a user