mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 17:52:56 +01:00
move multipart support to separate crate
This commit is contained in:
parent
f56072954b
commit
e738361e09
@ -1,5 +1,12 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
## [1.0.0-alpha.3] - 2019-04-xx
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Move multipart support to actix-multipart crate
|
||||||
|
|
||||||
|
|
||||||
## [1.0.0-alpha.3] - 2019-04-02
|
## [1.0.0-alpha.3] - 2019-04-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -30,6 +30,7 @@ members = [
|
|||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
|
"actix-multipart",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
"actix-web-codegen",
|
"actix-web-codegen",
|
||||||
"test-server",
|
"test-server",
|
||||||
@ -83,7 +84,6 @@ derive_more = "0.14"
|
|||||||
encoding = "0.2"
|
encoding = "0.2"
|
||||||
futures = "0.1"
|
futures = "0.1"
|
||||||
hashbrown = "0.1.8"
|
hashbrown = "0.1.8"
|
||||||
httparse = "1.3"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
net2 = "0.2.33"
|
net2 = "0.2.33"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
## [0.1.0-alpha.4] - 2019-04-xx
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
|
|
||||||
* Removed PayloadBuffer
|
* Removed PayloadBuffer
|
||||||
|
@ -12,7 +12,7 @@ mod service;
|
|||||||
pub use self::client::{ClientCodec, ClientPayloadCodec};
|
pub use self::client::{ClientCodec, ClientPayloadCodec};
|
||||||
pub use self::codec::Codec;
|
pub use self::codec::Codec;
|
||||||
pub use self::dispatcher::Dispatcher;
|
pub use self::dispatcher::Dispatcher;
|
||||||
pub use self::payload::Payload;
|
pub use self::payload::{Payload, PayloadWriter};
|
||||||
pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
|
pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -14,7 +14,7 @@ use crate::error::PayloadError;
|
|||||||
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) enum PayloadStatus {
|
pub enum PayloadStatus {
|
||||||
Read,
|
Read,
|
||||||
Pause,
|
Pause,
|
||||||
Dropped,
|
Dropped,
|
||||||
@ -106,7 +106,7 @@ impl Clone for Payload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Payload writer interface.
|
/// Payload writer interface.
|
||||||
pub(crate) trait PayloadWriter {
|
pub trait PayloadWriter {
|
||||||
/// Set stream error.
|
/// Set stream error.
|
||||||
fn set_error(&mut self, err: PayloadError);
|
fn set_error(&mut self, err: PayloadError);
|
||||||
|
|
||||||
|
5
actix-multipart/CHANGES.md
Normal file
5
actix-multipart/CHANGES.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Changes
|
||||||
|
|
||||||
|
## [0.1.0-alpha.1] - 2019-04-xx
|
||||||
|
|
||||||
|
* Split multipart support to separate crate
|
34
actix-multipart/Cargo.toml
Normal file
34
actix-multipart/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "actix-multipart"
|
||||||
|
version = "0.1.0-alpha.1"
|
||||||
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
|
description = "Multipart support for actix web framework."
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
|
homepage = "https://actix.rs"
|
||||||
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
|
documentation = "https://docs.rs/actix-multipart/"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
|
||||||
|
workspace = ".."
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "actix_multipart"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "1.0.0-alpha.3"
|
||||||
|
actix-service = "0.3.4"
|
||||||
|
bytes = "0.4"
|
||||||
|
derive_more = "0.14"
|
||||||
|
httparse = "1.3"
|
||||||
|
futures = "0.1.25"
|
||||||
|
log = "0.4"
|
||||||
|
mime = "0.3"
|
||||||
|
time = "0.1"
|
||||||
|
twoway = "0.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-rt = "0.2.2"
|
||||||
|
actix-http = "0.1.0-alpha.3"
|
1
actix-multipart/README.md
Normal file
1
actix-multipart/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Multipart support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-session)](https://crates.io/crates/actix-session) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
46
actix-multipart/src/error.rs
Normal file
46
actix-multipart/src/error.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//! Error and Result module
|
||||||
|
use actix_web::error::{ParseError, PayloadError};
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use derive_more::{Display, From};
|
||||||
|
|
||||||
|
/// A set of errors that can occur during parsing multipart streams
|
||||||
|
#[derive(Debug, Display, From)]
|
||||||
|
pub enum MultipartError {
|
||||||
|
/// Content-Type header is not found
|
||||||
|
#[display(fmt = "No Content-type header found")]
|
||||||
|
NoContentType,
|
||||||
|
/// Can not parse Content-Type header
|
||||||
|
#[display(fmt = "Can not parse Content-Type header")]
|
||||||
|
ParseContentType,
|
||||||
|
/// Multipart boundary is not found
|
||||||
|
#[display(fmt = "Multipart boundary is not found")]
|
||||||
|
Boundary,
|
||||||
|
/// Multipart stream is incomplete
|
||||||
|
#[display(fmt = "Multipart stream is incomplete")]
|
||||||
|
Incomplete,
|
||||||
|
/// Error during field parsing
|
||||||
|
#[display(fmt = "{}", _0)]
|
||||||
|
Parse(ParseError),
|
||||||
|
/// Payload error
|
||||||
|
#[display(fmt = "{}", _0)]
|
||||||
|
Payload(PayloadError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `BadRequest` for `MultipartError`
|
||||||
|
impl ResponseError for MultipartError {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::new(StatusCode::BAD_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multipart_error() {
|
||||||
|
let resp: HttpResponse = MultipartError::Boundary.error_response();
|
||||||
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
57
actix-multipart/src/extractor.rs
Normal file
57
actix-multipart/src/extractor.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
//! Multipart payload support
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures::Stream;
|
||||||
|
|
||||||
|
use actix_web::dev::ServiceFromRequest;
|
||||||
|
use actix_web::error::{Error, PayloadError};
|
||||||
|
use actix_web::FromRequest;
|
||||||
|
use actix_web::HttpMessage;
|
||||||
|
|
||||||
|
use crate::server::Multipart;
|
||||||
|
|
||||||
|
/// Get request's payload as multipart stream
|
||||||
|
///
|
||||||
|
/// Content-type: multipart/form-data;
|
||||||
|
///
|
||||||
|
/// ## Server example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use futures::{Future, Stream};
|
||||||
|
/// # use futures::future::{ok, result, Either};
|
||||||
|
/// use actix_web::{web, HttpResponse, Error};
|
||||||
|
/// use actix_multipart as mp;
|
||||||
|
///
|
||||||
|
/// fn index(payload: mp::Multipart) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
|
/// payload.from_err() // <- get multipart stream for current request
|
||||||
|
/// .and_then(|item| match item { // <- iterate over multipart items
|
||||||
|
/// mp::Item::Field(field) => {
|
||||||
|
/// // Field in turn is stream of *Bytes* object
|
||||||
|
/// Either::A(field.from_err()
|
||||||
|
/// .fold((), |_, chunk| {
|
||||||
|
/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk));
|
||||||
|
/// Ok::<_, Error>(())
|
||||||
|
/// }))
|
||||||
|
/// },
|
||||||
|
/// mp::Item::Nested(mp) => {
|
||||||
|
/// // Or item could be nested Multipart stream
|
||||||
|
/// Either::B(ok(()))
|
||||||
|
/// }
|
||||||
|
/// })
|
||||||
|
/// .fold((), |_, _| Ok::<_, Error>(()))
|
||||||
|
/// .map(|_| HttpResponse::Ok().into())
|
||||||
|
/// }
|
||||||
|
/// # fn main() {}
|
||||||
|
/// ```
|
||||||
|
impl<P> FromRequest<P> for Multipart
|
||||||
|
where
|
||||||
|
P: Stream<Item = Bytes, Error = PayloadError> + 'static,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
type Future = Result<Multipart, Error>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_request(req: &mut ServiceFromRequest<P>) -> Self::Future {
|
||||||
|
let pl = req.take_payload();
|
||||||
|
Ok(Multipart::new(req.headers(), pl))
|
||||||
|
}
|
||||||
|
}
|
6
actix-multipart/src/lib.rs
Normal file
6
actix-multipart/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod error;
|
||||||
|
mod extractor;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
pub use self::error::MultipartError;
|
||||||
|
pub use self::server::{Field, Item, Multipart};
|
@ -4,26 +4,22 @@ use std::marker::PhantomData;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::{cmp, fmt};
|
use std::{cmp, fmt};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures::task::{current as current_task, Task};
|
use futures::task::{current as current_task, Task};
|
||||||
use futures::{Async, Poll, Stream};
|
use futures::{Async, Poll, Stream};
|
||||||
use httparse;
|
use httparse;
|
||||||
use mime;
|
use mime;
|
||||||
|
|
||||||
use crate::error::{Error, MultipartError, ParseError, PayloadError};
|
use actix_web::error::{ParseError, PayloadError};
|
||||||
use crate::extract::FromRequest;
|
use actix_web::http::header::{
|
||||||
use crate::http::header::{
|
|
||||||
self, ContentDisposition, HeaderMap, HeaderName, HeaderValue,
|
self, ContentDisposition, HeaderMap, HeaderName, HeaderValue,
|
||||||
};
|
};
|
||||||
use crate::http::HttpTryFrom;
|
use actix_web::http::HttpTryFrom;
|
||||||
use crate::service::ServiceFromRequest;
|
|
||||||
use crate::HttpMessage;
|
use crate::error::MultipartError;
|
||||||
|
|
||||||
const MAX_HEADERS: usize = 32;
|
const MAX_HEADERS: usize = 32;
|
||||||
|
|
||||||
type PayloadBuffer =
|
|
||||||
actix_http::h1::PayloadBuffer<Box<dyn Stream<Item = Bytes, Error = PayloadError>>>;
|
|
||||||
|
|
||||||
/// The server-side implementation of `multipart/form-data` requests.
|
/// The server-side implementation of `multipart/form-data` requests.
|
||||||
///
|
///
|
||||||
/// This will parse the incoming stream into `MultipartItem` instances via its
|
/// This will parse the incoming stream into `MultipartItem` instances via its
|
||||||
@ -37,59 +33,13 @@ pub struct Multipart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Multipart item
|
/// Multipart item
|
||||||
pub enum MultipartItem {
|
pub enum Item {
|
||||||
/// Multipart field
|
/// Multipart field
|
||||||
Field(MultipartField),
|
Field(Field),
|
||||||
/// Nested multipart stream
|
/// Nested multipart stream
|
||||||
Nested(Multipart),
|
Nested(Multipart),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get request's payload as multipart stream
|
|
||||||
///
|
|
||||||
/// Content-type: multipart/form-data;
|
|
||||||
///
|
|
||||||
/// ## Server example
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use futures::{Future, Stream};
|
|
||||||
/// # use futures::future::{ok, result, Either};
|
|
||||||
/// use actix_web::{web, HttpResponse, Error};
|
|
||||||
///
|
|
||||||
/// fn index(payload: web::Multipart) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
||||||
/// payload.from_err() // <- get multipart stream for current request
|
|
||||||
/// .and_then(|item| match item { // <- iterate over multipart items
|
|
||||||
/// web::MultipartItem::Field(field) => {
|
|
||||||
/// // Field in turn is stream of *Bytes* object
|
|
||||||
/// Either::A(field.from_err()
|
|
||||||
/// .fold((), |_, chunk| {
|
|
||||||
/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk));
|
|
||||||
/// Ok::<_, Error>(())
|
|
||||||
/// }))
|
|
||||||
/// },
|
|
||||||
/// web::MultipartItem::Nested(mp) => {
|
|
||||||
/// // Or item could be nested Multipart stream
|
|
||||||
/// Either::B(ok(()))
|
|
||||||
/// }
|
|
||||||
/// })
|
|
||||||
/// .fold((), |_, _| Ok::<_, Error>(()))
|
|
||||||
/// .map(|_| HttpResponse::Ok().into())
|
|
||||||
/// }
|
|
||||||
/// # fn main() {}
|
|
||||||
/// ```
|
|
||||||
impl<P> FromRequest<P> for Multipart
|
|
||||||
where
|
|
||||||
P: Stream<Item = Bytes, Error = PayloadError> + 'static,
|
|
||||||
{
|
|
||||||
type Error = Error;
|
|
||||||
type Future = Result<Multipart, Error>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn from_request(req: &mut ServiceFromRequest<P>) -> Self::Future {
|
|
||||||
let pl = req.take_payload();
|
|
||||||
Ok(Multipart::new(req.headers(), pl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum InnerMultipartItem {
|
enum InnerMultipartItem {
|
||||||
None,
|
None,
|
||||||
Field(Rc<RefCell<InnerField>>),
|
Field(Rc<RefCell<InnerField>>),
|
||||||
@ -163,14 +113,18 @@ impl Multipart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for Multipart {
|
impl Stream for Multipart {
|
||||||
type Item = MultipartItem;
|
type Item = Item;
|
||||||
type Error = MultipartError;
|
type Error = MultipartError;
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||||
if let Some(err) = self.error.take() {
|
if let Some(err) = self.error.take() {
|
||||||
Err(err)
|
Err(err)
|
||||||
} else if self.safety.current() {
|
} else if self.safety.current() {
|
||||||
self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety)
|
let mut inner = self.inner.as_mut().unwrap().borrow_mut();
|
||||||
|
if let Some(payload) = inner.payload.get_mut(&self.safety) {
|
||||||
|
payload.poll_stream()?;
|
||||||
|
}
|
||||||
|
inner.poll(&self.safety)
|
||||||
} else {
|
} else {
|
||||||
Ok(Async::NotReady)
|
Ok(Async::NotReady)
|
||||||
}
|
}
|
||||||
@ -178,11 +132,18 @@ impl Stream for Multipart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InnerMultipart {
|
impl InnerMultipart {
|
||||||
fn read_headers(payload: &mut PayloadBuffer) -> Poll<HeaderMap, MultipartError> {
|
fn read_headers(
|
||||||
match payload.read_until(b"\r\n\r\n")? {
|
payload: &mut PayloadBuffer,
|
||||||
Async::NotReady => Ok(Async::NotReady),
|
) -> Result<Option<HeaderMap>, MultipartError> {
|
||||||
Async::Ready(None) => Err(MultipartError::Incomplete),
|
match payload.read_until(b"\r\n\r\n") {
|
||||||
Async::Ready(Some(bytes)) => {
|
None => {
|
||||||
|
if payload.eof {
|
||||||
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(bytes) => {
|
||||||
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
|
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||||
match httparse::parse_headers(&bytes, &mut hdrs) {
|
match httparse::parse_headers(&bytes, &mut hdrs) {
|
||||||
Ok(httparse::Status::Complete((_, hdrs))) => {
|
Ok(httparse::Status::Complete((_, hdrs))) => {
|
||||||
@ -199,7 +160,7 @@ impl InnerMultipart {
|
|||||||
return Err(ParseError::Header.into());
|
return Err(ParseError::Header.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Async::Ready(headers))
|
Ok(Some(headers))
|
||||||
}
|
}
|
||||||
Ok(httparse::Status::Partial) => Err(ParseError::Header.into()),
|
Ok(httparse::Status::Partial) => Err(ParseError::Header.into()),
|
||||||
Err(err) => Err(ParseError::from(err).into()),
|
Err(err) => Err(ParseError::from(err).into()),
|
||||||
@ -211,23 +172,28 @@ impl InnerMultipart {
|
|||||||
fn read_boundary(
|
fn read_boundary(
|
||||||
payload: &mut PayloadBuffer,
|
payload: &mut PayloadBuffer,
|
||||||
boundary: &str,
|
boundary: &str,
|
||||||
) -> Poll<bool, MultipartError> {
|
) -> Result<Option<bool>, MultipartError> {
|
||||||
// TODO: need to read epilogue
|
// TODO: need to read epilogue
|
||||||
match payload.readline()? {
|
match payload.readline() {
|
||||||
Async::NotReady => Ok(Async::NotReady),
|
None => {
|
||||||
Async::Ready(None) => Err(MultipartError::Incomplete),
|
if payload.eof {
|
||||||
Async::Ready(Some(chunk)) => {
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(chunk) => {
|
||||||
if chunk.len() == boundary.len() + 4
|
if chunk.len() == boundary.len() + 4
|
||||||
&& &chunk[..2] == b"--"
|
&& &chunk[..2] == b"--"
|
||||||
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
||||||
{
|
{
|
||||||
Ok(Async::Ready(false))
|
Ok(Some(false))
|
||||||
} else if chunk.len() == boundary.len() + 6
|
} else if chunk.len() == boundary.len() + 6
|
||||||
&& &chunk[..2] == b"--"
|
&& &chunk[..2] == b"--"
|
||||||
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
||||||
&& &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
|
&& &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
|
||||||
{
|
{
|
||||||
Ok(Async::Ready(true))
|
Ok(Some(true))
|
||||||
} else {
|
} else {
|
||||||
Err(MultipartError::Boundary)
|
Err(MultipartError::Boundary)
|
||||||
}
|
}
|
||||||
@ -238,11 +204,11 @@ impl InnerMultipart {
|
|||||||
fn skip_until_boundary(
|
fn skip_until_boundary(
|
||||||
payload: &mut PayloadBuffer,
|
payload: &mut PayloadBuffer,
|
||||||
boundary: &str,
|
boundary: &str,
|
||||||
) -> Poll<bool, MultipartError> {
|
) -> Result<Option<bool>, MultipartError> {
|
||||||
let mut eof = false;
|
let mut eof = false;
|
||||||
loop {
|
loop {
|
||||||
match payload.readline()? {
|
match payload.readline() {
|
||||||
Async::Ready(Some(chunk)) => {
|
Some(chunk) => {
|
||||||
if chunk.is_empty() {
|
if chunk.is_empty() {
|
||||||
//ValueError("Could not find starting boundary %r"
|
//ValueError("Could not find starting boundary %r"
|
||||||
//% (self._boundary))
|
//% (self._boundary))
|
||||||
@ -267,14 +233,19 @@ impl InnerMultipart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
None => {
|
||||||
Async::Ready(None) => return Err(MultipartError::Incomplete),
|
return if payload.eof {
|
||||||
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Async::Ready(eof))
|
Ok(Some(eof))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(&mut self, safety: &Safety) -> Poll<Option<MultipartItem>, MultipartError> {
|
fn poll(&mut self, safety: &Safety) -> Poll<Option<Item>, MultipartError> {
|
||||||
if self.state == InnerState::Eof {
|
if self.state == InnerState::Eof {
|
||||||
Ok(Async::Ready(None))
|
Ok(Async::Ready(None))
|
||||||
} else {
|
} else {
|
||||||
@ -317,7 +288,7 @@ impl InnerMultipart {
|
|||||||
payload,
|
payload,
|
||||||
&self.boundary,
|
&self.boundary,
|
||||||
)? {
|
)? {
|
||||||
Async::Ready(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
self.state = InnerState::Eof;
|
self.state = InnerState::Eof;
|
||||||
return Ok(Async::Ready(None));
|
return Ok(Async::Ready(None));
|
||||||
@ -325,14 +296,14 @@ impl InnerMultipart {
|
|||||||
self.state = InnerState::Headers;
|
self.state = InnerState::Headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
None => return Ok(Async::NotReady),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// read boundary
|
// read boundary
|
||||||
InnerState::Boundary => {
|
InnerState::Boundary => {
|
||||||
match InnerMultipart::read_boundary(payload, &self.boundary)? {
|
match InnerMultipart::read_boundary(payload, &self.boundary)? {
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
None => return Ok(Async::NotReady),
|
||||||
Async::Ready(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
self.state = InnerState::Eof;
|
self.state = InnerState::Eof;
|
||||||
return Ok(Async::Ready(None));
|
return Ok(Async::Ready(None));
|
||||||
@ -347,8 +318,7 @@ impl InnerMultipart {
|
|||||||
|
|
||||||
// read field headers for next field
|
// read field headers for next field
|
||||||
if self.state == InnerState::Headers {
|
if self.state == InnerState::Headers {
|
||||||
if let Async::Ready(headers) = InnerMultipart::read_headers(payload)?
|
if let Some(headers) = InnerMultipart::read_headers(payload)? {
|
||||||
{
|
|
||||||
self.state = InnerState::Boundary;
|
self.state = InnerState::Boundary;
|
||||||
headers
|
headers
|
||||||
} else {
|
} else {
|
||||||
@ -389,7 +359,7 @@ impl InnerMultipart {
|
|||||||
|
|
||||||
self.item = InnerMultipartItem::Multipart(Rc::clone(&inner));
|
self.item = InnerMultipartItem::Multipart(Rc::clone(&inner));
|
||||||
|
|
||||||
Ok(Async::Ready(Some(MultipartItem::Nested(Multipart {
|
Ok(Async::Ready(Some(Item::Nested(Multipart {
|
||||||
safety: safety.clone(),
|
safety: safety.clone(),
|
||||||
error: None,
|
error: None,
|
||||||
inner: Some(inner),
|
inner: Some(inner),
|
||||||
@ -402,9 +372,12 @@ impl InnerMultipart {
|
|||||||
)?));
|
)?));
|
||||||
self.item = InnerMultipartItem::Field(Rc::clone(&field));
|
self.item = InnerMultipartItem::Field(Rc::clone(&field));
|
||||||
|
|
||||||
Ok(Async::Ready(Some(MultipartItem::Field(
|
Ok(Async::Ready(Some(Item::Field(Field::new(
|
||||||
MultipartField::new(safety.clone(), headers, mt, field),
|
safety.clone(),
|
||||||
))))
|
headers,
|
||||||
|
mt,
|
||||||
|
field,
|
||||||
|
)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -418,21 +391,21 @@ impl Drop for InnerMultipart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A single field in a multipart stream
|
/// A single field in a multipart stream
|
||||||
pub struct MultipartField {
|
pub struct Field {
|
||||||
ct: mime::Mime,
|
ct: mime::Mime,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
safety: Safety,
|
safety: Safety,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MultipartField {
|
impl Field {
|
||||||
fn new(
|
fn new(
|
||||||
safety: Safety,
|
safety: Safety,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
ct: mime::Mime,
|
ct: mime::Mime,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
MultipartField {
|
Field {
|
||||||
ct,
|
ct,
|
||||||
headers,
|
headers,
|
||||||
inner,
|
inner,
|
||||||
@ -463,22 +436,28 @@ impl MultipartField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for MultipartField {
|
impl Stream for Field {
|
||||||
type Item = Bytes;
|
type Item = Bytes;
|
||||||
type Error = MultipartError;
|
type Error = MultipartError;
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||||
if self.safety.current() {
|
if self.safety.current() {
|
||||||
self.inner.borrow_mut().poll(&self.safety)
|
let mut inner = self.inner.borrow_mut();
|
||||||
|
if let Some(payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety)
|
||||||
|
{
|
||||||
|
payload.poll_stream()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.poll(&self.safety)
|
||||||
} else {
|
} else {
|
||||||
Ok(Async::NotReady)
|
Ok(Async::NotReady)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for MultipartField {
|
impl fmt::Debug for Field {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
writeln!(f, "\nMultipartField: {}", self.ct)?;
|
writeln!(f, "\nField: {}", self.ct)?;
|
||||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||||
writeln!(f, " headers:")?;
|
writeln!(f, " headers:")?;
|
||||||
for (key, val) in self.headers.iter() {
|
for (key, val) in self.headers.iter() {
|
||||||
@ -532,10 +511,8 @@ impl InnerField {
|
|||||||
if *size == 0 {
|
if *size == 0 {
|
||||||
Ok(Async::Ready(None))
|
Ok(Async::Ready(None))
|
||||||
} else {
|
} else {
|
||||||
match payload.readany() {
|
match payload.read_max(*size) {
|
||||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
Some(mut chunk) => {
|
||||||
Ok(Async::Ready(None)) => Err(MultipartError::Incomplete),
|
|
||||||
Ok(Async::Ready(Some(mut chunk))) => {
|
|
||||||
let len = cmp::min(chunk.len() as u64, *size);
|
let len = cmp::min(chunk.len() as u64, *size);
|
||||||
*size -= len;
|
*size -= len;
|
||||||
let ch = chunk.split_to(len as usize);
|
let ch = chunk.split_to(len as usize);
|
||||||
@ -544,7 +521,13 @@ impl InnerField {
|
|||||||
}
|
}
|
||||||
Ok(Async::Ready(Some(ch)))
|
Ok(Async::Ready(Some(ch)))
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
None => {
|
||||||
|
if payload.eof && (*size != 0) {
|
||||||
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(Async::NotReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -555,16 +538,26 @@ impl InnerField {
|
|||||||
payload: &mut PayloadBuffer,
|
payload: &mut PayloadBuffer,
|
||||||
boundary: &str,
|
boundary: &str,
|
||||||
) -> Poll<Option<Bytes>, MultipartError> {
|
) -> Poll<Option<Bytes>, MultipartError> {
|
||||||
match payload.read_until(b"\r")? {
|
match payload.read_until(b"\r") {
|
||||||
Async::NotReady => Ok(Async::NotReady),
|
None => {
|
||||||
Async::Ready(None) => Err(MultipartError::Incomplete),
|
if payload.eof {
|
||||||
Async::Ready(Some(mut chunk)) => {
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(Async::NotReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(mut chunk) => {
|
||||||
if chunk.len() == 1 {
|
if chunk.len() == 1 {
|
||||||
payload.unprocessed(chunk);
|
payload.unprocessed(chunk);
|
||||||
match payload.read_exact(boundary.len() + 4)? {
|
match payload.read_exact(boundary.len() + 4) {
|
||||||
Async::NotReady => Ok(Async::NotReady),
|
None => {
|
||||||
Async::Ready(None) => Err(MultipartError::Incomplete),
|
if payload.eof {
|
||||||
Async::Ready(Some(mut chunk)) => {
|
Err(MultipartError::Incomplete)
|
||||||
|
} else {
|
||||||
|
Ok(Async::NotReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(mut chunk) => {
|
||||||
if &chunk[..2] == b"\r\n"
|
if &chunk[..2] == b"\r\n"
|
||||||
&& &chunk[2..4] == b"--"
|
&& &chunk[2..4] == b"--"
|
||||||
&& &chunk[4..] == boundary.as_bytes()
|
&& &chunk[4..] == boundary.as_bytes()
|
||||||
@ -606,10 +599,9 @@ impl InnerField {
|
|||||||
Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)),
|
Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)),
|
||||||
Async::Ready(None) => {
|
Async::Ready(None) => {
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
match payload.readline()? {
|
match payload.readline() {
|
||||||
Async::NotReady => Async::NotReady,
|
None => Async::Ready(None),
|
||||||
Async::Ready(None) => Async::Ready(None),
|
Some(line) => {
|
||||||
Async::Ready(Some(line)) => {
|
|
||||||
if line.as_ref() != b"\r\n" {
|
if line.as_ref() != b"\r\n" {
|
||||||
log::warn!("multipart field did not read all the data or it is malformed");
|
log::warn!("multipart field did not read all the data or it is malformed");
|
||||||
}
|
}
|
||||||
@ -711,14 +703,86 @@ impl Drop for Safety {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Payload buffer
|
||||||
|
struct PayloadBuffer {
|
||||||
|
eof: bool,
|
||||||
|
buf: BytesMut,
|
||||||
|
stream: Box<dyn Stream<Item = Bytes, Error = PayloadError>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PayloadBuffer {
|
||||||
|
/// Create new `PayloadBuffer` instance
|
||||||
|
fn new<S>(stream: S) -> Self
|
||||||
|
where
|
||||||
|
S: Stream<Item = Bytes, Error = PayloadError> + 'static,
|
||||||
|
{
|
||||||
|
PayloadBuffer {
|
||||||
|
eof: false,
|
||||||
|
buf: BytesMut::new(),
|
||||||
|
stream: Box::new(stream),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_stream(&mut self) -> Result<(), PayloadError> {
|
||||||
|
loop {
|
||||||
|
match self.stream.poll()? {
|
||||||
|
Async::Ready(Some(data)) => self.buf.extend_from_slice(&data),
|
||||||
|
Async::Ready(None) => {
|
||||||
|
self.eof = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Async::NotReady => return Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read exact number of bytes
|
||||||
|
#[inline]
|
||||||
|
fn read_exact(&mut self, size: usize) -> Option<Bytes> {
|
||||||
|
if size <= self.buf.len() {
|
||||||
|
Some(self.buf.split_to(size).freeze())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_max(&mut self, size: u64) -> Option<Bytes> {
|
||||||
|
if !self.buf.is_empty() {
|
||||||
|
let size = std::cmp::min(self.buf.len() as u64, size) as usize;
|
||||||
|
Some(self.buf.split_to(size).freeze())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read until specified ending
|
||||||
|
pub fn read_until(&mut self, line: &[u8]) -> Option<Bytes> {
|
||||||
|
twoway::find_bytes(&self.buf, line)
|
||||||
|
.map(|idx| self.buf.split_to(idx + line.len()).freeze())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read bytes until new line delimiter
|
||||||
|
pub fn readline(&mut self) -> Option<Bytes> {
|
||||||
|
self.read_until(b"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put unprocessed data back to the buffer
|
||||||
|
pub fn unprocessed(&mut self, data: Bytes) {
|
||||||
|
let buf = BytesMut::from(data);
|
||||||
|
let buf = std::mem::replace(&mut self.buf, buf);
|
||||||
|
self.buf.extend_from_slice(&buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use actix_http::h1::{Payload, PayloadWriter};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::unsync::mpsc;
|
use futures::unsync::mpsc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::header::{DispositionParam, DispositionType};
|
use actix_web::http::header::{DispositionParam, DispositionType};
|
||||||
use crate::test::run_on;
|
use actix_web::test::run_on;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_boundary() {
|
fn test_boundary() {
|
||||||
@ -799,9 +863,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut multipart = Multipart::new(&headers, payload);
|
let mut multipart = Multipart::new(&headers, payload);
|
||||||
match multipart.poll() {
|
match multipart.poll().unwrap() {
|
||||||
Ok(Async::Ready(Some(item))) => match item {
|
Async::Ready(Some(item)) => match item {
|
||||||
MultipartItem::Field(mut field) => {
|
Item::Field(mut field) => {
|
||||||
{
|
{
|
||||||
let cd = field.content_disposition().unwrap();
|
let cd = field.content_disposition().unwrap();
|
||||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||||
@ -813,12 +877,12 @@ mod tests {
|
|||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
match field.poll() {
|
match field.poll().unwrap() {
|
||||||
Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "test"),
|
Async::Ready(Some(chunk)) => assert_eq!(chunk, "test"),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
match field.poll() {
|
match field.poll().unwrap() {
|
||||||
Ok(Async::Ready(None)) => (),
|
Async::Ready(None) => (),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -827,9 +891,9 @@ mod tests {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
match multipart.poll() {
|
match multipart.poll().unwrap() {
|
||||||
Ok(Async::Ready(Some(item))) => match item {
|
Async::Ready(Some(item)) => match item {
|
||||||
MultipartItem::Field(mut field) => {
|
Item::Field(mut field) => {
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
@ -847,10 +911,110 @@ mod tests {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
match multipart.poll() {
|
match multipart.poll().unwrap() {
|
||||||
Ok(Async::Ready(None)) => (),
|
Async::Ready(None) => (),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
run_on(|| {
|
||||||
|
let (_, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
|
assert_eq!(payload.buf.len(), 0);
|
||||||
|
payload.poll_stream().unwrap();
|
||||||
|
assert_eq!(None, payload.read_max(1));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eof() {
|
||||||
|
run_on(|| {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
|
assert_eq!(None, payload.read_max(4));
|
||||||
|
sender.feed_data(Bytes::from("data"));
|
||||||
|
sender.feed_eof();
|
||||||
|
payload.poll_stream().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from("data")), payload.read_max(4));
|
||||||
|
assert_eq!(payload.buf.len(), 0);
|
||||||
|
assert_eq!(None, payload.read_max(1));
|
||||||
|
assert!(payload.eof);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_err() {
|
||||||
|
run_on(|| {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
assert_eq!(None, payload.read_max(1));
|
||||||
|
sender.set_error(PayloadError::Incomplete(None));
|
||||||
|
payload.poll_stream().err().unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readmax() {
|
||||||
|
run_on(|| {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
|
sender.feed_data(Bytes::from("line1"));
|
||||||
|
sender.feed_data(Bytes::from("line2"));
|
||||||
|
payload.poll_stream().unwrap();
|
||||||
|
assert_eq!(payload.buf.len(), 10);
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from("line1")), payload.read_max(5));
|
||||||
|
assert_eq!(payload.buf.len(), 5);
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from("line2")), payload.read_max(5));
|
||||||
|
assert_eq!(payload.buf.len(), 0);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readexactly() {
|
||||||
|
run_on(|| {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
|
assert_eq!(None, payload.read_exact(2));
|
||||||
|
|
||||||
|
sender.feed_data(Bytes::from("line1"));
|
||||||
|
sender.feed_data(Bytes::from("line2"));
|
||||||
|
payload.poll_stream().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2));
|
||||||
|
assert_eq!(payload.buf.len(), 8);
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4));
|
||||||
|
assert_eq!(payload.buf.len(), 4);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readuntil() {
|
||||||
|
run_on(|| {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
|
assert_eq!(None, payload.read_until(b"ne"));
|
||||||
|
|
||||||
|
sender.feed_data(Bytes::from("line1"));
|
||||||
|
sender.feed_data(Bytes::from("line2"));
|
||||||
|
payload.poll_stream().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from("line")), payload.read_until(b"ne"));
|
||||||
|
assert_eq!(payload.buf.len(), 6);
|
||||||
|
|
||||||
|
assert_eq!(Some(Bytes::from("1line2")), payload.read_until(b"2"));
|
||||||
|
assert_eq!(payload.buf.len(), 0);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
pub(crate) mod form;
|
pub(crate) mod form;
|
||||||
pub(crate) mod json;
|
pub(crate) mod json;
|
||||||
mod multipart;
|
|
||||||
mod path;
|
mod path;
|
||||||
pub(crate) mod payload;
|
pub(crate) mod payload;
|
||||||
mod query;
|
mod query;
|
||||||
@ -10,7 +9,6 @@ pub(crate) mod readlines;
|
|||||||
|
|
||||||
pub use self::form::{Form, FormConfig};
|
pub use self::form::{Form, FormConfig};
|
||||||
pub use self::json::{Json, JsonConfig};
|
pub use self::json::{Json, JsonConfig};
|
||||||
pub use self::multipart::{Multipart, MultipartField, MultipartItem};
|
|
||||||
pub use self::path::Path;
|
pub use self::path::Path;
|
||||||
pub use self::payload::{Payload, PayloadConfig};
|
pub use self::payload::{Payload, PayloadConfig};
|
||||||
pub use self::query::Query;
|
pub use self::query::Query;
|
||||||
|
Loading…
Reference in New Issue
Block a user