Skip to content

Init multipart #175

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ anyhow = "1.0.26"

# features: cookies
cookie = { version = "0.14.0", features = ["percent-encode"], optional = true }
futures-core = "0.3.5"
infer = "0.2.3"
pin-project-lite = "0.2.0"
url = { version = "2.1.1", features = ["serde"] }
@@ -47,6 +48,7 @@ serde_urlencoded = "0.7.0"
rand = "0.7.3"
serde_qs = "0.7.0"
base64 = "0.13.0"
multipart = { version = "0.17.0", default-features = false, features = ["server"] }

[dev-dependencies]
http = "0.2.0"
25 changes: 23 additions & 2 deletions src/body.rs
Original file line number Diff line number Diff line change
@@ -56,7 +56,8 @@ pin_project_lite::pin_project! {
reader: Box<dyn AsyncBufRead + Unpin + Send + Sync + 'static>,
mime: Mime,
length: Option<usize>,
bytes_read: usize
bytes_read: usize,
pub(crate) file_name: Option<String>,
}
}

@@ -80,6 +81,7 @@ impl Body {
mime: mime::BYTE_STREAM,
length: Some(0),
bytes_read: 0,
file_name: None,
}
}

@@ -111,6 +113,7 @@ impl Body {
mime: mime::BYTE_STREAM,
length: len,
bytes_read: 0,
file_name: None,
}
}

@@ -155,6 +158,7 @@ impl Body {
length: Some(bytes.len()),
reader: Box::new(io::Cursor::new(bytes)),
bytes_read: 0,
file_name: None,
}
}

@@ -205,6 +209,7 @@ impl Body {
length: Some(s.len()),
reader: Box::new(io::Cursor::new(s.into_bytes())),
bytes_read: 0,
file_name: None,
}
}

@@ -251,6 +256,7 @@ impl Body {
reader: Box::new(io::Cursor::new(bytes)),
mime: mime::JSON,
bytes_read: 0,
file_name: None,
};
Ok(body)
}
@@ -316,6 +322,7 @@ impl Body {
reader: Box::new(io::Cursor::new(bytes)),
mime: mime::FORM,
bytes_read: 0,
file_name: None,
};
Ok(body)
}
@@ -370,7 +377,7 @@ impl Body {
P: AsRef<std::path::Path>,
{
let path = path.as_ref();
let mut file = async_std::fs::File::open(path).await?;
let mut file = async_std::fs::File::open(&path).await?;
let len = file.metadata().await?.len();

// Look at magic bytes first, look at extension second, fall back to
@@ -385,6 +392,7 @@ impl Body {
length: Some(len as usize),
reader: Box::new(io::BufReader::new(file)),
bytes_read: 0,
file_name: Some(path.to_string_lossy().to_string()),
})
}

@@ -419,6 +427,19 @@ impl Body {
pub fn set_mime(&mut self, mime: impl Into<Mime>) {
self.mime = mime.into();
}

/// Get the file name of the `Body`, if it's set.
pub fn file_name(&self) -> Option<&str> {
self.file_name.as_deref()
}

/// Set the file name of the `Body`.
pub fn set_file_name<S>(&mut self, file_name: Option<S>)
where
S: AsRef<str>,
{
self.file_name = file_name.map(|v| v.as_ref().to_owned());
}
}

