From 56c1e49e620a2c52b590533d9d07afe925f49422 Mon Sep 17 00:00:00 2001 From: NaokiM03 Date: Tue, 23 Jan 2024 16:07:41 +0900 Subject: [PATCH 1/5] Add file function to async_impl::multipart --- src/async_impl/multipart.rs | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs index 6b6a81e3d..4ef424499 100644 --- a/src/async_impl/multipart.rs +++ b/src/async_impl/multipart.rs @@ -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}; @@ -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 + /// # fn run() -> std::io::Result<()> { + /// let form = reqwest::multipart::Form::new() + /// .file("key", "/path/to/file")?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + #[cfg(feature = "stream")] + #[cfg_attr(docsrs, doc(cfg(feature = "stream")))] + pub async fn file(self, name: T, path: U) -> io::Result
+ where + T: Into>, + U: AsRef, + { + Ok(self.part(name, Part::file(path).await?)) + } + /// Adds a customized Part. pub fn part(self, name: T, part: Part) -> Form where @@ -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>(path: T) -> io::Result { + 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) -> Part { Part { meta: PartMetadata::new(), From a472eef2cf7cfbbcdc79dcf4e899cffae05af95d Mon Sep 17 00:00:00 2001 From: NaokiM03 Date: Tue, 23 Jan 2024 16:07:55 +0900 Subject: [PATCH 2/5] Add test for asynchronous file function in multipart --- tests/multipart.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/multipart.rs b/tests/multipart.rs index 59ada280d..53967f79d 100644 --- a/tests/multipart.rs +++ b/tests/multipart.rs @@ -181,3 +181,61 @@ 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 |mut 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 mut full: Vec = Vec::new(); + while let Some(item) = req.body_mut().next().await { + full.extend(&*item.unwrap()); + } + + 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); +} From d40a76501ec227ce6b16671fdfa7b4aeb32304cf Mon Sep 17 00:00:00 2001 From: NaokiM03 Date: Tue, 23 Jan 2024 16:08:06 +0900 Subject: [PATCH 3/5] Fix doc of file function in blocking::multipart --- src/blocking/multipart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocking/multipart.rs b/src/blocking/multipart.rs index 5014b3975..d168ed489 100644 --- a/src/blocking/multipart.rs +++ b/src/blocking/multipart.rs @@ -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(()) /// # } From 350733fb18c29f9517ef0457fe6ebcb885130859 Mon Sep 17 00:00:00 2001 From: NaokiM03 Date: Sat, 31 Aug 2024 10:00:47 +0900 Subject: [PATCH 4/5] Fix test Follow up on this pull request https://github.com/seanmonstar/reqwest/pull/2059 --- tests/multipart.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/multipart.rs b/tests/multipart.rs index e6677fed9..bac6da314 100644 --- a/tests/multipart.rs +++ b/tests/multipart.rs @@ -202,7 +202,7 @@ async fn async_impl_file_part() { let ct = format!("multipart/form-data; boundary={}", form.boundary()); - let server = server::http(move |mut req| { + let server = server::http(move |req| { let ct = ct.clone(); let expected_body = expected_body.clone(); async move { @@ -210,10 +210,7 @@ async fn async_impl_file_part() { assert_eq!(req.headers()["content-type"], ct); assert_eq!(req.headers()["transfer-encoding"], "chunked"); - let mut full: Vec = Vec::new(); - while let Some(item) = req.body_mut().next().await { - full.extend(&*item.unwrap()); - } + let full = req.collect().await.unwrap().to_bytes(); assert_eq!(full, expected_body.as_bytes()); From 24b6270b93ce79bf07f0a14198d24d8a495f4347 Mon Sep 17 00:00:00 2001 From: NaokiM03 Date: Sat, 31 Aug 2024 10:18:14 +0900 Subject: [PATCH 5/5] Fix doc test --- src/async_impl/multipart.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs index 013a08dab..525876dde 100644 --- a/src/async_impl/multipart.rs +++ b/src/async_impl/multipart.rs @@ -96,9 +96,9 @@ impl Form { /// # Examples /// /// ```no_run - /// # fn run() -> std::io::Result<()> { + /// # async fn run() -> std::io::Result<()> { /// let form = reqwest::multipart::Form::new() - /// .file("key", "/path/to/file")?; + /// .file("key", "/path/to/file").await?; /// # Ok(()) /// # } /// ```