Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multipart with multer #1033

Merged
merged 9 commits into from
Apr 18, 2023
9 changes: 7 additions & 2 deletions Cargo.toml
Expand Up @@ -27,7 +27,7 @@ hyper = { version = "0.14", features = ["stream", "server", "http1", "http2", "t
log = "0.4"
mime = "0.3"
mime_guess = "2.0.0"
multiparty = { version = "0.1", features = ["server", "futures03"], optional = true }
multer = { version = "2.1.0", optional = true }
scoped-tls = "1.0"
serde = "1.0"
serde_json = "1.0"
Expand Down Expand Up @@ -55,7 +55,7 @@ listenfd = "1.0"

[features]
default = ["multipart", "websocket"]
multipart = ["multiparty"]
multipart = ["multer"]
websocket = ["tokio-tungstenite"]
tls = ["tokio-rustls"]

Expand Down Expand Up @@ -97,3 +97,8 @@ required-features = ["websocket"]

[[example]]
name = "query_string"


[[example]]
name = "multipart"
required-features = ["multipart"]
28 changes: 28 additions & 0 deletions examples/multipart.rs
@@ -0,0 +1,28 @@
use futures_util::TryStreamExt;
use warp::multipart::FormData;
use warp::Buf;
use warp::Filter;

#[tokio::main]
async fn main() {
// Running curl -F file=@.gitignore 'localhost:3030/' should print [("file", ".gitignore", "\n/target\n**/*.rs.bk\nCargo.lock\n.idea/\nwarp.iml\n")]
let route = warp::multipart::form().and_then(|form: FormData| async move {
let field_names: Vec<_> = form
.and_then(|mut field| async move {
let contents =
String::from_utf8_lossy(field.data().await.unwrap().unwrap().chunk())
.to_string();
Ok((
field.name().to_string(),
field.filename().unwrap().to_string(),
contents,
))
})
.try_collect()
.await
.unwrap();

Ok::<_, warp::Rejection>(format!("{:?}", field_names))
});
warp::serve(route).run(([127, 0, 0, 1], 3030)).await;
}
35 changes: 14 additions & 21 deletions src/filters/multipart.rs
Expand Up @@ -12,8 +12,7 @@ use futures_util::{future, Stream};
use headers::ContentType;
use hyper::Body;
use mime::Mime;
use multiparty::headers::Headers;
use multiparty::server::owned_futures03::{FormData as FormDataInner, Part as PartInner};
use multer::{Field as PartInner, Multipart as FormDataInner};

use crate::filter::{Filter, FilterBase, Internal};
use crate::reject::{self, Rejection};
Expand All @@ -33,15 +32,14 @@ pub struct FormOptions {
///
/// Extracted with a `warp::multipart::form` filter.
pub struct FormData {
inner: FormDataInner<BodyIoError>,
inner: FormDataInner<'static>,
}

/// A single "part" of a multipart/form-data body.
///
/// Yielded from the `FormData` stream.
pub struct Part {
headers: Headers,
part: PartInner<BodyIoError>,
part: PartInner<'static>,
}

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

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.inner).poll_next(cx) {
match self.inner.poll_next_field(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Some(Ok(part))) => {
let headers = match part.raw_headers().parse() {
Ok(headers) => headers,
Err(err) => return Poll::Ready(Some(Err(crate::Error::new(err)))),
};
Poll::Ready(Some(Ok(Part { part, headers })))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(crate::Error::new(err)))),
Poll::Ready(Ok(Some(part))) => Poll::Ready(Some(Ok(Part { part }))),
Poll::Ready(Ok(None)) => Poll::Ready(None),
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(crate::Error::new(err)))),
}
}
}
Expand All @@ -131,17 +123,18 @@ impl Stream for FormData {
impl Part {
/// Get the name of this part.
pub fn name(&self) -> &str {
&self.headers.name
self.part.name().expect("checked for name previously")
seanmonstar marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

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

if let Some(ref filename) = self.headers.filename {
if let Some(ref filename) = self.part.file_name() {
builder.field("filename", filename);
}

if let Some(ref mime) = self.headers.content_type {
if let Some(ref mime) = self.part.content_type() {
builder.field("content_type", mime);
}

Expand Down