Skip to content

Commit cc3dd51

Browse files
authored
Add file function to async::multipart (#2106)
* 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 #2059 * Fix doc test
1 parent 193ed1f commit cc3dd51

File tree

3 files changed

+114
-1
lines changed

3 files changed

+114
-1
lines changed

src/async_impl/multipart.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ use std::borrow::Cow;
33
use std::fmt;
44
use std::pin::Pin;
55

6+
#[cfg(feature = "stream")]
7+
use std::io;
8+
#[cfg(feature = "stream")]
9+
use std::path::Path;
10+
611
use bytes::Bytes;
712
use mime_guess::Mime;
813
use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
14+
#[cfg(feature = "stream")]
15+
use tokio::fs::File;
916

1017
use futures_core::Stream;
1118
use futures_util::{future, stream, StreamExt};
@@ -82,6 +89,33 @@ impl Form {
8289
self.part(name, Part::text(value))
8390
}
8491

92+
/// Adds a file field.
93+
///
94+
/// The path will be used to try to guess the filename and mime.
95+
///
96+
/// # Examples
97+
///
98+
/// ```no_run
99+
/// # async fn run() -> std::io::Result<()> {
100+
/// let form = reqwest::multipart::Form::new()
101+
/// .file("key", "/path/to/file").await?;
102+
/// # Ok(())
103+
/// # }
104+
/// ```
105+
///
106+
/// # Errors
107+
///
108+
/// Errors when the file cannot be opened.
109+
#[cfg(feature = "stream")]
110+
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
111+
pub async fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
112+
where
113+
T: Into<Cow<'static, str>>,
114+
U: AsRef<Path>,
115+
{
116+
Ok(self.part(name, Part::file(path).await?))
117+
}
118+
85119
/// Adds a customized Part.
86120
pub fn part<T>(self, name: T, part: Part) -> Form
87121
where
@@ -218,6 +252,30 @@ impl Part {
218252
Part::new(value.into(), Some(length))
219253
}
220254

255+
/// Makes a file parameter.
256+
///
257+
/// # Errors
258+
///
259+
/// Errors when the file cannot be opened.
260+
#[cfg(feature = "stream")]
261+
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
262+
pub async fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
263+
let path = path.as_ref();
264+
let file_name = path
265+
.file_name()
266+
.map(|filename| filename.to_string_lossy().into_owned());
267+
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
268+
let mime = mime_guess::from_ext(ext).first_or_octet_stream();
269+
let file = File::open(path).await?;
270+
let field = Part::stream(file).mime(mime);
271+
272+
Ok(if let Some(file_name) = file_name {
273+
field.file_name(file_name)
274+
} else {
275+
field
276+
})
277+
}
278+
221279
fn new(value: Body, body_length: Option<u64>) -> Part {
222280
Part {
223281
meta: PartMetadata::new(),

src/blocking/multipart.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl Form {
104104
///
105105
/// ```no_run
106106
/// # fn run() -> std::io::Result<()> {
107-
/// let files = reqwest::blocking::multipart::Form::new()
107+
/// let form = reqwest::blocking::multipart::Form::new()
108108
/// .file("key", "/path/to/file")?;
109109
/// # Ok(())
110110
/// # }

tests/multipart.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,58 @@ fn blocking_file_part() {
175175
assert_eq!(res.url().as_str(), &url);
176176
assert_eq!(res.status(), reqwest::StatusCode::OK);
177177
}
178+
179+
#[cfg(feature = "stream")]
180+
#[tokio::test]
181+
async fn async_impl_file_part() {
182+
let _ = env_logger::try_init();
183+
184+
let form = reqwest::multipart::Form::new()
185+
.file("foo", "Cargo.lock")
186+
.await
187+
.unwrap();
188+
189+
let fcontents = std::fs::read_to_string("Cargo.lock").unwrap();
190+
191+
let expected_body = format!(
192+
"\
193+
--{0}\r\n\
194+
Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\
195+
Content-Type: application/octet-stream\r\n\r\n\
196+
{1}\r\n\
197+
--{0}--\r\n\
198+
",
199+
form.boundary(),
200+
fcontents
201+
);
202+
203+
let ct = format!("multipart/form-data; boundary={}", form.boundary());
204+
205+
let server = server::http(move |req| {
206+
let ct = ct.clone();
207+
let expected_body = expected_body.clone();
208+
async move {
209+
assert_eq!(req.method(), "POST");
210+
assert_eq!(req.headers()["content-type"], ct);
211+
assert_eq!(req.headers()["transfer-encoding"], "chunked");
212+
213+
let full = req.collect().await.unwrap().to_bytes();
214+
215+
assert_eq!(full, expected_body.as_bytes());
216+
217+
http::Response::default()
218+
}
219+
});
220+
221+
let url = format!("http://{}/multipart/3", server.addr());
222+
223+
let res = reqwest::Client::new()
224+
.post(&url)
225+
.multipart(form)
226+
.send()
227+
.await
228+
.unwrap();
229+
230+
assert_eq!(res.url().as_str(), &url);
231+
assert_eq!(res.status(), reqwest::StatusCode::OK);
232+
}

0 commit comments

Comments
 (0)