//! Multipart requests which write out their data in one fell swoop. | |
use mime::Mime; | |
use std::borrow::Cow; | |
use std::error::Error; | |
use std::fs::File; | |
use std::path::{Path, PathBuf}; | |
use std::io::prelude::*; | |
use std::io::Cursor; | |
use std::{fmt, io}; | |
use super::{HttpRequest, HttpStream}; | |
macro_rules! try_lazy ( | |
($field:expr, $try:expr) => ( | |
match $try { | |
Ok(ok) => ok, | |
Err(e) => return Err(LazyError::with_field($field.into(), e)), | |
} | |
); | |
($try:expr) => ( | |
match $try { | |
Ok(ok) => ok, | |
Err(e) => return Err(LazyError::without_field(e)), | |
} | |
) | |
); | |
/// A `LazyError` wrapping `std::io::Error`. | |
pub type LazyIoError<'a> = LazyError<'a, io::Error>; | |
/// `Result` type for `LazyIoError`. | |
pub type LazyIoResult<'a, T> = Result<T, LazyIoError<'a>>; | |
/// An error for lazily written multipart requests, including the original error as well | |
/// as the field which caused the error, if applicable. | |
pub struct LazyError<'a, E> { | |
/// The field that caused the error. | |
/// If `None`, there was a problem opening the stream to write or finalizing the stream. | |
pub field_name: Option<Cow<'a, str>>, | |
/// The inner error. | |
pub error: E, | |
/// Private field for back-compat. | |
_priv: (), | |
} | |
impl<'a, E> LazyError<'a, E> { | |
fn without_field<E_: Into<E>>(error: E_) -> Self { | |
LazyError { | |
field_name: None, | |
error: error.into(), | |
_priv: (), | |
} | |
} | |
fn with_field<E_: Into<E>>(field_name: Cow<'a, str>, error: E_) -> Self { | |
LazyError { | |
field_name: Some(field_name), | |
error: error.into(), | |
_priv: (), | |
} | |
} | |
fn transform_err<E_: From<E>>(self) -> LazyError<'a, E_> { | |
LazyError { | |
field_name: self.field_name, | |
error: self.error.into(), | |
_priv: (), | |
} | |
} | |
} | |
/// Take `self.error`, discarding `self.field_name`. | |
impl<'a> Into<io::Error> for LazyError<'a, io::Error> { | |
fn into(self) -> io::Error { | |
self.error | |
} | |
} | |
impl<'a, E: Error> Error for LazyError<'a, E> { | |
fn description(&self) -> &str { | |
self.error.description() | |
} | |
fn cause(&self) -> Option<&Error> { | |
Some(&self.error) | |
} | |
} | |
impl<'a, E: fmt::Debug> fmt::Debug for LazyError<'a, E> { | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | |
if let Some(ref field_name) = self.field_name { | |
fmt.write_fmt(format_args!("LazyError (on field {:?}): {:?}", field_name, self.error)) | |
} else { | |
fmt.write_fmt(format_args!("LazyError (misc): {:?}", self.error)) | |
} | |
} | |
} | |
impl<'a, E: fmt::Display> fmt::Display for LazyError<'a, E> { | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | |
if let Some(ref field_name) = self.field_name { | |
fmt.write_fmt(format_args!("Error writing field {:?}: {}", field_name, self.error)) | |
} else { | |
fmt.write_fmt(format_args!("Error opening or flushing stream: {}", self.error)) | |
} | |
} | |
} | |
/// A multipart request which writes all fields at once upon being provided an output stream. | |
/// | |
/// Sacrifices static dispatch for support for dynamic construction. Reusable. | |
/// | |
/// #### Lifetimes | |
/// * `'n`: Lifetime for field **n**ames; will only escape this struct in `LazyIoError<'n>`. | |
/// * `'d`: Lifetime for **d**ata: will only escape this struct in `PreparedFields<'d>`. | |
#[derive(Debug, Default)] | |
pub struct Multipart<'n, 'd> { | |
fields: Vec<Field<'n, 'd>>, | |
} | |
impl<'n, 'd> Multipart<'n, 'd> { | |
/// Initialize a new lazy dynamic request. | |
pub fn new() -> Self { | |
Default::default() | |
} | |
/// Add a text field to this request. | |
pub fn add_text<N, T>(&mut self, name: N, text: T) -> &mut Self where N: Into<Cow<'n, str>>, T: Into<Cow<'d, str>> { | |
self.fields.push( | |
Field { | |
name: name.into(), | |
data: Data::Text(text.into()) | |
} | |
); | |
self | |
} | |
/// Add a file field to this request. | |
/// | |
/// ### Note | |
/// Does not check if `path` exists. | |
pub fn add_file<N, P>(&mut self, name: N, path: P) -> &mut Self where N: Into<Cow<'n, str>>, P: IntoCowPath<'d> { | |
self.fields.push( | |
Field { | |
name: name.into(), | |
data: Data::File(path.into_cow_path()), | |
} | |
); | |
self | |
} | |
/// Add a generic stream field to this request, | |
pub fn add_stream<N, R, F>(&mut self, name: N, stream: R, filename: Option<F>, mime: Option<Mime>) -> &mut Self where N: Into<Cow<'n, str>>, R: Read + 'd, F: Into<Cow<'n, str>> { | |
self.fields.push( | |
Field { | |
name: name.into(), | |
data: Data::Stream(Stream { | |
content_type: mime.unwrap_or_else(::mime_guess::octet_stream), | |
filename: filename.map(|f| f.into()), | |
stream: Box::new(stream) | |
}), | |
} | |
); | |
self | |
} | |
/// Convert `req` to `HttpStream`, write out the fields in this request, and finish the | |
/// request, returning the response if successful, or the first error encountered. | |
/// | |
/// If any files were added by path they will now be opened for reading. | |
pub fn send<R: HttpRequest>(&mut self, mut req: R) -> Result<< R::Stream as HttpStream >::Response, LazyError<'n, < R::Stream as HttpStream >::Error>> { | |
let mut prepared = self.prepare().map_err(LazyError::transform_err)?; | |
req.apply_headers(prepared.boundary(), prepared.content_len()); | |
let mut stream = try_lazy!(req.open_stream()); | |
try_lazy!(io::copy(&mut prepared, &mut stream)); | |
stream.finish().map_err(LazyError::without_field) | |
} | |
/// Export the multipart data contained in this lazy request as an adaptor which implements `Read`. | |
/// | |
/// During this step, if any files were added by path then they will be opened for reading | |
/// and their length measured. | |
pub fn prepare(&mut self) -> LazyIoResult<'n, PreparedFields<'d>> { | |
PreparedFields::from_fields(&mut self.fields) | |
} | |
} | |
#[derive(Debug)] | |
struct Field<'n, 'd> { | |
name: Cow<'n, str>, | |
data: Data<'n, 'd>, | |
} | |
enum Data<'n, 'd> { | |
Text(Cow<'d, str>), | |
File(Cow<'d, Path>), | |
Stream(Stream<'n, 'd>), | |
} | |
impl<'n, 'd> fmt::Debug for Data<'n, 'd> { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match *self { | |
Data::Text(ref text) => write!(f, "Data::Text({:?})", text), | |
Data::File(ref path) => write!(f, "Data::File({:?})", path), | |
Data::Stream(_) => f.write_str("Data::Stream(Box<Read>)"), | |
} | |
} | |
} | |
struct Stream<'n, 'd> { | |
filename: Option<Cow<'n, str>>, | |
content_type: Mime, | |
stream: Box<Read + 'd>, | |
} | |
/// The result of [`Multipart::prepare()`](struct.Multipart.html#method.prepare). | |
/// | |
/// Implements `Read`, contains the entire request body. | |
/// | |
/// Individual files/streams are dropped as they are read to completion. | |
/// | |
/// ### Note | |
/// The fields in the request may have been reordered to simplify the preparation step. | |
/// No compliant server implementation will be relying on the specific ordering of fields anyways. | |
pub struct PreparedFields<'d> { | |
text_data: Cursor<Vec<u8>>, | |
streams: Vec<PreparedField<'d>>, | |
end_boundary: Cursor<String>, | |
content_len: Option<u64>, | |
} | |
impl<'d> PreparedFields<'d> { | |
fn from_fields<'n>(fields: &mut Vec<Field<'n, 'd>>) -> Result<Self, LazyIoError<'n>> { | |
debug!("Field count: {}", fields.len()); | |
// One of the two RFCs specifies that any bytes before the first boundary are to be | |
// ignored anyway | |
let mut boundary = format!("\r\n--{}", super::gen_boundary()); | |
let mut text_data = Vec::new(); | |
let mut streams = Vec::new(); | |
let mut content_len = 0u64; | |
let mut use_len = true; | |
for field in fields.drain(..) { | |
match field.data { | |
Data::Text(text) => write!(text_data, "{}\r\nContent-Disposition: form-data; \ | |
name=\"{}\"\r\n\r\n{}", | |
boundary, field.name, text).unwrap(), | |
Data::File(file) => { | |
let (stream, len) = PreparedField::from_path(field.name, &file, &boundary)?; | |
content_len += len; | |
streams.push(stream); | |
}, | |
Data::Stream(stream) => { | |
use_len = false; | |
streams.push( | |
PreparedField::from_stream(&field.name, &boundary, &stream.content_type, | |
stream.filename.as_ref().map(|f| &**f), | |
stream.stream)); | |
}, | |
} | |
} | |
// So we don't write a spurious end boundary | |
if text_data.is_empty() && streams.is_empty() { | |
boundary = String::new(); | |
} else { | |
boundary.push_str("--"); | |
} | |
content_len += boundary.len() as u64; | |
Ok(PreparedFields { | |
text_data: Cursor::new(text_data), | |
streams, | |
end_boundary: Cursor::new(boundary), | |
content_len: if use_len { Some(content_len) } else { None } , | |
}) | |
} | |
/// Get the content-length value for this set of fields, if applicable (all fields are sized, | |
/// i.e. not generic streams). | |
pub fn content_len(&self) -> Option<u64> { | |
self.content_len | |
} | |
/// Get the boundary that was used to serialize the request. | |
pub fn boundary(&self) -> &str { | |
let boundary = self.end_boundary.get_ref(); | |
// Get just the bare boundary string | |
&boundary[4 .. boundary.len() - 2] | |
} | |
} | |
impl<'d> Read for PreparedFields<'d> { | |
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { | |
if buf.is_empty() { | |
debug!("PreparedFields::read() was passed a zero-sized buffer."); | |
return Ok(0); | |
} | |
let mut total_read = 0; | |
while total_read < buf.len() && !cursor_at_end(&self.end_boundary){ | |
let buf = &mut buf[total_read..]; | |
total_read += if !cursor_at_end(&self.text_data) { | |
self.text_data.read(buf)? | |
} else if let Some(mut field) = self.streams.pop() { | |
match field.read(buf) { | |
Ok(0) => continue, | |
res => { | |
self.streams.push(field); | |
res | |
}, | |
}? | |
} else { | |
self.end_boundary.read(buf)? | |
}; | |
} | |
Ok(total_read) | |
} | |
} | |
struct PreparedField<'d> { | |
header: Cursor<Vec<u8>>, | |
stream: Box<Read + 'd>, | |
} | |
impl<'d> PreparedField<'d> { | |
fn from_path<'n>(name: Cow<'n, str>, path: &Path, boundary: &str) -> Result<(Self, u64), LazyIoError<'n>> { | |
let (content_type, filename) = super::mime_filename(&path); | |
let file = try_lazy!(name, File::open(path)); | |
let content_len = try_lazy!(name, file.metadata()).len(); | |
let stream = Self::from_stream(&name, boundary, &content_type, filename, Box::new(file)); | |
let content_len = content_len + (stream.header.get_ref().len() as u64); | |
Ok((stream, content_len)) | |
} | |
fn from_stream(name: &str, boundary: &str, content_type: &Mime, filename: Option<&str>, stream: Box<Read + 'd>) -> Self { | |
let mut header = Vec::new(); | |
write!(header, "{}\r\nContent-Disposition: form-data; name=\"{}\"", | |
boundary, name).unwrap(); | |
if let Some(filename) = filename { | |
write!(header, "; filename=\"{}\"", filename).unwrap(); | |
} | |
write!(header, "\r\nContent-Type: {}\r\n\r\n", content_type).unwrap(); | |
PreparedField { | |
header: Cursor::new(header), | |
stream, | |
} | |
} | |
} | |
impl<'d> Read for PreparedField<'d> { | |
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { | |
debug!("PreparedField::read()"); | |
if !cursor_at_end(&self.header) { | |
self.header.read(buf) | |
} else { | |
self.stream.read(buf) | |
} | |
} | |
} | |
impl<'d> fmt::Debug for PreparedField<'d> { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
f.debug_struct("PreparedField") | |
.field("header", &self.header) | |
.field("stream", &"Box<Read>") | |
.finish() | |
} | |
} | |
/// Conversion trait necessary for `Multipart::add_file()` to accept borrowed or owned strings | |
/// and borrowed or owned paths | |
pub trait IntoCowPath<'a> { | |
/// Self-explanatory, hopefully | |
fn into_cow_path(self) -> Cow<'a, Path>; | |
} | |
impl<'a> IntoCowPath<'a> for Cow<'a, Path> { | |
fn into_cow_path(self) -> Cow<'a, Path> { | |
self | |
} | |
} | |
impl IntoCowPath<'static> for PathBuf { | |
fn into_cow_path(self) -> Cow<'static, Path> { | |
self.into() | |
} | |
} | |
impl<'a> IntoCowPath<'a> for &'a Path { | |
fn into_cow_path(self) -> Cow<'a, Path> { | |
self.into() | |
} | |
} | |
impl IntoCowPath<'static> for String { | |
fn into_cow_path(self) -> Cow<'static, Path> { | |
PathBuf::from(self).into() | |
} | |
} | |
impl<'a> IntoCowPath<'a> for &'a str { | |
fn into_cow_path(self) -> Cow<'a, Path> { | |
Path::new(self).into() | |
} | |
} | |
fn cursor_at_end<T: AsRef<[u8]>>(cursor: &Cursor<T>) -> bool { | |
cursor.position() == (cursor.get_ref().as_ref().len() as u64) | |
} | |
#[cfg(feature = "hyper")] | |
mod hyper { | |
use hyper::client::{Body, Client, IntoUrl, RequestBuilder, Response}; | |
use hyper::Result as HyperResult; | |
impl<'n, 'd> super::Multipart<'n, 'd> { | |
/// #### Feature: `hyper` | |
/// Complete a POST request with the given `hyper::client::Client` and URL. | |
/// | |
/// Supplies the fields in the body, optionally setting the content-length header if | |
/// applicable (all added fields were text or files, i.e. no streams). | |
pub fn client_request<U: IntoUrl>(&mut self, client: &Client, url: U) -> HyperResult<Response> { | |
self.client_request_mut(client, url, |r| r) | |
} | |
/// #### Feature: `hyper` | |
/// Complete a POST request with the given `hyper::client::Client` and URL; | |
/// allows mutating the `hyper::client::RequestBuilder` via the passed closure. | |
/// | |
/// Note that the body, and the `ContentType` and `ContentLength` headers will be | |
/// overwritten, either by this method or by Hyper. | |
pub fn client_request_mut<U: IntoUrl, F: FnOnce(RequestBuilder) -> RequestBuilder>(&mut self, client: &Client, url: U, | |
mut_fn: F) -> HyperResult<Response> { | |
let mut fields = match self.prepare() { | |
Ok(fields) => fields, | |
Err(err) => { | |
error!("Error preparing request: {}", err); | |
return Err(err.error.into()); | |
}, | |
}; | |
mut_fn(client.post(url)) | |
.header(::client::hyper::content_type(fields.boundary())) | |
.body(fields.to_body()) | |
.send() | |
} | |
} | |
impl<'d> super::PreparedFields<'d> { | |
/// #### Feature: `hyper` | |
/// Convert `self` to `hyper::client::Body`. | |
#[cfg_attr(feature="clippy", warn(wrong_self_convention))] | |
pub fn to_body<'b>(&'b mut self) -> Body<'b> where 'd: 'b { | |
if let Some(content_len) = self.content_len { | |
Body::SizedBody(self, content_len) | |
} else { | |
Body::ChunkedBody(self) | |
} | |
} | |
} | |
} |