Skip to content

Commit

Permalink
Add file function to async::multipart (seanmonstar#2106)
Browse files Browse the repository at this point in the history
* Add file function to async_impl::multipart

* Add test for asynchronous file function in multipart

* Fix doc of file function in blocking::multipart

* Fix test

Follow up on this pull request
seanmonstar#2059

* Fix doc test
  • Loading branch information
NaokiM03 authored and Nutomic committed Nov 7, 2024
1 parent 91ff3c3 commit bdc780b
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 1 deletion.
58 changes: 58 additions & 0 deletions src/async_impl/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ use std::borrow::Cow;
use std::fmt;
use std::pin::Pin;

#[cfg(feature = "stream")]
use std::io;
#[cfg(feature = "stream")]
use std::path::Path;

use bytes::Bytes;
use mime_guess::Mime;
use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
#[cfg(feature = "stream")]
use tokio::fs::File;

use futures_core::Stream;
use futures_util::{future, stream, StreamExt};
Expand Down Expand Up @@ -82,6 +89,33 @@ impl Form {
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
/// # async fn run() -> std::io::Result<()> {
/// let form = reqwest::multipart::Form::new()
/// .file("key", "/path/to/file").await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Errors when the file cannot be opened.
#[cfg(feature = "stream")]
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
pub async 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).await?))
}

/// Adds a customized Part.
pub fn part<T>(self, name: T, part: Part) -> Form
where
Expand Down Expand Up @@ -218,6 +252,30 @@ impl Part {
Part::new(value.into(), Some(length))
}

/// Makes a file parameter.
///
/// # Errors
///
/// Errors when the file cannot be opened.
#[cfg(feature = "stream")]
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
pub async 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).await?;
let field = Part::stream(file).mime(mime);

Ok(if let Some(file_name) = file_name {
field.file_name(file_name)
} else {
field
})
}

fn new(value: Body, body_length: Option<u64>) -> Part {
Part {
meta: PartMetadata::new(),
Expand Down
2 changes: 1 addition & 1 deletion src/blocking/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Form {
///
/// ```no_run
/// # fn run() -> std::io::Result<()> {
/// let files = reqwest::blocking::multipart::Form::new()
/// let form = reqwest::blocking::multipart::Form::new()
/// .file("key", "/path/to/file")?;
/// # Ok(())
/// # }
Expand Down
55 changes: 55 additions & 0 deletions tests/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,58 @@ fn blocking_file_part() {
assert_eq!(res.url().as_str(), &url);
assert_eq!(res.status(), reqwest::StatusCode::OK);
}

#[cfg(feature = "stream")]
#[tokio::test]
async fn async_impl_file_part() {
let _ = env_logger::try_init();

let form = reqwest::multipart::Form::new()
.file("foo", "Cargo.lock")
.await
.unwrap();

let fcontents = std::fs::read_to_string("Cargo.lock").unwrap();

let expected_body = format!(
"\
--{0}\r\n\
Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\
Content-Type: application/octet-stream\r\n\r\n\
{1}\r\n\
--{0}--\r\n\
",
form.boundary(),
fcontents
);

let ct = format!("multipart/form-data; boundary={}", form.boundary());

let server = server::http(move |req| {
let ct = ct.clone();
let expected_body = expected_body.clone();
async move {
assert_eq!(req.method(), "POST");
assert_eq!(req.headers()["content-type"], ct);
assert_eq!(req.headers()["transfer-encoding"], "chunked");

let full = req.collect().await.unwrap().to_bytes();

assert_eq!(full, expected_body.as_bytes());

http::Response::default()
}
});

let url = format!("http://{}/multipart/3", server.addr());

let res = reqwest::Client::new()
.post(&url)
.multipart(form)
.send()
.await
.unwrap();

assert_eq!(res.url().as_str(), &url);
assert_eq!(res.status(), reqwest::StatusCode::OK);
}

0 comments on commit bdc780b

Please sign in to comment.