Middleware-based approach
This commit is contained in:
parent
95ef512a93
commit
0deb486d99
|
@ -9,12 +9,9 @@ readme = "README.md"
|
|||
keywords = ["actix", "form-data", "multipart", "async"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "form_data"
|
||||
|
||||
[dependencies]
|
||||
actix-http = "2.0.0-alpha.3"
|
||||
actix-multipart = "0.2.0"
|
||||
actix-multipart = "0.3.0-alpha.1"
|
||||
actix-rt = "1.1.1"
|
||||
actix-web = "3.0.0-alpha.2"
|
||||
bytes = "0.5.0"
|
||||
|
@ -22,10 +19,12 @@ futures = "0.3.4"
|
|||
log = "0.4.8"
|
||||
mime = "0.3.16"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "0.2.21", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-fs = { git = "https://git.asonix.dog/asonix/actix-fs" }
|
||||
anyhow = "1.0"
|
||||
env_logger = "0.7.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "0.2.21", features = ["fs"] }
|
||||
thiserror = "1.0"
|
||||
|
|
73
README.md
73
README.md
|
@ -14,8 +14,7 @@ Add it to your dependencies.
|
|||
|
||||
[dependencies]
|
||||
actix-web = "1.0.0"
|
||||
actix-multipart = "0.1.0"
|
||||
actix-form-data = "0.4.0"
|
||||
actix-form-data = "0.5.0-alpha.0"
|
||||
```
|
||||
|
||||
Require it in your project.
|
||||
|
@ -32,61 +31,51 @@ let form = Form::new().field("field-name", Field::text());
|
|||
```
|
||||
This creates a form with one required field named "field-name" that will be parsed as text.
|
||||
|
||||
Then, pass it to `handle_multipart` in your request handler.
|
||||
Then, pass it as a middleware.
|
||||
```rust
|
||||
fn request_handler(mp: Multipart, state: Data<State>) -> ... {
|
||||
let future = form_data::handle_multipart(mp, state.form);
|
||||
App::new()
|
||||
.wrap(form.clone())
|
||||
.service(resource("/upload").route(post().to(upload)))
|
||||
```
|
||||
|
||||
In your handler, get the value
|
||||
|
||||
```rust
|
||||
async fn upload(value: Value) -> {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This returns a `Future<Item = Value, Error = form_data::Error>`, which can be used to
|
||||
fetch your data.
|
||||
|
||||
And interact with it
|
||||
```rust
|
||||
let field_value = match value {
|
||||
Value::Map(mut hashmap) => {
|
||||
hashmap.remove("field-name")?
|
||||
hashmap.remove("field-name")?;
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
#### Example
|
||||
```rust
|
||||
/// examples/simple.rs
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_form_data::{Error, Field, Form, Value};
|
||||
use actix_web::{
|
||||
web::{post, resource, Data},
|
||||
web::{post, resource},
|
||||
App, HttpResponse, HttpServer,
|
||||
};
|
||||
use form_data::{handle_multipart, Error, Field, FilenameGenerator, Form};
|
||||
use futures::Future;
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
struct Gen;
|
||||
|
||||
impl FilenameGenerator for Gen {
|
||||
fn next_filename(&self, _: &mime::Mime) -> Option<PathBuf> {
|
||||
let mut p = PathBuf::new();
|
||||
p.push("examples/filename.png");
|
||||
Some(p)
|
||||
}
|
||||
async fn upload(uploaded_content: Value) -> HttpResponse {
|
||||
println!("Uploaded Content: {:#?}", uploaded_content);
|
||||
HttpResponse::Created().finish()
|
||||
}
|
||||
|
||||
fn upload((mp, state): (Multipart, Data<Form>)) -> Box<Future<Item = HttpResponse, Error = Error>> {
|
||||
Box::new(
|
||||
handle_multipart(mp, state.get_ref().clone()).map(|uploaded_content| {
|
||||
println!("Uploaded Content: {:?}", uploaded_content);
|
||||
HttpResponse::Created().finish()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
#[actix_rt::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let form = Form::new()
|
||||
.field("Hey", Field::text())
|
||||
.field(
|
||||
|
@ -96,21 +85,29 @@ fn main() -> Result<(), failure::Error> {
|
|||
.field("Two", Field::float())
|
||||
.finalize(),
|
||||
)
|
||||
.field("files", Field::array(Field::file(Gen)));
|
||||
.field(
|
||||
"files",
|
||||
Field::array(Field::file(|_, _, mut stream| async move {
|
||||
while let Some(res) = stream.next().await {
|
||||
res?;
|
||||
}
|
||||
Ok(()) as Result<(), Error>
|
||||
})),
|
||||
);
|
||||
|
||||
println!("{:?}", form);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(form.clone())
|
||||
.wrap(form.clone())
|
||||
.service(resource("/upload").route(post().to(upload)))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()?;
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
@ -118,7 +115,7 @@ Feel free to open issues for anything you find an issue with. Please note that a
|
|||
|
||||
### License
|
||||
|
||||
Copyright © 2018 Riley Trautman
|
||||
Copyright © 2020 Riley Trautman
|
||||
|
||||
Actix Form Data is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
3.7.0
|
|
@ -1,17 +1,13 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_form_data::{Error, Field, Form, Value};
|
||||
use actix_web::{
|
||||
web::{post, resource, Data},
|
||||
web::{post, resource},
|
||||
App, HttpResponse, HttpServer,
|
||||
};
|
||||
use form_data::{handle_multipart, Error, Field, Form};
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
async fn upload(mp: Multipart, state: Data<Form>) -> Result<HttpResponse, Error> {
|
||||
let uploaded_content = handle_multipart(mp, state.get_ref().clone()).await?;
|
||||
|
||||
println!("Uploaded Content: {:?}", uploaded_content);
|
||||
Ok(HttpResponse::Created().finish())
|
||||
async fn upload(uploaded_content: Value) -> HttpResponse {
|
||||
println!("Uploaded Content: {:#?}", uploaded_content);
|
||||
HttpResponse::Created().finish()
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
|
@ -25,13 +21,21 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
.field("Two", Field::float())
|
||||
.finalize(),
|
||||
)
|
||||
.field("files", Field::array(Field::file()));
|
||||
.field(
|
||||
"files",
|
||||
Field::array(Field::file(|_, _, mut stream| async move {
|
||||
while let Some(res) = stream.next().await {
|
||||
res?;
|
||||
}
|
||||
Ok(()) as Result<(), Error>
|
||||
})),
|
||||
);
|
||||
|
||||
println!("{:?}", form);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(form.clone())
|
||||
.wrap(form.clone())
|
||||
.service(resource("/upload").route(post().to(upload)))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
|
|
|
@ -1,68 +1,45 @@
|
|||
use std::{
|
||||
env,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_form_data::{Error, Field, Form, Value};
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
middleware::Logger,
|
||||
web::{post, resource, Data},
|
||||
web::{post, resource},
|
||||
App, HttpResponse, HttpServer, ResponseError,
|
||||
};
|
||||
use failure::Fail;
|
||||
use form_data::*;
|
||||
use futures::Future;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{Stream, TryStreamExt};
|
||||
use log::info;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
struct Gen(AtomicUsize);
|
||||
|
||||
impl Gen {
|
||||
pub fn new() -> Self {
|
||||
Gen(AtomicUsize::new(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl FilenameGenerator for Gen {
|
||||
fn next_filename(&self, _: &mime::Mime) -> Option<PathBuf> {
|
||||
let mut p = PathBuf::new();
|
||||
p.push("examples");
|
||||
p.push(&format!(
|
||||
"filename{}.png",
|
||||
self.0.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
use std::{
|
||||
env,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AppState {
|
||||
form: Form,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Fail, Serialize)]
|
||||
#[fail(display = "{}", msg)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct JsonError {
|
||||
msg: String,
|
||||
}
|
||||
|
||||
impl From<Error> for JsonError {
|
||||
fn from(e: Error) -> Self {
|
||||
impl<T> From<T> for JsonError
|
||||
where
|
||||
T: std::error::Error,
|
||||
{
|
||||
fn from(e: T) -> Self {
|
||||
JsonError {
|
||||
msg: format!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for JsonError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::BadRequest().json(Errors::from(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Fail, Serialize)]
|
||||
#[fail(display = "Errors occurred")]
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, thiserror::Error)]
|
||||
#[error("Errors occurred")]
|
||||
struct Errors {
|
||||
errors: Vec<JsonError>,
|
||||
}
|
||||
|
@ -74,30 +51,48 @@ impl From<JsonError> for Errors {
|
|||
}
|
||||
|
||||
impl ResponseError for Errors {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::BadRequest().json(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn upload(
|
||||
(mp, state): (Multipart, Data<AppState>),
|
||||
) -> Box<Future<Item = HttpResponse, Error = Errors>> {
|
||||
Box::new(
|
||||
handle_multipart(mp, state.form.clone())
|
||||
.map(|uploaded_content| {
|
||||
info!("Uploaded Content: {:?}", uploaded_content);
|
||||
HttpResponse::Created().finish()
|
||||
})
|
||||
.map_err(JsonError::from)
|
||||
.map_err(Errors::from),
|
||||
)
|
||||
async fn upload(uploaded_content: Value) -> HttpResponse {
|
||||
info!("Uploaded Content: {:#?}", uploaded_content);
|
||||
|
||||
HttpResponse::Created().finish()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
async fn save_file(
|
||||
stream: Pin<Box<dyn Stream<Item = Result<Bytes, Error>>>>,
|
||||
count: usize,
|
||||
) -> Result<(), JsonError> {
|
||||
let stream = stream.err_into::<JsonError>();
|
||||
let filename = format!("examples/filename{}.png", count);
|
||||
|
||||
info!("Creating {}", filename);
|
||||
let file = actix_fs::create(filename.clone()).await?;
|
||||
|
||||
info!("Writing to file");
|
||||
if let Err(e) = actix_fs::write_stream(file, stream).await {
|
||||
info!("Error writing, deleting file");
|
||||
actix_fs::remove(filename.clone()).await?;
|
||||
return Err(e);
|
||||
}
|
||||
info!("Written!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
env::set_var("RUST_LOG", "upload=info");
|
||||
env_logger::init();
|
||||
|
||||
let sys = actix::System::new("upload-test");
|
||||
let file_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let form = Form::new()
|
||||
.field("Hey", Field::text())
|
||||
|
@ -108,22 +103,25 @@ fn main() -> Result<(), failure::Error> {
|
|||
.field("Two", Field::float())
|
||||
.finalize(),
|
||||
)
|
||||
.field("files", Field::array(Field::file(Gen::new())));
|
||||
.field(
|
||||
"files",
|
||||
Field::array(Field::file(move |_filename, _content_type, stream| {
|
||||
let count = file_count.clone().fetch_add(1, Ordering::Relaxed);
|
||||
async move { save_file(stream, count).await.map_err(Errors::from) }
|
||||
})),
|
||||
);
|
||||
|
||||
info!("{:?}", form);
|
||||
|
||||
let state = AppState { form };
|
||||
info!("{:#?}", form);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(state.clone())
|
||||
.wrap(form.clone())
|
||||
.wrap(Logger::default())
|
||||
.service(resource("/upload").route(post().to(upload)))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.start();
|
||||
|
||||
sys.run()?;
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
22
src/error.rs
22
src/error.rs
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* This file is part of Actix Form Data.
|
||||
*
|
||||
* Copyright © 2018 Riley Trautman
|
||||
* Copyright © 2020 Riley Trautman
|
||||
*
|
||||
* Actix Form Data is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,11 +25,14 @@ use std::{
|
|||
use actix_multipart::MultipartError;
|
||||
use actix_web::{
|
||||
error::{PayloadError, ResponseError},
|
||||
http::StatusCode,
|
||||
HttpResponse,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Error in file function, {0}")]
|
||||
FileFn(#[from] actix_web::Error),
|
||||
#[error("Error parsing payload, {0}")]
|
||||
Payload(#[from] PayloadError),
|
||||
#[error("Error in multipart creation, {0}")]
|
||||
|
@ -58,6 +61,10 @@ pub enum Error {
|
|||
FileCount,
|
||||
#[error("File too large")]
|
||||
FileSize,
|
||||
#[error("Uploaded guard used without Multipart middleware")]
|
||||
MissingMiddleware,
|
||||
#[error("Impossible Error! Middleware exists, didn't fail, and didn't send value")]
|
||||
TxDropped,
|
||||
}
|
||||
|
||||
impl From<MultipartError> for Error {
|
||||
|
@ -67,8 +74,18 @@ impl From<MultipartError> for Error {
|
|||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
Error::FileFn(ref e) => ResponseError::status_code(e.as_response_error()),
|
||||
Error::Payload(ref e) => ResponseError::status_code(e),
|
||||
Error::MissingMiddleware | Error::TxDropped => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
Error::FileFn(ref e) => ResponseError::error_response(e.as_response_error()),
|
||||
Error::Payload(ref e) => ResponseError::error_response(e),
|
||||
Error::Multipart(_)
|
||||
| Error::ParseField(_)
|
||||
|
@ -83,6 +100,9 @@ impl ResponseError for Error {
|
|||
| Error::Filename
|
||||
| Error::FileCount
|
||||
| Error::FileSize => HttpResponse::BadRequest().finish(),
|
||||
Error::MissingMiddleware | Error::TxDropped => {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
46
src/lib.rs
46
src/lib.rs
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* This file is part of Actix Form Data.
|
||||
*
|
||||
* Copyright © 2018 Riley Trautman
|
||||
* Copyright © 2020 Riley Trautman
|
||||
*
|
||||
* Actix Form Data is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,25 +25,20 @@
|
|||
//! # Example
|
||||
//!
|
||||
//!```rust
|
||||
//! use actix_multipart::Multipart;
|
||||
//! use actix_form_data::{Error, Field, Form, Value};
|
||||
//! use actix_web::{
|
||||
//! web::{post, resource, Data},
|
||||
//! web::{post, resource},
|
||||
//! App, HttpResponse, HttpServer,
|
||||
//! };
|
||||
//! use form_data::{handle_multipart, Error, Field, FilenameGenerator, Form};
|
||||
//! use futures::Future;
|
||||
//! use futures::stream::StreamExt;
|
||||
//!
|
||||
//!
|
||||
//! fn upload((mp, state): (Multipart, Data<Form>)) -> Box<Future<Item = HttpResponse, Error = Error>> {
|
||||
//! Box::new(
|
||||
//! handle_multipart(mp, state.get_ref().clone()).map(|uploaded_content| {
|
||||
//! println!("Uploaded Content: {:?}", uploaded_content);
|
||||
//! HttpResponse::Created().finish()
|
||||
//! }),
|
||||
//! )
|
||||
//! async fn upload(uploaded_content: Value) -> HttpResponse {
|
||||
//! println!("Uploaded Content: {:#?}", uploaded_content);
|
||||
//! HttpResponse::Created().finish()
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), failure::Error> {
|
||||
//! #[actix_rt::main]
|
||||
//! async fn main() -> Result<(), anyhow::Error> {
|
||||
//! let form = Form::new()
|
||||
//! .field("Hey", Field::text())
|
||||
//! .field(
|
||||
|
@ -53,24 +48,39 @@
|
|||
//! .field("Two", Field::float())
|
||||
//! .finalize(),
|
||||
//! )
|
||||
//! .field("files", Field::array(Field::file(Gen)));
|
||||
//! .field(
|
||||
//! "files",
|
||||
//! Field::array(Field::file(|_, _, mut stream| async move {
|
||||
//! while let Some(res) = stream.next().await {
|
||||
//! res?;
|
||||
//! }
|
||||
//! Ok(()) as Result<(), Error>
|
||||
//! })),
|
||||
//! );
|
||||
//!
|
||||
//! println!("{:?}", form);
|
||||
//!
|
||||
//! HttpServer::new(move || {
|
||||
//! App::new()
|
||||
//! .data(form.clone())
|
||||
//! .wrap(form.clone())
|
||||
//! .service(resource("/upload").route(post().to(upload)))
|
||||
//! })
|
||||
//! .bind("127.0.0.1:8080")?;
|
||||
//! // .run()?;
|
||||
//! // commented out to prevent infinite doctest
|
||||
//! // .run()
|
||||
//! // .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!```
|
||||
|
||||
mod error;
|
||||
mod middleware;
|
||||
mod types;
|
||||
mod upload;
|
||||
|
||||
pub use self::{error::Error, types::*, upload::handle_multipart};
|
||||
pub use self::{
|
||||
error::Error,
|
||||
types::{Field, FileMeta, Form, Value},
|
||||
upload::handle_multipart,
|
||||
};
|
||||
|
|
109
src/middleware.rs
Normal file
109
src/middleware.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* This file is part of Actix Form Data.
|
||||
*
|
||||
* Copyright © 2020 Riley Trautman
|
||||
*
|
||||
* Actix Form Data is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Actix Form Data is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Actix Form Data. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
types::{Form, Value},
|
||||
upload::handle_multipart,
|
||||
};
|
||||
use actix_web::{
|
||||
dev::{Payload, Service, ServiceRequest, Transform},
|
||||
FromRequest, HttpMessage, HttpRequest,
|
||||
};
|
||||
use futures::future::{ok, Ready};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::sync::oneshot::{channel, Receiver};
|
||||
|
||||
struct Uploaded {
|
||||
rx: Receiver<Value>,
|
||||
}
|
||||
|
||||
pub struct MultipartMiddleware<S> {
|
||||
form: Form,
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl FromRequest for Value {
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let opt = req.extensions_mut().remove::<Uploaded>();
|
||||
Box::pin(async move {
|
||||
let fut = opt.ok_or(Error::MissingMiddleware)?;
|
||||
|
||||
fut.rx.await.map_err(|_| Error::TxDropped)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Transform<S> for Form
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type InitError = ();
|
||||
type Transform = MultipartMiddleware<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(MultipartMiddleware {
|
||||
form: self.clone(),
|
||||
service,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Service for MultipartMiddleware<S>
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>>>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, mut req: S::Request) -> Self::Future {
|
||||
let (tx, rx) = channel();
|
||||
req.extensions_mut().insert(Uploaded { rx });
|
||||
let payload = req.take_payload();
|
||||
let multipart = actix_multipart::Multipart::new(req.headers(), payload);
|
||||
let form = self.form.clone();
|
||||
let fut = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let uploaded = handle_multipart(multipart, form).await?;
|
||||
let _ = tx.send(uploaded);
|
||||
fut.await
|
||||
})
|
||||
}
|
||||
}
|
82
src/types.rs
82
src/types.rs
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* This file is part of Actix Form Data.
|
||||
*
|
||||
* Copyright © 2018 Riley Trautman
|
||||
* Copyright © 2020 Riley Trautman
|
||||
*
|
||||
* Actix Form Data is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,22 +25,15 @@ use mime::Mime;
|
|||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub struct FileStream {
|
||||
#[derive(Debug)]
|
||||
pub struct FileMeta {
|
||||
pub filename: String,
|
||||
pub content_type: Mime,
|
||||
pub stream: Pin<Box<dyn Stream<Item = Result<Bytes, Error>>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FileStream {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("FileStream")
|
||||
.field("filename", &self.filename)
|
||||
.field("content_type", &self.content_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a succesfull parse through a given multipart stream.
|
||||
|
@ -72,7 +65,7 @@ impl fmt::Debug for FileStream {
|
|||
pub enum Value {
|
||||
Map(HashMap<String, Value>),
|
||||
Array(Vec<Value>),
|
||||
File(FileStream),
|
||||
File(FileMeta),
|
||||
Bytes(Bytes),
|
||||
Text(String),
|
||||
Int(i64),
|
||||
|
@ -118,9 +111,9 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn file(self) -> Option<FileStream> {
|
||||
pub fn file(self) -> Option<FileMeta> {
|
||||
match self {
|
||||
Value::File(file_stream) => Some(file_stream),
|
||||
Value::File(file_meta) => Some(file_meta),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +150,7 @@ impl Value {
|
|||
impl From<MultipartContent> for Value {
|
||||
fn from(mc: MultipartContent) -> Self {
|
||||
match mc {
|
||||
MultipartContent::File(file_stream) => Value::File(file_stream),
|
||||
MultipartContent::File(file_meta) => Value::File(file_meta),
|
||||
MultipartContent::Bytes(bytes) => Value::Bytes(bytes),
|
||||
MultipartContent::Text(string) => Value::Text(string),
|
||||
MultipartContent::Int(i) => Value::Int(i),
|
||||
|
@ -166,12 +159,22 @@ impl From<MultipartContent> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub type FileFn = Arc<
|
||||
dyn Fn(
|
||||
String,
|
||||
Mime,
|
||||
Pin<Box<dyn Stream<Item = Result<Bytes, Error>>>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), actix_web::Error>>>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
/// The field type represents a field in the form-data that is allowed to be parsed.
|
||||
#[derive(Clone)]
|
||||
pub enum Field {
|
||||
Array(Array),
|
||||
Map(Map),
|
||||
File,
|
||||
File(FileFn),
|
||||
Bytes,
|
||||
Int,
|
||||
Float,
|
||||
|
@ -181,9 +184,9 @@ pub enum Field {
|
|||
impl fmt::Debug for Field {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Field::Array(ref arr) => write!(f, "Array({:?})", arr),
|
||||
Field::Map(ref map) => write!(f, "Map({:?})", map),
|
||||
Field::File => write!(f, "File"),
|
||||
Field::Array(ref arr) => f.debug_tuple("Array").field(arr).finish(),
|
||||
Field::Map(ref map) => f.debug_tuple("Map").field(map).finish(),
|
||||
Field::File(_) => write!(f, "File"),
|
||||
Field::Bytes => write!(f, "Bytes"),
|
||||
Field::Int => write!(f, "Int"),
|
||||
Field::Float => write!(f, "Float"),
|
||||
|
@ -202,11 +205,32 @@ impl Field {
|
|||
/// # Example
|
||||
/// ```rust
|
||||
/// # use form_data::{Form, Field};
|
||||
/// # use tokio::sync::mpsc::channel;
|
||||
/// #
|
||||
/// let form = Form::new().field("file-field", Field::file());
|
||||
/// let (tx, rx) = channel(1);
|
||||
/// let form = Form::new().field("file-field", Field::file(|_, _, stream| async move {
|
||||
/// while let Some(res) = stream.next().await {
|
||||
/// if let Err(_) = tx.send(res).await {
|
||||
/// break;
|
||||
/// }
|
||||
/// }
|
||||
/// Ok(()) as Result<(), std::convert::Infallible>
|
||||
/// }));
|
||||
/// ```
|
||||
pub fn file() -> Self {
|
||||
Field::File
|
||||
pub fn file<F, Fut, E>(f: F) -> Self
|
||||
where
|
||||
F: Fn(String, Mime, Pin<Box<dyn Stream<Item = Result<Bytes, Error>>>>) -> Fut
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ 'static,
|
||||
Fut: Future<Output = Result<(), E>> + 'static,
|
||||
E: actix_web::ResponseError + 'static,
|
||||
{
|
||||
Field::File(Arc::new(move |filename, mime, stream| {
|
||||
let f = f.clone();
|
||||
Box::pin(async move { (f)(filename, mime, stream).await.map_err(|e| e.into()) })
|
||||
}))
|
||||
}
|
||||
|
||||
/// Add a Bytes field to a form
|
||||
|
@ -295,9 +319,9 @@ impl Field {
|
|||
match *self {
|
||||
Field::Array(ref arr) => arr.valid_field(name),
|
||||
Field::Map(ref map) => map.valid_field(name),
|
||||
Field::File => {
|
||||
Field::File(ref file_fn) => {
|
||||
if name.is_empty() {
|
||||
Some(FieldTerminator::File)
|
||||
Some(FieldTerminator::File(file_fn.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -512,7 +536,7 @@ impl Form {
|
|||
|
||||
impl fmt::Debug for Form {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Form({:?})", self.inner)
|
||||
f.debug_struct("Form").field("inner", &self.inner).finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -548,7 +572,7 @@ impl NamePart {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum FieldTerminator {
|
||||
File,
|
||||
File(FileFn),
|
||||
Bytes,
|
||||
Int,
|
||||
Float,
|
||||
|
@ -558,7 +582,7 @@ pub(crate) enum FieldTerminator {
|
|||
impl fmt::Debug for FieldTerminator {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
FieldTerminator::File => write!(f, "File"),
|
||||
FieldTerminator::File(_) => write!(f, "File"),
|
||||
FieldTerminator::Bytes => write!(f, "Bytes"),
|
||||
FieldTerminator::Int => write!(f, "Int"),
|
||||
FieldTerminator::Float => write!(f, "Float"),
|
||||
|
@ -572,7 +596,7 @@ pub(crate) type MultipartForm = Vec<MultipartHash>;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum MultipartContent {
|
||||
File(FileStream),
|
||||
File(FileMeta),
|
||||
Bytes(Bytes),
|
||||
Text(String),
|
||||
Int(i64),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* This file is part of Actix Form Data.
|
||||
*
|
||||
* Copyright © 2018 Riley Trautman
|
||||
* Copyright © 2020 Riley Trautman
|
||||
*
|
||||
* Actix Form Data is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,8 +20,8 @@
|
|||
use crate::{
|
||||
error::Error,
|
||||
types::{
|
||||
ContentDisposition, FieldTerminator, FileStream, Form, MultipartContent, MultipartForm,
|
||||
MultipartHash, NamePart, Value,
|
||||
ContentDisposition, FieldTerminator, FileFn, FileMeta, Form, MultipartContent,
|
||||
MultipartForm, MultipartHash, NamePart, Value,
|
||||
},
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
|
@ -100,6 +100,7 @@ async fn handle_file_upload(
|
|||
field: actix_multipart::Field,
|
||||
filename: Option<String>,
|
||||
form: Form,
|
||||
file_fn: FileFn,
|
||||
) -> Result<MultipartContent, Error> {
|
||||
let filename = filename.ok_or(Error::Filename)?;
|
||||
let path: &Path = filename.as_ref();
|
||||
|
@ -112,10 +113,10 @@ async fn handle_file_upload(
|
|||
|
||||
let content_type = field.content_type().clone();
|
||||
|
||||
Ok(MultipartContent::File(FileStream {
|
||||
filename,
|
||||
content_type,
|
||||
stream: Box::pin(field.then(move |res| {
|
||||
file_fn(
|
||||
filename.clone(),
|
||||
content_type.clone(),
|
||||
Box::pin(field.then(move |res| {
|
||||
let form = form.clone();
|
||||
let file_size = file_size.clone();
|
||||
async move {
|
||||
|
@ -133,6 +134,12 @@ async fn handle_file_upload(
|
|||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(MultipartContent::File(FileMeta {
|
||||
filename,
|
||||
content_type,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -160,7 +167,7 @@ async fn handle_form_data(
|
|||
let s = String::from_utf8(bytes.to_vec()).map_err(Error::ParseField)?;
|
||||
|
||||
match term {
|
||||
FieldTerminator::Bytes | FieldTerminator::File => {
|
||||
FieldTerminator::Bytes | FieldTerminator::File(_) => {
|
||||
return Err(Error::FieldType);
|
||||
}
|
||||
FieldTerminator::Text => Ok(MultipartContent::Text(s)),
|
||||
|
@ -189,8 +196,8 @@ async fn handle_stream_field(
|
|||
.ok_or(Error::FieldType)?;
|
||||
|
||||
let content = match term {
|
||||
FieldTerminator::File => {
|
||||
handle_file_upload(field, content_disposition.filename, form).await?
|
||||
FieldTerminator::File(file_fn) => {
|
||||
handle_file_upload(field, content_disposition.filename, form, file_fn).await?
|
||||
}
|
||||
term => handle_form_data(field, term, form.clone()).await?,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue