| @@ -16,6 +16,7 @@ hyper = "0.11" | ||||
| hyper-tls = "0.1.2" | ||||
| libflate = "0.1.11" | ||||
| log = "0.3" | ||||
| mime_guess = "2.0.0-alpha.2" | ||||
| native-tls = "0.1.3" | ||||
| serde = "1.0" | ||||
| serde_json = "1.0" | ||||
| @@ -24,11 +25,12 @@ tokio-core = "0.1.6" | ||||
| tokio-io = "0.1" | ||||
| tokio-tls = "0.1" | ||||
| url = "1.2" | ||||
| uuid = { version = "0.5", features = ["v4"] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| env_logger = "0.4" | ||||
| serde_derive = "1.0" | ||||
| error-chain = "0.10" | ||||
| serde_derive = "1.0" | ||||
|  | ||||
| [features] | ||||
| unstable = [] | ||||
|   | ||||
							
								
								
									
										83
									
								
								src/body.rs
									
									
									
									
									
								
							
							
						
						
									
										83
									
								
								src/body.rs
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| use std::io::{self, Read}; | ||||
| use std::fs::File; | ||||
| use std::fmt; | ||||
| use std::io::{self, Cursor, Read}; | ||||
|  | ||||
| use bytes::Bytes; | ||||
| use hyper::{self, Chunk}; | ||||
| @@ -16,7 +16,7 @@ use {async_impl, wait}; | ||||
| /// [builder]: ./struct.RequestBuilder.html#method.body | ||||
| #[derive(Debug)] | ||||
| pub struct Body { | ||||
|     reader: Kind, | ||||
|     kind: Kind, | ||||
| } | ||||
|  | ||||
| impl Body { | ||||
| @@ -54,7 +54,7 @@ impl Body { | ||||
|     /// ``` | ||||
|     pub fn new<R: Read + Send + 'static>(reader: R) -> Body { | ||||
|         Body { | ||||
|             reader: Kind::Reader(Box::new(reader), None), | ||||
|             kind: Kind::Reader(Box::from(reader), None), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -74,21 +74,11 @@ impl Body { | ||||
|     /// ``` | ||||
|     pub fn sized<R: Read + Send + 'static>(reader: R, len: u64) -> Body { | ||||
|         Body { | ||||
|             reader: Kind::Reader(Box::new(reader), Some(len)), | ||||
|             kind: Kind::Reader(Box::from(reader), Some(len)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // useful for tests, but not publicly exposed | ||||
| #[cfg(test)] | ||||
| pub fn read_to_string(mut body: Body) -> ::std::io::Result<String> { | ||||
|     let mut s = String::new(); | ||||
|     match body.reader { | ||||
|             Kind::Reader(ref mut reader, _) => reader.read_to_string(&mut s), | ||||
|             Kind::Bytes(ref mut bytes) => (&**bytes).read_to_string(&mut s), | ||||
|         } | ||||
|         .map(|_| s) | ||||
| } | ||||
|  | ||||
| enum Kind { | ||||
|     Reader(Box<Read + Send>, Option<u64>), | ||||
| @@ -99,7 +89,7 @@ impl From<Vec<u8>> for Body { | ||||
|     #[inline] | ||||
|     fn from(v: Vec<u8>) -> Body { | ||||
|         Body { | ||||
|             reader: Kind::Bytes(v.into()), | ||||
|             kind: Kind::Bytes(v.into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -116,7 +106,7 @@ impl From<&'static [u8]> for Body { | ||||
|     #[inline] | ||||
|     fn from(s: &'static [u8]) -> Body { | ||||
|         Body { | ||||
|             reader: Kind::Bytes(Bytes::from_static(s)), | ||||
|             kind: Kind::Bytes(Bytes::from_static(s)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -133,7 +123,7 @@ impl From<File> for Body { | ||||
|     fn from(f: File) -> Body { | ||||
|         let len = f.metadata().map(|m| m.len()).ok(); | ||||
|         Body { | ||||
|             reader: Kind::Reader(Box::new(f), len), | ||||
|             kind: Kind::Reader(Box::new(f), len), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -141,8 +131,21 @@ impl From<File> for Body { | ||||
| impl fmt::Debug for Kind { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
|         match *self { | ||||
|             Kind::Reader(_, ref v) => f.debug_tuple("Kind::Reader").field(&"_").field(v).finish(), | ||||
|             Kind::Bytes(ref v) => f.debug_tuple("Kind::Bytes").field(v).finish(), | ||||
|             Kind::Reader(_, ref v) => f.debug_struct("Reader") | ||||
|                 .field("length", &DebugLength(v)) | ||||
|                 .finish(), | ||||
|             Kind::Bytes(ref v) => fmt::Debug::fmt(v, f), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct DebugLength<'a>(&'a Option<u64>); | ||||
|  | ||||
| impl<'a> fmt::Debug for DebugLength<'a> { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
|         match *self.0 { | ||||
|             Some(ref len) => fmt::Debug::fmt(len, f), | ||||
|             None => f.write_str("Unknown"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -150,6 +153,35 @@ impl fmt::Debug for Kind { | ||||
|  | ||||
| // pub(crate) | ||||
|  | ||||
| pub fn len(body: &Body) -> Option<u64> { | ||||
|     match body.kind { | ||||
|         Kind::Reader(_, len) => len, | ||||
|         Kind::Bytes(ref bytes) => Some(bytes.len() as u64), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum Reader { | ||||
|     Reader(Box<Read + Send>), | ||||
|     Bytes(Cursor<Bytes>), | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn reader(body: Body) -> Reader { | ||||
|     match body.kind { | ||||
|         Kind::Reader(r, _) => Reader::Reader(r), | ||||
|         Kind::Bytes(b) => Reader::Bytes(Cursor::new(b)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Read for Reader { | ||||
|     fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { | ||||
|         match *self { | ||||
|             Reader::Reader(ref mut rdr) => rdr.read(buf), | ||||
|             Reader::Bytes(ref mut rdr) => rdr.read(buf), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct Sender { | ||||
|     body: (Box<Read + Send>, Option<u64>), | ||||
|     tx: wait::WaitSink<::futures::sync::mpsc::Sender<hyper::Result<Chunk>>>, | ||||
| @@ -193,7 +225,7 @@ impl Sender { | ||||
|  | ||||
| #[inline] | ||||
| pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) { | ||||
|     match body.reader { | ||||
|     match body.kind { | ||||
|         Kind::Reader(read, len) => { | ||||
|             let (tx, rx) = hyper::Body::pair(); | ||||
|             let tx = Sender { | ||||
| @@ -208,3 +240,14 @@ pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // useful for tests, but not publicly exposed | ||||
| #[cfg(test)] | ||||
| pub fn read_to_string(mut body: Body) -> io::Result<String> { | ||||
|     let mut s = String::new(); | ||||
|     match body.kind { | ||||
|             Kind::Reader(ref mut reader, _) => reader.read_to_string(&mut s), | ||||
|             Kind::Bytes(ref mut bytes) => (&**bytes).read_to_string(&mut s), | ||||
|         } | ||||
|         .map(|_| s) | ||||
| } | ||||
|   | ||||
							
								
								
									
										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}; | ||||
							
								
								
									
										437
									
								
								src/multipart_.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										437
									
								
								src/multipart_.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,437 @@ | ||||
| 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; | ||||
|  | ||||
| use {Body}; | ||||
|  | ||||
| /// A multipart/form-data request. | ||||
| 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(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the boundary that this form will use. | ||||
|     #[inline] | ||||
|     pub fn boundary(&self) -> &str { | ||||
|         &self.boundary | ||||
|     } | ||||
|  | ||||
|     /// 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: Into<Cow<'static, str>>, | ||||
|     { | ||||
|         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 ::body::len(&field.value) { | ||||
|                 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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl fmt::Debug for Form { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
|         f.debug_struct("Form") | ||||
|             .field("boundary", &self.boundary) | ||||
|             .field("parts", &self.fields) | ||||
|             .finish() | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// A field in a multipart form. | ||||
| pub struct Part { | ||||
|     value: Body, | ||||
|     mime: Option<Mime>, | ||||
|     file_name: Option<Cow<'static, str>>, | ||||
| } | ||||
|  | ||||
| impl Part { | ||||
|     /// Makes a text parameter. | ||||
|     pub fn text<T>(value: T) -> Part | ||||
|     where T: Into<Cow<'static, str>>, | ||||
|     { | ||||
|         let body = match value.into() { | ||||
|             Cow::Borrowed(slice) => Body::from(slice), | ||||
|             Cow::Owned(string) => Body::from(string), | ||||
|         }; | ||||
|         Part::new(body) | ||||
|     } | ||||
|  | ||||
|     /// Adds a generic reader. | ||||
|     /// | ||||
|     /// Does not set filename or mime. | ||||
|     pub fn reader<T: Read + Send + 'static>(value: T) -> Part { | ||||
|         Part::new(Body::new(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 { | ||||
|         Part::new(Body::sized(value, length)) | ||||
|     } | ||||
|  | ||||
|     /// 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 mut field = Part::new(Body::from(file)); | ||||
|         field.mime = Some(mime); | ||||
|         field.file_name = file_name; | ||||
|         Ok(field) | ||||
|     } | ||||
|  | ||||
|     fn new(value: Body) -> Part { | ||||
|         Part { | ||||
|             value: value, | ||||
|             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", &self.value) | ||||
|             .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(::body::reader(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 form = Form::new(); | ||||
|         let length = form.compute_length(); | ||||
|         form.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 form = 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"), | ||||
|             ); | ||||
|         form.boundary = "boundary".to_string(); | ||||
|         let length = form.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--"; | ||||
|         form.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 form = Form::new() | ||||
|             .text("key1", "value1") | ||||
|             .part( | ||||
|                 "key2", | ||||
|                 Part::text("value2").mime(::mime::IMAGE_BMP), | ||||
|             ) | ||||
|             .part( | ||||
|                 "key3", | ||||
|                 Part::text("value3").file_name("filename"), | ||||
|             ); | ||||
|         form.boundary = "boundary".to_string(); | ||||
|         let length = form.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--"; | ||||
|         form.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()`. | ||||
|     /// | ||||
|   | ||||
							
								
								
									
										50
									
								
								tests/multipart.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								tests/multipart.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| extern crate reqwest; | ||||
|  | ||||
| #[macro_use] | ||||
| mod support; | ||||
|  | ||||
| #[test] | ||||
| fn test_multipart() { | ||||
|     let form = reqwest::multipart::Form::new() | ||||
|         .text("foo", "bar"); | ||||
|  | ||||
|     let expected_body = format!("\ | ||||
|         --{0}\r\n\ | ||||
|         Content-Disposition: form-data; name=\"foo\"\r\n\r\n\ | ||||
|         bar\r\n\ | ||||
|         --{0}--\ | ||||
|     ", form.boundary()); | ||||
|  | ||||
|     let server = server! { | ||||
|         request: format!("\ | ||||
|             POST /multipart/1 HTTP/1.1\r\n\ | ||||
|             Host: $HOST\r\n\ | ||||
|             Content-Type: multipart/form-data; boundary={}\r\n\ | ||||
|             Content-Length: 123\r\n\ | ||||
|             User-Agent: $USERAGENT\r\n\ | ||||
|             Accept: */*\r\n\ | ||||
|             Accept-Encoding: gzip\r\n\ | ||||
|             \r\n\ | ||||
|             {}\ | ||||
|             ", form.boundary(), expected_body), | ||||
|         response: b"\ | ||||
|             HTTP/1.1 200 OK\r\n\ | ||||
|             Server: multipart\r\n\ | ||||
|             Content-Length: 0\r\n\ | ||||
|             \r\n\ | ||||
|             " | ||||
|     }; | ||||
|  | ||||
|     let url = format!("http://{}/multipart/1", server.addr()); | ||||
|  | ||||
|     let res = reqwest::Client::new() | ||||
|         .unwrap() | ||||
|         .post(&url) | ||||
|         .unwrap() | ||||
|         .multipart(form) | ||||
|         .send() | ||||
|         .unwrap(); | ||||
|  | ||||
|     assert_eq!(res.url().as_str(), &url); | ||||
|     assert_eq!(res.status(), reqwest::StatusCode::Ok); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user