Impl OptionalFromRequest on MultipartFile and change behaviour on MultipartFiles to contain 0 files
This commit is contained in:
1571
Cargo.lock
generated
1571
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ exclude = ["examples"]
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.89"
|
||||||
authors = ["Martin Berg Alstad"]
|
authors = ["Martin Berg Alstad"]
|
||||||
homepage = "martials.no"
|
homepage = "martials.no"
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ derive_more = { workspace = true, features = ["from", "constructor"] }
|
|||||||
tokio = "1.40"
|
tokio = "1.40"
|
||||||
# Database
|
# Database
|
||||||
diesel = "2.2"
|
diesel = "2.2"
|
||||||
diesel-async = "0.5"
|
diesel-async = "0.6"
|
||||||
diesel_migrations = "2.2"
|
diesel_migrations = "2.2"
|
||||||
deadpool-diesel = "0.6"
|
deadpool-diesel = "0.6"
|
||||||
# Error handling
|
# Error handling
|
||||||
@ -71,13 +71,13 @@ quote = "1.0"
|
|||||||
deluxe = "0.5"
|
deluxe = "0.5"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
# Test
|
# Test
|
||||||
testcontainers-modules = "0.11"
|
testcontainers-modules = "0.13"
|
||||||
# Utils
|
# Utils
|
||||||
derive_more = "2.0"
|
derive_more = "2.0"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
axum = ["dep:axum", "dep:tower", "dep:tower-http", "dep:thiserror", "dep:tracing", "dep:tracing-subscriber", "dep:tokio", "dep:mime"]
|
axum = ["dep:axum", "dep:tower", "dep:serde", "dep:tower-http", "dep:thiserror", "dep:tracing", "dep:tracing-subscriber", "dep:tokio", "dep:mime"]
|
||||||
diesel = ["dep:diesel-crud-trait", "dep:diesel", "dep:diesel-async", "dep:deadpool-diesel", "dep:diesel_migrations"]
|
diesel = ["dep:diesel-crud-trait", "dep:diesel", "dep:diesel-async", "dep:deadpool-diesel", "dep:diesel_migrations"]
|
||||||
io = ["dep:tokio", "dep:tokio-util"]
|
io = ["dep:tokio", "dep:tokio-util"]
|
||||||
iter = []
|
iter = []
|
||||||
|
94
examples/multipart_file/Cargo.lock
generated
94
examples/multipart_file/Cargo.lock
generated
@ -17,58 +17,13 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-trait"
|
|
||||||
version = "0.1.80"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum"
|
|
||||||
version = "0.7.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core 0.4.3",
|
|
||||||
"bytes",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper",
|
|
||||||
"hyper-util",
|
|
||||||
"itoa",
|
|
||||||
"matchit 0.7.3",
|
|
||||||
"memchr",
|
|
||||||
"mime",
|
|
||||||
"percent-encoding",
|
|
||||||
"pin-project-lite",
|
|
||||||
"rustversion",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_path_to_error",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"sync_wrapper 1.0.1",
|
|
||||||
"tokio",
|
|
||||||
"tower 0.4.13",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core 0.5.2",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -78,7 +33,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
"matchit 0.8.4",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
"multer",
|
"multer",
|
||||||
@ -89,7 +44,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
@ -97,27 +52,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-core"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"bytes",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"mime",
|
|
||||||
"pin-project-lite",
|
|
||||||
"rustversion",
|
|
||||||
"sync_wrapper 0.1.2",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -132,7 +66,7 @@ dependencies = [
|
|||||||
"mime",
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@ -375,9 +309,10 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
name = "lib"
|
name = "lib"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.8.4",
|
"axum",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"mime",
|
"mime",
|
||||||
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
@ -398,12 +333,6 @@ version = "0.4.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "matchit"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@ -464,7 +393,7 @@ dependencies = [
|
|||||||
name = "multipart_file"
|
name = "multipart_file"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.7.5",
|
"axum",
|
||||||
"lib",
|
"lib",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -675,12 +604,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sync_wrapper"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -758,7 +681,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -770,7 +692,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "multipart_file"
|
name = "multipart_file"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lib = { path = "../..", features = ["axum"] }
|
lib = { path = "../..", features = ["axum"] }
|
||||||
axum = "0.7.5"
|
axum = "0.8"
|
||||||
tokio = { version = "1.40", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.47", features = ["rt-multi-thread", "macros"] }
|
||||||
|
@ -1,22 +1,10 @@
|
|||||||
use axum::extract::DefaultBodyLimit;
|
use axum::extract::DefaultBodyLimit;
|
||||||
use lib::axum::app::AppBuilder;
|
use lib::axum::app::AppBuilder;
|
||||||
use lib::axum::extractor::MultipartFiles;
|
use lib::axum::extractor::{MultipartFile, MultipartFiles};
|
||||||
use lib::routes;
|
use lib::routes;
|
||||||
|
|
||||||
// 0 or more
|
// 0 or more files
|
||||||
async fn with_optional_file(files: Option<MultipartFiles>) -> String {
|
async fn several_files(MultipartFiles(files): MultipartFiles) -> String {
|
||||||
format!(
|
|
||||||
"{:?}",
|
|
||||||
files.map(|files| files
|
|
||||||
.0
|
|
||||||
.into_iter()
|
|
||||||
.map(|file| file.filename)
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 or more files
|
|
||||||
async fn handler(MultipartFiles(files): MultipartFiles) -> String {
|
|
||||||
format!(
|
format!(
|
||||||
"{:?} uploaded",
|
"{:?} uploaded",
|
||||||
files
|
files
|
||||||
@ -26,11 +14,26 @@ async fn handler(MultipartFiles(files): MultipartFiles) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1 file exactly
|
||||||
|
async fn single_file(MultipartFile(file): MultipartFile) -> String {
|
||||||
|
format!("{:?} uploaded", file.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 or 1 file
|
||||||
|
async fn optional_single_file(file: Option<MultipartFile>) -> String {
|
||||||
|
format!(
|
||||||
|
"{:?} uploaded",
|
||||||
|
file.map(|file| file.0.filename)
|
||||||
|
.unwrap_or(String::from("No file found"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let route = routes!(
|
let route = routes!(
|
||||||
get "/" => handler,
|
get "/" => several_files,
|
||||||
get "/opt" => with_optional_file
|
get "/file" => single_file,
|
||||||
|
get "/opt/file" => optional_single_file
|
||||||
)
|
)
|
||||||
.layer(DefaultBodyLimit::disable());
|
.layer(DefaultBodyLimit::disable());
|
||||||
AppBuilder::new().route(route).serve().await.unwrap();
|
AppBuilder::new().route(route).serve().await.unwrap();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use axum::{
|
use axum::extract::FromRequest;
|
||||||
extract::{
|
use axum::extract::Multipart;
|
||||||
FromRequest, Multipart, Request,
|
use axum::extract::OptionalFromRequest;
|
||||||
multipart::{Field, MultipartError, MultipartRejection},
|
use axum::extract::Request;
|
||||||
},
|
use axum::extract::multipart::Field;
|
||||||
response::IntoResponse,
|
use axum::extract::multipart::MultipartError;
|
||||||
};
|
use axum::extract::multipart::MultipartRejection;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -140,6 +141,40 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S> OptionalFromRequest<S> for MultipartFile
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = MultipartFileRejection;
|
||||||
|
|
||||||
|
/// Extracts a single file from a multipart request.
|
||||||
|
/// Expects exactly one file. A file must have a name, bytes and optionally a content type.
|
||||||
|
/// This extractor consumes the request and must ble placed last in the handler.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use std::io::Read;
|
||||||
|
/// use axum::response::Html;
|
||||||
|
/// use lib::axum::extractor::{MultipartFile, MultipartFiles};
|
||||||
|
///
|
||||||
|
/// async fn upload_file(opt_file: Option<MultipartFile>) -> Html<String> {
|
||||||
|
/// Html(opt_file
|
||||||
|
/// .map(|MultipartFile(file)| String::from_utf8(file.bytes).unwrap())
|
||||||
|
/// .unwrap_or_else(|| String::from("<p>Not Found</p>"))
|
||||||
|
/// )
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
async fn from_request(req: Request, state: &S) -> Result<Option<Self>, Self::Rejection> {
|
||||||
|
let multipart = Multipart::from_request(req, state).await?;
|
||||||
|
let files = get_files(multipart).await?;
|
||||||
|
if files.len() > 1 {
|
||||||
|
Err(MultipartFileRejection::SeveralFiles)
|
||||||
|
} else {
|
||||||
|
let file = files.first().ok_or(MultipartFileRejection::NoFiles)?;
|
||||||
|
Ok(Some(MultipartFile(file.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<S> FromRequest<S> for MultipartFiles
|
impl<S> FromRequest<S> for MultipartFiles
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
@ -147,7 +182,7 @@ where
|
|||||||
type Rejection = MultipartFileRejection;
|
type Rejection = MultipartFileRejection;
|
||||||
|
|
||||||
/// Extracts multiple files from a multipart request.
|
/// Extracts multiple files from a multipart request.
|
||||||
/// Expects at least one file. A file must have a name, bytes and optionally a content type.
|
/// Can contain 0 files. A file must have a name, bytes and optionally a content type.
|
||||||
/// This extractor consumes the request and must ble placed last in the handler.
|
/// This extractor consumes the request and must ble placed last in the handler.
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
@ -167,12 +202,8 @@ where
|
|||||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let multipart = Multipart::from_request(req, state).await?;
|
let multipart = Multipart::from_request(req, state).await?;
|
||||||
let files = get_files(multipart).await?;
|
let files = get_files(multipart).await?;
|
||||||
if files.is_empty() {
|
|
||||||
Err(MultipartFileRejection::NoFiles)
|
|
||||||
} else {
|
|
||||||
Ok(MultipartFiles(files))
|
Ok(MultipartFiles(files))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_files(mut multipart: Multipart) -> Result<Vec<File>, MultipartFileRejection> {
|
async fn get_files(mut multipart: Multipart) -> Result<Vec<File>, MultipartFileRejection> {
|
||||||
|
Reference in New Issue
Block a user