// Copyright 2016 `multipart` Crate Developers | |
// | |
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or | |
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or | |
// http://opensource.org/licenses/MIT>, at your option. This file may not be | |
// copied, modified, or distributed except according to those terms. | |
//! The client-side abstraction for multipart requests. Enabled with the `client` feature. | |
//! | |
//! Use this when sending POST requests with files to a server. | |
use mime::Mime; | |
use std::borrow::Cow; | |
use std::fs::File; | |
use std::io; | |
use std::io::prelude::*; | |
use std::path::Path; | |
#[cfg(feature = "hyper")] | |
pub mod hyper; | |
pub mod lazy; | |
mod sized; | |
pub use self::sized::SizedRequest; | |
const BOUNDARY_LEN: usize = 16; | |
macro_rules! map_self { | |
($selff:expr, $try:expr) => ( | |
match $try { | |
Ok(_) => Ok($selff), | |
Err(err) => Err(err.into()), | |
} | |
) | |
} | |
/// The entry point of the client-side multipart API. | |
/// | |
/// Though they perform I/O, the `.write_*()` methods do not return `io::Result<_>` in order to | |
/// facilitate method chaining. Upon the first error, all subsequent API calls will be no-ops until | |
/// `.send()` is called, at which point the error will be reported. | |
pub struct Multipart<S> { | |
writer: MultipartWriter<'static, S>, | |
} | |
impl Multipart<()> { | |
/// Create a new `Multipart` to wrap a request. | |
/// | |
/// ## Returns Error | |
/// If `req.open_stream()` returns an error. | |
pub fn from_request<R: HttpRequest>(req: R) -> Result<Multipart<R::Stream>, R::Error> { | |
let (boundary, stream) = open_stream(req, None)?; | |
Ok(Multipart { | |
writer: MultipartWriter::new(stream, boundary), | |
}) | |
} | |
} | |
impl<S: HttpStream> Multipart<S> { | |
/// Write a text field to this multipart request. | |
/// `name` and `val` can be either owned `String` or `&str`. | |
/// | |
/// ## Errors | |
/// If something went wrong with the HTTP stream. | |
pub fn write_text<N: AsRef<str>, V: AsRef<str>>(&mut self, name: N, val: V) -> Result<&mut Self, S::Error> { | |
map_self!(self, self.writer.write_text(name.as_ref(), val.as_ref())) | |
} | |
/// Open a file pointed to by `path` and write its contents to the multipart request, | |
/// supplying its filename and guessing its `Content-Type` from its extension. | |
/// | |
/// If you want to set these values manually, or use another type that implements `Read`, | |
/// use `.write_stream()`. | |
/// | |
/// `name` can be either `String` or `&str`, and `path` can be `PathBuf` or `&Path`. | |
/// | |
/// ## Errors | |
/// If there was a problem opening the file (was a directory or didn't exist), | |
/// or if something went wrong with the HTTP stream. | |
pub fn write_file<N: AsRef<str>, P: AsRef<Path>>(&mut self, name: N, path: P) -> Result<&mut Self, S::Error> { | |
let name = name.as_ref(); | |
let path = path.as_ref(); | |
map_self!(self, self.writer.write_file(name, path)) | |
} | |
/// Write a byte stream to the multipart request as a file field, supplying `filename` if given, | |
/// and `content_type` if given or `"application/octet-stream"` if not. | |
/// | |
/// `name` can be either `String` or `&str`, and `read` can take the `Read` by-value or | |
/// with an `&mut` borrow. | |
/// | |
/// ## Warning | |
/// The given `Read` **must** be able to read to EOF (end of file/no more data), meaning | |
/// `Read::read()` returns `Ok(0)`. If it never returns EOF it will be read to infinity | |
/// and the request will never be completed. | |
/// | |
/// When using `SizedRequest` this also can cause out-of-control memory usage as the | |
/// multipart data has to be written to an in-memory buffer so its size can be calculated. | |
/// | |
/// Use `Read::take()` if you wish to send data from a `Read` | |
/// that will never return EOF otherwise. | |
/// | |
/// ## Errors | |
/// If the reader returned an error, or if something went wrong with the HTTP stream. | |
// RFC: How to format this declaration? | |
pub fn write_stream<N: AsRef<str>, St: Read>( | |
&mut self, name: N, stream: &mut St, filename: Option<&str>, content_type: Option<Mime> | |
) -> Result<&mut Self, S::Error> { | |
let name = name.as_ref(); | |
map_self!(self, self.writer.write_stream(stream, name, filename, content_type)) | |
} | |
/// Finalize the request and return the response from the server, or the last error if set. | |
pub fn send(self) -> Result<S::Response, S::Error> { | |
self.writer.finish().map_err(io::Error::into).and_then(|body| body.finish()) | |
} | |
} | |
impl<R: HttpRequest> Multipart<SizedRequest<R>> | |
where <R::Stream as HttpStream>::Error: From<R::Error> { | |
/// Create a new `Multipart` using the `SizedRequest` wrapper around `req`. | |
pub fn from_request_sized(req: R) -> Result<Self, R::Error> { | |
Multipart::from_request(SizedRequest::from_request(req)) | |
} | |
} | |
/// A trait describing an HTTP request that can be used to send multipart data. | |
pub trait HttpRequest { | |
/// The HTTP stream type that can be opend by this request, to which the multipart data will be | |
/// written. | |
type Stream: HttpStream; | |
/// The error type for this request. | |
/// Must be compatible with `io::Error` as well as `Self::HttpStream::Error` | |
type Error: From<io::Error> + Into<<Self::Stream as HttpStream>::Error>; | |
/// Set the `Content-Type` header to `multipart/form-data` and supply the `boundary` value. | |
/// If `content_len` is given, set the `Content-Length` header to its value. | |
/// | |
/// Return `true` if any and all sanity checks passed and the stream is ready to be opened, | |
/// or `false` otherwise. | |
fn apply_headers(&mut self, boundary: &str, content_len: Option<u64>) -> bool; | |
/// Open the request stream and return it or any error otherwise. | |
fn open_stream(self) -> Result<Self::Stream, Self::Error>; | |
} | |
/// A trait describing an open HTTP stream that can be written to. | |
pub trait HttpStream: Write { | |
/// The request type that opened this stream. | |
type Request: HttpRequest; | |
/// The response type that will be returned after the request is completed. | |
type Response; | |
/// The error type for this stream. | |
/// Must be compatible with `io::Error` as well as `Self::Request::Error`. | |
type Error: From<io::Error> + From<<Self::Request as HttpRequest>::Error>; | |
/// Finalize and close the stream and return the response object, or any error otherwise. | |
fn finish(self) -> Result<Self::Response, Self::Error>; | |
} | |
impl HttpRequest for () { | |
type Stream = io::Sink; | |
type Error = io::Error; | |
fn apply_headers(&mut self, _: &str, _: Option<u64>) -> bool { true } | |
fn open_stream(self) -> Result<Self::Stream, Self::Error> { Ok(io::sink()) } | |
} | |
impl HttpStream for io::Sink { | |
type Request = (); | |
type Response = (); | |
type Error = io::Error; | |
fn finish(self) -> Result<Self::Response, Self::Error> { Ok(()) } | |
} | |
fn gen_boundary() -> String { | |
::random_alphanumeric(BOUNDARY_LEN) | |
} | |
fn open_stream<R: HttpRequest>(mut req: R, content_len: Option<u64>) -> Result<(String, R::Stream), R::Error> { | |
let boundary = gen_boundary(); | |
req.apply_headers(&boundary, content_len); | |
req.open_stream().map(|stream| (boundary, stream)) | |
} | |
struct MultipartWriter<'a, W> { | |
inner: W, | |
boundary: Cow<'a, str>, | |
data_written: bool, | |
} | |
impl<'a, W: Write> MultipartWriter<'a, W> { | |
fn new<B: Into<Cow<'a, str>>>(inner: W, boundary: B) -> Self { | |
MultipartWriter { | |
inner, | |
boundary: boundary.into(), | |
data_written: false, | |
} | |
} | |
fn write_boundary(&mut self) -> io::Result<()> { | |
if self.data_written { | |
self.inner.write_all(b"\r\n")?; | |
} | |
write!(self.inner, "--{}\r\n", self.boundary) | |
} | |
fn write_text(&mut self, name: &str, text: &str) -> io::Result<()> { | |
chain_result! { | |
self.write_field_headers(name, None, None), | |
self.inner.write_all(text.as_bytes()) | |
} | |
} | |
fn write_file(&mut self, name: &str, path: &Path) -> io::Result<()> { | |
let (content_type, filename) = mime_filename(path); | |
let mut file = File::open(path)?; | |
self.write_stream(&mut file, name, filename, Some(content_type)) | |
} | |
fn write_stream<S: Read>(&mut self, stream: &mut S, name: &str, filename: Option<&str>, content_type: Option<Mime>) -> io::Result<()> { | |
// This is necessary to make sure it is interpreted as a file on the server end. | |
let content_type = Some(content_type.unwrap_or_else(::mime_guess::octet_stream)); | |
chain_result! { | |
self.write_field_headers(name, filename, content_type), | |
io::copy(stream, &mut self.inner), | |
Ok(()) | |
} | |
} | |
fn write_field_headers(&mut self, name: &str, filename: Option<&str>, content_type: Option<Mime>) | |
-> io::Result<()> { | |
chain_result! { | |
// Write the first boundary, or the boundary for the previous field. | |
self.write_boundary(), | |
{ self.data_written = true; Ok(()) }, | |
write!(self.inner, "Content-Disposition: form-data; name=\"{}\"", name), | |
filename.map(|filename| write!(self.inner, "; filename=\"{}\"", filename)) | |
.unwrap_or(Ok(())), | |
content_type.map(|content_type| write!(self.inner, "\r\nContent-Type: {}", content_type)) | |
.unwrap_or(Ok(())), | |
self.inner.write_all(b"\r\n\r\n") | |
} | |
} | |
fn finish(mut self) -> io::Result<W> { | |
if self.data_written { | |
self.inner.write_all(b"\r\n")?; | |
} | |
// always write the closing boundary, even for empty bodies | |
write!(self.inner, "--{}--", self.boundary)?; | |
Ok(self.inner) | |
} | |
} | |
fn mime_filename(path: &Path) -> (Mime, Option<&str>) { | |
let content_type = ::mime_guess::guess_mime_type(path); | |
let filename = opt_filename(path); | |
(content_type, filename) | |
} | |
fn opt_filename(path: &Path) -> Option<&str> { | |
path.file_name().and_then(|filename| filename.to_str()) | |
} |