Skip to content

Commit e2f4501

Browse files
authored
Implement multipart with multer (#1033)
* Implement multipart with multer * Update multer to the version where one can poll its stream * Add a multipart example * Add a multipart example to Cargo.toml * Use expect when getting a part's name with multipart * Implement error MultipartFieldMissingName for when multer does not send a part with a name field * Reformat multipart filter with nightly * Make MultipartFieldMissingName private
1 parent 08abe44 commit e2f4501

File tree

3 files changed

+68
-22
lines changed

3 files changed

+68
-22
lines changed

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ hyper = { version = "0.14", features = ["stream", "server", "http1", "http2", "t
2727
log = "0.4"
2828
mime = "0.3"
2929
mime_guess = "2.0.0"
30-
multiparty = { version = "0.1", features = ["server", "futures03"], optional = true }
30+
multer = { version = "2.1.0", optional = true }
3131
scoped-tls = "1.0"
3232
serde = "1.0"
3333
serde_json = "1.0"
@@ -55,7 +55,7 @@ listenfd = "1.0"
5555

5656
[features]
5757
default = ["multipart", "websocket"]
58-
multipart = ["multiparty"]
58+
multipart = ["multer"]
5959
websocket = ["tokio-tungstenite"]
6060
tls = ["tokio-rustls"]
6161

@@ -97,3 +97,8 @@ required-features = ["websocket"]
9797

9898
[[example]]
9999
name = "query_string"
100+
101+
102+
[[example]]
103+
name = "multipart"
104+
required-features = ["multipart"]

examples/multipart.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use futures_util::TryStreamExt;
2+
use warp::multipart::FormData;
3+
use warp::Buf;
4+
use warp::Filter;
5+
6+
#[tokio::main]
7+
async fn main() {
8+
// Running curl -F [email protected] 'localhost:3030/' should print [("file", ".gitignore", "\n/target\n**/*.rs.bk\nCargo.lock\n.idea/\nwarp.iml\n")]
9+
let route = warp::multipart::form().and_then(|form: FormData| async move {
10+
let field_names: Vec<_> = form
11+
.and_then(|mut field| async move {
12+
let contents =
13+
String::from_utf8_lossy(field.data().await.unwrap().unwrap().chunk())
14+
.to_string();
15+
Ok((
16+
field.name().to_string(),
17+
field.filename().unwrap().to_string(),
18+
contents,
19+
))
20+
})
21+
.try_collect()
22+
.await
23+
.unwrap();
24+
25+
Ok::<_, warp::Rejection>(format!("{:?}", field_names))
26+
});
27+
warp::serve(route).run(([127, 0, 0, 1], 3030)).await;
28+
}

src/filters/multipart.rs

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//!
33
//! Filters that extract a multipart body for a route.
44
5+
use std::error::Error as StdError;
6+
use std::fmt::{Display, Formatter};
57
use std::future::Future;
68
use std::pin::Pin;
79
use std::task::{Context, Poll};
@@ -12,8 +14,7 @@ use futures_util::{future, Stream};
1214
use headers::ContentType;
1315
use hyper::Body;
1416
use mime::Mime;
15-
use multiparty::headers::Headers;
16-
use multiparty::server::owned_futures03::{FormData as FormDataInner, Part as PartInner};
17+
use multer::{Field as PartInner, Multipart as FormDataInner};
1718

1819
use crate::filter::{Filter, FilterBase, Internal};
1920
use crate::reject::{self, Rejection};
@@ -33,15 +34,14 @@ pub struct FormOptions {
3334
///
3435
/// Extracted with a `warp::multipart::form` filter.
3536
pub struct FormData {
36-
inner: FormDataInner<BodyIoError>,
37+
inner: FormDataInner<'static>,
3738
}
3839

3940
/// A single "part" of a multipart/form-data body.
4041
///
4142
/// Yielded from the `FormData` stream.
4243
pub struct Part {
43-
headers: Headers,
44-
part: PartInner<BodyIoError>,
44+
part: PartInner<'static>,
4545
}
4646

4747
/// Create a `Filter` to extract a `multipart/form-data` body from a request.
@@ -111,17 +111,17 @@ impl Stream for FormData {
111111
type Item = Result<Part, crate::Error>;
112112

113113
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
114-
match Pin::new(&mut self.inner).poll_next(cx) {
114+
match self.inner.poll_next_field(cx) {
115115
Poll::Pending => Poll::Pending,
116-
Poll::Ready(Some(Ok(part))) => {
117-
let headers = match part.raw_headers().parse() {
118-
Ok(headers) => headers,
119-
Err(err) => return Poll::Ready(Some(Err(crate::Error::new(err)))),
120-
};
121-
Poll::Ready(Some(Ok(Part { part, headers })))
116+
Poll::Ready(Ok(Some(part))) => {
117+
if part.name().is_some() {
118+
Poll::Ready(Some(Ok(Part { part })))
119+
} else {
120+
Poll::Ready(Some(Err(crate::Error::new(MultipartFieldMissingName))))
121+
}
122122
}
123-
Poll::Ready(None) => Poll::Ready(None),
124-
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(crate::Error::new(err)))),
123+
Poll::Ready(Ok(None)) => Poll::Ready(None),
124+
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(crate::Error::new(err)))),
125125
}
126126
}
127127
}
@@ -131,17 +131,18 @@ impl Stream for FormData {
131131
impl Part {
132132
/// Get the name of this part.
133133
pub fn name(&self) -> &str {
134-
&self.headers.name
134+
self.part.name().expect("checked for name previously")
135135
}
136136

137137
/// Get the filename of this part, if present.
138138
pub fn filename(&self) -> Option<&str> {
139-
self.headers.filename.as_deref()
139+
self.part.file_name()
140140
}
141141

142142
/// Get the content-type of this part, if present.
143143
pub fn content_type(&self) -> Option<&str> {
144-
self.headers.content_type.as_deref()
144+
let content_type = self.part.content_type();
145+
content_type.map(|t| t.type_().as_str())
145146
}
146147

147148
/// Asynchronously get some of the data for this `Part`.
@@ -167,13 +168,13 @@ impl Part {
167168
impl fmt::Debug for Part {
168169
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169170
let mut builder = f.debug_struct("Part");
170-
builder.field("name", &self.headers.name);
171+
builder.field("name", &self.part.name());
171172

172-
if let Some(ref filename) = self.headers.filename {
173+
if let Some(ref filename) = self.part.file_name() {
173174
builder.field("filename", filename);
174175
}
175176

176-
if let Some(ref mime) = self.headers.content_type {
177+
if let Some(ref mime) = self.part.content_type() {
177178
builder.field("content_type", mime);
178179
}
179180

@@ -207,3 +208,15 @@ impl Stream for BodyIoError {
207208
}
208209
}
209210
}
211+
212+
/// An error used when a multipart field is missing a name.
213+
#[derive(Debug)]
214+
struct MultipartFieldMissingName;
215+
216+
impl Display for MultipartFieldMissingName {
217+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218+
write!(f, "Multipart field is missing a name")
219+
}
220+
}
221+
222+
impl StdError for MultipartFieldMissingName {}

0 commit comments

Comments
 (0)