impl Debug for Body {
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -124,6 +124,7 @@ pub mod conditional;
pub mod content;
pub mod headers;
pub mod mime;
pub mod multipart;
pub mod other;
pub mod proxies;
pub mod server;
151 changes: 151 additions & 0 deletions src/multipart/entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use crate::{Body, Mime};

use std::fmt::{self, Debug};
// use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};

use futures_lite::{io, prelude::*};

pin_project_lite::pin_project! {
/// A single multipart entry.
///
/// Structurally Multipart entries are similar to `Body`.
pub struct Entry {
name: String,
body: Body,
}
}

impl Entry {
/// Create a new `Entry`.
pub fn new<S, B>(name: S, body: B) -> Self
where
S: AsRef<str>,
B: Into<Body>,
{
Self {
name: name.as_ref().to_owned(),
body: body.into(),
}
}

/// Create an empty `Entry`.
pub fn empty<S>(name: S) -> Self
where
S: AsRef<str>,
{
Self::new(name, Body::empty())
}

/// Create an `Entry` from a file.
#[cfg(all(feature = "async_std", not(target_os = "unknown")))]
pub async fn from_file<S, P>(name: S, path: P) -> crate::Result<Self>
where
S: AsRef<str>,
P: AsRef<Path>,
{
let body = Body::from_file(path).await?;
Ok(Self::new(name, body))
}

/// Get the entry name.
pub fn name(&self) -> &String {
&self.name
}

/// Set the entry name.
pub fn set_name<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.name = name.as_ref().to_owned();
}

/// Returns the mime type of this Body.
pub fn mime(&self) -> &Mime {
self.body.mime()
}

/// Sets the mime type of this Body.
pub fn set_mime(&mut self, mime: Mime) {
self.body.set_mime(mime)
}

/// Get the file name of the entry, if it's set.
pub fn file_name(&self) -> Option<&str> {
self.body.file_name()
}

/// Set the file name of the `Body`.
pub fn set_file_name<P>(&mut self, file_name: Option<P>)
where
P: AsRef<str>,
{
self.body.set_file_name(file_name);
}
}

impl Debug for Entry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Entry")
.field("name", &self.name)
.field("body", &self.body)
.finish()
}
}

impl AsyncRead for Entry {
#[allow(missing_doc_code_examples)]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.body).poll_read(cx, buf)
}
}

impl AsyncBufRead for Entry {
#[allow(missing_doc_code_examples)]
#[allow(unused_mut)]
#[allow(unused_variables)]
fn poll_fill_buf(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
// Pin::new(&mut self.body).poll_fill_buf(cx)
todo!("Pin::new(&mut self.body).poll_fill_buf(cx)")
}

fn consume(mut self: Pin<&mut Self>, amt: usize) {
Pin::new(&mut self.body).consume(amt)
}
}

impl AsRef<Body> for Entry {
fn as_ref(&self) -> &Body {
&self.body
}
}

impl AsMut<Body> for Entry {
fn as_mut(&mut self) -> &mut Body {
&mut self.body
}
}

impl Into<Body> for Entry {
fn into(self) -> Body {
self.body
}
}

impl From<Body> for Entry {
fn from(body: Body) -> Self {
match body.file_name.clone() {
Some(name) => Self { body, name },
None => Self {
body,
name: String::new(),
},
}
}
}
191 changes: 191 additions & 0 deletions src/multipart/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//! Multipart/form-data types.
//!
//! # Specifications
//!
//! [RFC 2046, section 5.1: Multipart Media Type](https://tools.ietf.org/html/rfc2046#section-5.1)
//! [RFC 2388: Returning Values from Forms: multipart/form-data](https://tools.ietf.org/html/rfc2388)
//! [RFC 7578: Returning Values from Forms: multipart/form-data](https://tools.ietf.org/html/rfc7578)
//!
//! # Examples
//!
//! Request:
//!
//! ```
//! use http_types::multipart::{Multipart, Entry};
//!
//! let mut req = Request::new(Method::Get, "http://example.website");
//!
//! let mut multi = Multipart::new();
//! multi.push(Entry::new("description", "hello world"));
//!
//! let mut entry = Entry::from_file("my_file", Body::from_file("./cats.jpeg").await?);
//! entry.set_file_name("cats.jpeg");
//! multi.push("myFile", Body::from_file("./cats.jpeg").await?);
//!
//! req.set_body(multi);
//! ```
//!
//! Response:
//!
//! ```
//! use http_types::multipart::{Multipart, Entry};
//! let mut res = Response::new(200); // get this from somewhere
//!
//! let mut entries = res.body_multipart();
//! while let Some(entry) = entries.await {
//! println!("name: {}", entry.name());
//! println!("data: {}", entry.into_string().await?);
//! }
//! ```
use std::io::{Cursor, Read};
use std::task::Context;
use std::task::Poll;
use std::{fmt::Debug, pin::Pin, str::FromStr};

