473 lines
14 KiB
Rust
473 lines
14 KiB
Rust
//! multipart/form-data
|
|
//!
|
|
//! To send a `multipart/form-data` body, a [`Form`](crate::multipart::Form) is built up, adding
|
|
//! fields or customized [`Part`](crate::multipart::Part)s, and then calling the
|
|
//! [`multipart`][builder] method on the `RequestBuilder`.
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```
|
|
//! use reqwest::multipart;
|
|
//!
|
|
//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! let form = multipart::Form::new()
|
|
//! // Adding just a simple text field...
|
|
//! .text("username", "seanmonstar")
|
|
//! // And a file...
|
|
//! .file("photo", "/path/to/photo.png")?;
|
|
//!
|
|
//! // Customize all the details of a Part if needed...
|
|
//! let bio = multipart::Part::text("hallo peeps")
|
|
//! .file_name("bio.txt")
|
|
//! .mime_str("text/plain")?;
|
|
//!
|
|
//! // Add the custom part to our form...
|
|
//! let form = form.part("biography", bio);
|
|
//!
|
|
//! // And finally, send the form
|
|
//! let client = reqwest::Client::new();
|
|
//! let resp = client
|
|
//! .post("http://localhost:8080/user")
|
|
//! .multipart(form)
|
|
//! .send()?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! # fn main() {}
|
|
//! ```
|
|
//!
|
|
//! [builder]: ../struct.RequestBuilder.html#method.multipart
|
|
use std::borrow::Cow;
|
|
use std::fmt;
|
|
use std::fs::File;
|
|
use std::io::{self, Cursor, Read};
|
|
use std::path::Path;
|
|
|
|
use mime_guess::{self, Mime};
|
|
|
|
use crate::async_impl::multipart::{FormParts, PartMetadata, PartProps};
|
|
use crate::Body;
|
|
|
|
/// A multipart/form-data request.
|
|
pub struct Form {
|
|
inner: FormParts<Part>,
|
|
}
|
|
|
|
/// A field in a multipart form.
|
|
pub struct Part {
|
|
meta: PartMetadata,
|
|
value: Body,
|
|
}
|
|
|
|
impl Form {
|
|
/// Creates a new Form without any content.
|
|
pub fn new() -> Form {
|
|
Form {
|
|
inner: FormParts::new(),
|
|
}
|
|
}
|
|
|
|
/// Get the boundary that this form will use.
|
|
#[inline]
|
|
pub fn boundary(&self) -> &str {
|
|
self.inner.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>(self, name: T, part: Part) -> Form
|
|
where
|
|
T: Into<Cow<'static, str>>,
|
|
{
|
|
self.with_inner(move |inner| inner.part(name, part))
|
|
}
|
|
|
|
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
|
pub fn percent_encode_path_segment(self) -> Form {
|
|
self.with_inner(|inner| inner.percent_encode_path_segment())
|
|
}
|
|
|
|
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
|
pub fn percent_encode_attr_chars(self) -> Form {
|
|
self.with_inner(|inner| inner.percent_encode_attr_chars())
|
|
}
|
|
|
|
/// Configure this `Form` to skip percent-encoding
|
|
pub fn percent_encode_noop(self) -> Form {
|
|
self.with_inner(|inner| inner.percent_encode_noop())
|
|
}
|
|
|
|
pub(crate) fn reader(self) -> Reader {
|
|
Reader::new(self)
|
|
}
|
|
|
|
// 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(crate) fn compute_length(&mut self) -> Option<u64> {
|
|
self.inner.compute_length()
|
|
}
|
|
|
|
fn with_inner<F>(self, func: F) -> Self
|
|
where
|
|
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
|
|
{
|
|
Form {
|
|
inner: func(self.inner),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Form {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
self.inner.fmt_fields("Form", f)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// 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()
|
|
.map(|filename| filename.to_string_lossy().into_owned());
|
|
|
|
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
|
let mime = mime_guess::from_ext(ext).first_or_octet_stream();
|
|
let file = File::open(path)?;
|
|
let field = Part::new(Body::from(file)).mime(mime);
|
|
|
|
Ok(if let Some(file_name) = file_name {
|
|
field.file_name(file_name)
|
|
} else {
|
|
field
|
|
})
|
|
}
|
|
|
|
fn new(value: Body) -> Part {
|
|
Part {
|
|
meta: PartMetadata::new(),
|
|
value,
|
|
}
|
|
}
|
|
|
|
/// Tries to set the mime of this part.
|
|
pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
|
|
Ok(self.mime(try_!(mime.parse())))
|
|
}
|
|
|
|
// Re-export when mime 0.4 is available, with split MediaType/MediaRange.
|
|
fn mime(self, mime: Mime) -> Part {
|
|
self.with_inner(move |inner| inner.mime(mime))
|
|
}
|
|
|
|
/// Sets the filename, builder style.
|
|
pub fn file_name<T>(self, filename: T) -> Part
|
|
where
|
|
T: Into<Cow<'static, str>>,
|
|
{
|
|
self.with_inner(move |inner| inner.file_name(filename))
|
|
}
|
|
|
|
fn with_inner<F>(self, func: F) -> Self
|
|
where
|
|
F: FnOnce(PartMetadata) -> PartMetadata,
|
|
{
|
|
Part {
|
|
meta: func(self.meta),
|
|
value: self.value,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Part {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut dbg = f.debug_struct("Part");
|
|
dbg.field("value", &self.value);
|
|
self.meta.fmt_fields(&mut dbg);
|
|
dbg.finish()
|
|
}
|
|
}
|
|
|
|
impl PartProps for Part {
|
|
fn value_len(&self) -> Option<u64> {
|
|
self.value.len()
|
|
}
|
|
|
|
fn metadata(&self) -> &PartMetadata {
|
|
&self.meta
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Reader {
|
|
form: Form,
|
|
active_reader: Option<Box<dyn 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,
|
|
active_reader: None,
|
|
};
|
|
reader.next_reader();
|
|
reader
|
|
}
|
|
|
|
fn next_reader(&mut self) {
|
|
self.active_reader = if !self.form.inner.fields.is_empty() {
|
|
// We need to move out of the vector here because we are consuming the field's reader
|
|
let (name, field) = self.form.inner.fields.remove(0);
|
|
let boundary = Cursor::new(format!("--{}\r\n", self.form.boundary()));
|
|
let header = Cursor::new({
|
|
// Try to use cached headers created by compute_length
|
|
let mut h = if !self.form.inner.computed_headers.is_empty() {
|
|
self.form.inner.computed_headers.remove(0)
|
|
} else {
|
|
self.form
|
|
.inner
|
|
.percent_encoding
|
|
.encode_headers(&name, field.metadata())
|
|
};
|
|
h.extend_from_slice(b"\r\n\r\n");
|
|
h
|
|
});
|
|
let reader = boundary
|
|
.chain(header)
|
|
.chain(field.value.into_reader())
|
|
.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.inner.fields.is_empty() {
|
|
Some(Box::new(reader))
|
|
} else {
|
|
Some(Box::new(reader.chain(Cursor::new(format!(
|
|
"--{}--\r\n",
|
|
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.is_empty() {
|
|
self.next_reader();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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.inner.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--\r\n";
|
|
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.inner.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--\r\n";
|
|
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 read_to_end_with_header() {
|
|
let mut output = Vec::new();
|
|
let mut part = Part::text("value2").mime(mime::IMAGE_BMP);
|
|
part.meta.headers.insert("Hdr3", "/a/b/c".parse().unwrap());
|
|
let mut form = Form::new().part("key2", part);
|
|
form.inner.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";
|
|
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);
|
|
}
|
|
}
|