@@ -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 = []
|
||||||
|
|||||||
83
src/body.rs
83
src/body.rs
@@ -1,6 +1,6 @@
|
|||||||
use std::io::{self, Read};
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::io::{self, Cursor, Read};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use hyper::{self, Chunk};
|
use hyper::{self, Chunk};
|
||||||
@@ -16,7 +16,7 @@ use {async_impl, wait};
|
|||||||
/// [builder]: ./struct.RequestBuilder.html#method.body
|
/// [builder]: ./struct.RequestBuilder.html#method.body
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Body {
|
pub struct Body {
|
||||||
reader: Kind,
|
kind: Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Body {
|
impl Body {
|
||||||
@@ -54,7 +54,7 @@ impl Body {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn new<R: Read + Send + 'static>(reader: R) -> Body {
|
pub fn new<R: Read + Send + 'static>(reader: R) -> Body {
|
||||||
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 {
|
pub fn sized<R: Read + Send + 'static>(reader: R, len: u64) -> Body {
|
||||||
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 {
|
enum Kind {
|
||||||
Reader(Box<Read + Send>, Option<u64>),
|
Reader(Box<Read + Send>, Option<u64>),
|
||||||
@@ -99,7 +89,7 @@ impl From<Vec<u8>> for Body {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn from(v: Vec<u8>) -> Body {
|
fn from(v: Vec<u8>) -> Body {
|
||||||
Body {
|
Body {
|
||||||
reader: Kind::Bytes(v.into()),
|
kind: Kind::Bytes(v.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +106,7 @@ impl From<&'static [u8]> for Body {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn from(s: &'static [u8]) -> Body {
|
fn from(s: &'static [u8]) -> Body {
|
||||||
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 {
|
fn from(f: File) -> Body {
|
||||||
let len = f.metadata().map(|m| m.len()).ok();
|
let len = f.metadata().map(|m| m.len()).ok();
|
||||||
Body {
|
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 {
|
impl fmt::Debug for Kind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Kind::Reader(_, ref v) => f.debug_tuple("Kind::Reader").field(&"_").field(v).finish(),
|
Kind::Reader(_, ref v) => f.debug_struct("Reader")
|
||||||
Kind::Bytes(ref v) => f.debug_tuple("Kind::Bytes").field(v).finish(),
|
.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(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 {
|
pub struct Sender {
|
||||||
body: (Box<Read + Send>, Option<u64>),
|
body: (Box<Read + Send>, Option<u64>),
|
||||||
tx: wait::WaitSink<::futures::sync::mpsc::Sender<hyper::Result<Chunk>>>,
|
tx: wait::WaitSink<::futures::sync::mpsc::Sender<hyper::Result<Chunk>>>,
|
||||||
@@ -193,7 +225,7 @@ impl Sender {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) {
|
pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) {
|
||||||
match body.reader {
|
match body.kind {
|
||||||
Kind::Reader(read, len) => {
|
Kind::Reader(read, len) => {
|
||||||
let (tx, rx) = hyper::Body::pair();
|
let (tx, rx) = hyper::Body::pair();
|
||||||
let tx = Sender {
|
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.
|
//! 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};
|
||||||
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)
|
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()`.
|
||||||
///
|
///
|
||||||
|
|||||||
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