add async multipart request handling
This commit is contained in:
committed by
Sean McArthur
parent
11d7812e88
commit
4c21127f15
@@ -182,6 +182,12 @@ impl IntoIterator for Chunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Chunk> for hyper::Chunk {
|
||||||
|
fn from(val: Chunk) -> hyper::Chunk {
|
||||||
|
val.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Body {
|
impl fmt::Debug for Body {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_struct("Body")
|
f.debug_struct("Body")
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ pub use self::response::{Response, ResponseBuilderExt};
|
|||||||
pub mod body;
|
pub mod body;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod decoder;
|
pub mod decoder;
|
||||||
|
pub mod multipart;
|
||||||
pub(crate) mod request;
|
pub(crate) mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
|||||||
332
src/async_impl/multipart.rs
Normal file
332
src/async_impl/multipart.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
//! multipart/form-data
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use mime_guess::Mime;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use http::HeaderMap;
|
||||||
|
|
||||||
|
use futures::Stream;
|
||||||
|
|
||||||
|
use super::Body;
|
||||||
|
use multipart::{PercentEncoding, PartProp};
|
||||||
|
|
||||||
|
/// An async multipart/form-data request.
|
||||||
|
pub struct Form {
|
||||||
|
boundary: String,
|
||||||
|
fields: Vec<(Cow<'static, str>, Part)>,
|
||||||
|
percent_encoding: PercentEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Form {
|
||||||
|
/// Creates a new async Form without any content.
|
||||||
|
pub fn new() -> Form {
|
||||||
|
Form {
|
||||||
|
boundary: format!("{}", Uuid::new_v4().to_simple()),
|
||||||
|
fields: Vec::new(),
|
||||||
|
percent_encoding: PercentEncoding::PathSegment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::async::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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
||||||
|
pub fn percent_encode_path_segment(mut self) -> Form {
|
||||||
|
self.percent_encoding = PercentEncoding::PathSegment;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
||||||
|
pub fn percent_encode_attr_chars(mut self) -> Form {
|
||||||
|
self.percent_encoding = PercentEncoding::AttrChar;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this instance and transform into an instance of hyper::Body for use in a request.
|
||||||
|
pub(crate) fn stream(mut self) -> hyper::Body {
|
||||||
|
if self.fields.len() == 0 {
|
||||||
|
return hyper::Body::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create initial part to init reduce chain
|
||||||
|
let (name, part) = self.fields.remove(0);
|
||||||
|
let start = self.part_stream(name, part);
|
||||||
|
|
||||||
|
let fields = self.take_fields();
|
||||||
|
// for each field, chain an additional stream
|
||||||
|
let stream = fields.into_iter().fold(start, |memo, (name, part)| {
|
||||||
|
let part_stream = self.part_stream(name, part);
|
||||||
|
hyper::Body::wrap_stream(memo.chain(part_stream))
|
||||||
|
});
|
||||||
|
// append special ending boundary
|
||||||
|
let last = hyper::Body::from(format!("--{}--\r\n", self.boundary));
|
||||||
|
hyper::Body::wrap_stream(stream.chain(last))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a hyper::Body stream for a single Part instance of a Form request.
|
||||||
|
pub(crate) fn part_stream<T: Into<Cow<'static, str>>>(&mut self, name: T, part: Part) -> hyper::Body {
|
||||||
|
// start with boundary
|
||||||
|
let boundary = hyper::Body::from(format!("--{}\r\n", self.boundary));
|
||||||
|
// append headers
|
||||||
|
let header = hyper::Body::from({
|
||||||
|
let mut h = self.percent_encoding.encode_headers(&name.into(), &part);
|
||||||
|
h.extend_from_slice(b"\r\n\r\n");
|
||||||
|
h
|
||||||
|
});
|
||||||
|
// then append form data followed by terminating CRLF
|
||||||
|
hyper::Body::wrap_stream(boundary.chain(header).chain(hyper::Body::wrap_stream(part.value)).chain(hyper::Body::from("\r\n".to_owned())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the fields vector of this instance, replacing with an empty vector.
|
||||||
|
fn take_fields(&mut self) -> Vec<(Cow<'static, str>, Part)> {
|
||||||
|
std::mem::replace(&mut self.fields, Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new parameter from arbitrary bytes
|
||||||
|
pub fn bytes<T>(value: T) -> Part
|
||||||
|
where T: Into<Cow<'static, [u8]>>
|
||||||
|
{
|
||||||
|
let body = match value.into() {
|
||||||
|
Cow::Borrowed(slice) => Body::from(slice),
|
||||||
|
Cow::Owned(vec) => Body::from(vec),
|
||||||
|
};
|
||||||
|
Part::new(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new parameter from an arbitrary stream.
|
||||||
|
pub fn stream<T: Stream + Send + 'static>(value: T) -> Part
|
||||||
|
where hyper::Chunk: std::convert::From<<T as futures::Stream>::Item>,
|
||||||
|
<T as futures::Stream>::Error: std::error::Error + Send + Sync {
|
||||||
|
Part::new(Body::wrap(hyper::Body::wrap_stream(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(value: Body) -> Part {
|
||||||
|
Part {
|
||||||
|
value: value,
|
||||||
|
mime: None,
|
||||||
|
file_name: None,
|
||||||
|
headers: HeaderMap::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to set the mime of this part.
|
||||||
|
pub fn mime_str(mut self, mime: &str) -> ::Result<Part> {
|
||||||
|
self.mime = Some(try_!(mime.parse()));
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable when mime 0.4 is available, with split MediaType/MediaRange.
|
||||||
|
#[cfg(test)]
|
||||||
|
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 PartProp for Part {
|
||||||
|
fn mime(&self) -> &Option<Mime> {
|
||||||
|
&self.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mime_mut(&mut self) -> &mut Option<Mime> {
|
||||||
|
&mut self.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name(&self) -> &Option<Cow<'static, str>> {
|
||||||
|
&self.file_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>> {
|
||||||
|
&mut self.file_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers(&self) -> &HeaderMap {
|
||||||
|
&self.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
|
&mut self.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.field("headers", &self.headers)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_empty() {
|
||||||
|
let form = Form::new();
|
||||||
|
|
||||||
|
let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
|
||||||
|
let body_ft = form.stream();
|
||||||
|
|
||||||
|
let out = rt.block_on(body_ft.map(|c| c.into_bytes()).concat2());
|
||||||
|
assert_eq!(out.unwrap(), Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_to_end() {
|
||||||
|
let mut form = Form::new()
|
||||||
|
.part("reader1", Part::stream(futures::stream::once::<_, hyper::Error>(Ok(hyper::Chunk::from("part1".to_owned())))))
|
||||||
|
.part("key1", Part::text("value1"))
|
||||||
|
.part(
|
||||||
|
"key2",
|
||||||
|
Part::text("value2").mime(::mime::IMAGE_BMP),
|
||||||
|
)
|
||||||
|
.part("reader2", Part::stream(futures::stream::once::<_, hyper::Error>(Ok(hyper::Chunk::from("part2".to_owned())))))
|
||||||
|
.part(
|
||||||
|
"key3",
|
||||||
|
Part::text("value3").file_name("filename"),
|
||||||
|
);
|
||||||
|
form.boundary = "boundary".to_string();
|
||||||
|
let expected = "--boundary\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
||||||
|
part1\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\
|
||||||
|
part2\r\n\
|
||||||
|
--boundary\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
|
||||||
|
value3\r\n--boundary--\r\n";
|
||||||
|
let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
|
||||||
|
let body_ft = form.stream();
|
||||||
|
|
||||||
|
let out = rt.block_on(body_ft.map(|c| c.into_bytes()).concat2()).unwrap();
|
||||||
|
// These prints are for debug purposes in case the test fails
|
||||||
|
println!(
|
||||||
|
"START REAL\n{}\nEND REAL",
|
||||||
|
::std::str::from_utf8(&out).unwrap()
|
||||||
|
);
|
||||||
|
println!("START EXPECTED\n{}\nEND EXPECTED", expected);
|
||||||
|
assert_eq!(::std::str::from_utf8(&out).unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_to_end_with_header() {
|
||||||
|
let mut part = Part::text("value2").mime(::mime::IMAGE_BMP);
|
||||||
|
part.headers_mut().insert("Hdr3", "/a/b/c".parse().unwrap());
|
||||||
|
let mut form = Form::new().part("key2", part);
|
||||||
|
form.boundary = "boundary".to_string();
|
||||||
|
let expected = "--boundary\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"key2\"\r\n\
|
||||||
|
Content-Type: image/bmp\r\n\
|
||||||
|
hdr3: /a/b/c\r\n\
|
||||||
|
\r\n\
|
||||||
|
value2\r\n\
|
||||||
|
--boundary--\r\n";
|
||||||
|
let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
|
||||||
|
let body_ft = form.stream();
|
||||||
|
|
||||||
|
let out = rt.block_on(body_ft.map(|c| c.into_bytes()).concat2()).unwrap();
|
||||||
|
// These prints are for debug purposes in case the test fails
|
||||||
|
println!(
|
||||||
|
"START REAL\n{}\nEND REAL",
|
||||||
|
::std::str::from_utf8(&out).unwrap()
|
||||||
|
);
|
||||||
|
println!("START EXPECTED\n{}\nEND EXPECTED", expected);
|
||||||
|
assert_eq!(::std::str::from_utf8(&out).unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_percent_encoding() {
|
||||||
|
let name = "start%'\"\r\nßend";
|
||||||
|
let field = Part::text("");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PercentEncoding::PathSegment.encode_headers(name, &field),
|
||||||
|
&b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PercentEncoding::AttrChar.encode_headers(name, &field),
|
||||||
|
&b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use serde_json;
|
|||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use super::body::{Body};
|
use super::body::{Body};
|
||||||
|
use super::multipart as multipart_;
|
||||||
use super::client::{Client, Pending};
|
use super::client::{Client, Pending};
|
||||||
use header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
|
use header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
|
||||||
use http::HttpTryFrom;
|
use http::HttpTryFrom;
|
||||||
@@ -179,6 +180,47 @@ impl RequestBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a multipart/form-data body.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # extern crate futures;
|
||||||
|
/// # extern crate reqwest;
|
||||||
|
///
|
||||||
|
/// # use reqwest::Error;
|
||||||
|
/// # use futures::future::Future;
|
||||||
|
///
|
||||||
|
/// # fn run() -> Result<(), Error> {
|
||||||
|
/// let client = reqwest::async::Client::new();
|
||||||
|
/// let form = reqwest::async::multipart::Form::new()
|
||||||
|
/// .text("key3", "value3")
|
||||||
|
/// .text("key4", "value4");
|
||||||
|
///
|
||||||
|
/// let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
|
||||||
|
///
|
||||||
|
/// let response = client.post("your url")
|
||||||
|
/// .multipart(form)
|
||||||
|
/// .send()
|
||||||
|
/// .and_then(|_| {
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// rt.block_on(response)
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn multipart(self, multipart: multipart_::Form) -> RequestBuilder {
|
||||||
|
let mut builder = self.header(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
format!(
|
||||||
|
"multipart/form-data; boundary={}",
|
||||||
|
multipart.boundary()
|
||||||
|
).as_str()
|
||||||
|
);
|
||||||
|
if let Ok(ref mut req) = builder.request {
|
||||||
|
*req.body_mut() = Some(Body::wrap(multipart.stream()))
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
/// Modify the query string of the URL.
|
/// Modify the query string of the URL.
|
||||||
///
|
///
|
||||||
/// Modifies the URL of this request, adding the parameters provided.
|
/// Modifies the URL of this request, adding the parameters provided.
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ pub mod async {
|
|||||||
RequestBuilder,
|
RequestBuilder,
|
||||||
Response,
|
Response,
|
||||||
ResponseBuilderExt,
|
ResponseBuilderExt,
|
||||||
|
multipart
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub struct Form {
|
|||||||
percent_encoding: PercentEncoding,
|
percent_encoding: PercentEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PercentEncoding {
|
pub(crate) enum PercentEncoding {
|
||||||
PathSegment,
|
PathSegment,
|
||||||
AttrChar,
|
AttrChar,
|
||||||
}
|
}
|
||||||
@@ -145,6 +145,15 @@ impl fmt::Debug for Form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PartProp {
|
||||||
|
fn mime(&self) -> &Option<Mime>;
|
||||||
|
fn mime_mut(&mut self) -> &mut Option<Mime>;
|
||||||
|
fn file_name(&self) -> &Option<Cow<'static, str>>;
|
||||||
|
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>>;
|
||||||
|
fn headers(&self) -> &HeaderMap;
|
||||||
|
fn headers_mut(&mut self) -> &mut HeaderMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A field in a multipart form.
|
/// A field in a multipart form.
|
||||||
pub struct Part {
|
pub struct Part {
|
||||||
@@ -240,13 +249,31 @@ impl Part {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the map with additional header fields
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
}
|
||||||
|
|
||||||
|
impl PartProp for Part {
|
||||||
|
fn mime(&self) -> &Option<Mime> {
|
||||||
|
&self.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mime_mut(&mut self) -> &mut Option<Mime> {
|
||||||
|
&mut self.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name(&self) -> &Option<Cow<'static, str>> {
|
||||||
|
&self.file_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>> {
|
||||||
|
&mut self.file_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers(&self) -> &HeaderMap {
|
||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the map with additional header fields
|
fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
||||||
&mut self.headers
|
&mut self.headers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +369,7 @@ impl Read for Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AttrCharEncodeSet;
|
pub(crate) struct AttrCharEncodeSet;
|
||||||
|
|
||||||
impl EncodeSet for AttrCharEncodeSet {
|
impl EncodeSet for AttrCharEncodeSet {
|
||||||
fn contains(&self, ch: u8) -> bool {
|
fn contains(&self, ch: u8) -> bool {
|
||||||
@@ -369,20 +396,20 @@ impl EncodeSet for AttrCharEncodeSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PercentEncoding {
|
impl PercentEncoding {
|
||||||
fn encode_headers(&self, name: &str, field: &Part) -> Vec<u8> {
|
pub fn encode_headers<T: PartProp>(&self, name: &str, field: &T) -> Vec<u8> {
|
||||||
let s = format!(
|
let s = format!(
|
||||||
"Content-Disposition: form-data; {}{}{}",
|
"Content-Disposition: form-data; {}{}{}",
|
||||||
self.format_parameter("name", name),
|
self.format_parameter("name", name),
|
||||||
match field.file_name {
|
match *field.file_name() {
|
||||||
Some(ref file_name) => format!("; {}", self.format_parameter("filename", file_name)),
|
Some(ref file_name) => format!("; {}", self.format_parameter("filename", file_name)),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
},
|
},
|
||||||
match field.mime {
|
match *field.mime() {
|
||||||
Some(ref mime) => format!("\r\nContent-Type: {}", mime),
|
Some(ref mime) => format!("\r\nContent-Type: {}", mime),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
field.headers.iter().fold(s.into_bytes(), |mut header, (k,v)| {
|
field.headers().iter().fold(s.into_bytes(), |mut header, (k,v)| {
|
||||||
header.extend_from_slice(b"\r\n");
|
header.extend_from_slice(b"\r\n");
|
||||||
header.extend_from_slice(k.as_str().as_bytes());
|
header.extend_from_slice(k.as_str().as_bytes());
|
||||||
header.extend_from_slice(b": ");
|
header.extend_from_slice(b": ");
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
extern crate futures;
|
extern crate futures;
|
||||||
extern crate libflate;
|
extern crate libflate;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
|
extern crate hyper;
|
||||||
extern crate tokio;
|
extern crate tokio;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
use reqwest::async::Client;
|
use reqwest::async::Client;
|
||||||
|
use reqwest::async::multipart::{Form, Part};
|
||||||
use futures::{Future, Stream};
|
use futures::{Future, Stream};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -21,6 +23,78 @@ fn async_test_gzip_single_byte_chunks() {
|
|||||||
test_gzip(10, 1);
|
test_gzip(10, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn async_test_multipart() {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
|
let stream = futures::stream::once::<_, hyper::Error>(Ok(hyper::Chunk::from("part1 part2".to_owned())));
|
||||||
|
let part = Part::stream(stream);
|
||||||
|
|
||||||
|
let form = Form::new()
|
||||||
|
.text("foo", "bar")
|
||||||
|
.part("part_stream", part);
|
||||||
|
|
||||||
|
let expected_body = format!("\
|
||||||
|
24\r\n\
|
||||||
|
--{0}\r\n\r\n\
|
||||||
|
2E\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"foo\"\r\n\r\n\r\n\
|
||||||
|
3\r\n\
|
||||||
|
bar\r\n\
|
||||||
|
2\r\n\
|
||||||
|
\r\n\r\n\
|
||||||
|
24\r\n\
|
||||||
|
--{0}\r\n\r\n\
|
||||||
|
36\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"part_stream\"\r\n\r\n\r\n\
|
||||||
|
B\r\n\
|
||||||
|
part1 part2\r\n\
|
||||||
|
2\r\n\
|
||||||
|
\r\n\r\n\
|
||||||
|
26\r\n\
|
||||||
|
--{0}--\r\n\r\n\
|
||||||
|
0\r\n\r\n\
|
||||||
|
", form.boundary());
|
||||||
|
|
||||||
|
let server = server! {
|
||||||
|
request: format!("\
|
||||||
|
POST /multipart/1 HTTP/1.1\r\n\
|
||||||
|
user-agent: $USERAGENT\r\n\
|
||||||
|
accept: */*\r\n\
|
||||||
|
content-type: multipart/form-data; boundary={}\r\n\
|
||||||
|
accept-encoding: gzip\r\n\
|
||||||
|
host: $HOST\r\n\
|
||||||
|
transfer-encoding: chunked\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 mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let res_future = client.post(&url)
|
||||||
|
.multipart(form)
|
||||||
|
.send()
|
||||||
|
.and_then(|res| {
|
||||||
|
assert_eq!(res.url().as_str(), &url);
|
||||||
|
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
rt.block_on(res_future).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn test_gzip(response_size: usize, chunk_size: usize) {
|
fn test_gzip(response_size: usize, chunk_size: usize) {
|
||||||
let content: String = (0..response_size).into_iter().map(|i| format!("test {}", i)).collect();
|
let content: String = (0..response_size).into_iter().map(|i| format!("test {}", i)).collect();
|
||||||
let mut encoder = ::libflate::gzip::Encoder::new(Vec::new()).unwrap();
|
let mut encoder = ::libflate::gzip::Encoder::new(Vec::new()).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user