use futures_core::stream::Stream;
use futures_lite::{io, prelude::*};
use multipart::server::Multipart as Parser;

use crate::mime;
use crate::{format_err, Body, Mime, Status};
pub use entry::Entry;

mod entry;

/// A multipart response body.
pub struct Multipart {
entries: Vec<Entry>,
body: Option<Parser<Cursor<String>>>,
}

impl Debug for Multipart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Multipart").finish()
}
}

impl Multipart {
/// Create a new instance of `Multipart`.
pub fn new() -> Self {
Self {
entries: vec![],
body: None,
}
}

/// Parse a `Body` stream as a `Multipart` instance.
pub async fn from_req(req: &mut crate::Request) -> crate::Result<Self> {
let boundary = req
.content_type()
.map(|ct| ct.param("boundary").cloned())
.flatten();

let boundary = match boundary {
Some(boundary) => boundary.as_str().to_owned(),
None => {
let mut err =
format_err!("Invalid `Content-Type` header. Expected a `boundary` param");
err.set_status(400);
return Err(err);
}
};

// Not ideal, but done for now so we can avoid implementing all of Multipart ourselves for the time being.
let body = req.take_body().into_string().await?;

let multipart = Parser::with_body(Cursor::new(body), boundary);
Ok(Self {
entries: vec![],
body: Some(multipart),
})
}

/// Add a new entry to the `Multipart` instance.
pub fn push<E>(&mut self, entry: E)
where
E: Into<Entry>,
{
self.entries.push(entry.into());
// if let Some(entries) = self.entries.as_mut() {
// entries.push(entry.into());
// } else {
// self.entries = Some(vec![entry.into()]);
// }
}
}

impl Stream for Multipart {
type Item = crate::Result<Entry>;

fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
let body = match self.body.as_mut() {
None => return Poll::Ready(None),
Some(body) => body,
};

match body.read_entry() {
Ok(Some(mut field)) => {
let mut body = vec![];
field.data.read_to_end(&mut body).status(400)?;

let mut entry = Entry::new(field.headers.name, body);
entry.set_file_name(field.headers.filename);
let mime = field
.headers
.content_type
.map(|ct| Mime::from_str(&ct.to_string()))
.transpose()?;
if let Some(mime) = mime {
entry.set_mime(mime);
} else {
// Each part MAY have an (optional) "Content-Type" header
// field, which defaults to "text/plain".
// src: https://tools.ietf.org/html/rfc7578#section-4.4
entry.set_mime(mime::PLAIN);
}

Poll::Ready(Some(Ok(entry)))
}
Ok(None) => Poll::Ready(None),
Err(e) => {
let mut err = format_err!("Invalid multipart entry: {}", e);
err.set_status(400);
Poll::Ready(Some(Err(err)))
}
}
}
}

struct MultipartReader {
entry_iter: Box<dyn Iterator<Item = Entry>>,
}

impl From<Multipart> for MultipartReader {
fn from(multipart: Multipart) -> Self {
Self {
entry_iter: Box::new(multipart.entries.into_iter()),
}
}
}

impl AsyncRead for MultipartReader {
#[allow(missing_doc_code_examples)]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
if let Some(mut entry) = self.entry_iter.next() {
Pin::new(&mut entry).poll_read(cx, buf)
} else {
todo!();
}
}
}

impl From<Multipart> for Body {
fn from(_multipart: Multipart) -> Self {
todo!();
}
}
13 changes: 13 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -40,3 +40,16 @@ pub(crate) fn sort_by_weight<T: PartialOrd + Clone>(props: &mut Vec<T>) {
});
*props = arr.into_iter().map(|(_, t)| t).collect::<Vec<T>>();
}

/// Declares unstable items.
#[allow(dead_code)]
#[doc(hidden)]
macro_rules! cfg_unstable {
($($item:item)*) => {
$(
#[cfg(feature = "unstable")]
#[cfg_attr(feature = "docs", doc(cfg(unstable)))]
$item
)*
}
}