Add multipart/form-data support

This commit is contained in:
e00E
2017-07-09 14:23:32 +02:00
committed by Sean McArthur
parent fe8c7a2d01
commit 93c8321305
5 changed files with 483 additions and 16 deletions

View File

@@ -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 = []

View File

@@ -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
View File

@@ -0,0 +1,2 @@
//! # multipart/form-data
pub use ::multipart_::{Form, Part};

424
src/multipart_.rs Normal file
View 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);
}
}

View File

@@ -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()`.
///