rename Tempfile -> TempFile

This commit is contained in:
Rob Ede 2023-02-26 02:36:43 +00:00
parent 8578dadbb2
commit facc43e5ad
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
6 changed files with 86 additions and 79 deletions

View File

@ -3,13 +3,12 @@
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
- Added `MultipartForm` typed data extractor. [#2883] - Added `MultipartForm` typed data extractor. [#2883]
[#2880]: https://github.com/actix/actix-web/pull/2880
[#2883]: https://github.com/actix/actix-web/pull/2883 [#2883]: https://github.com/actix/actix-web/pull/2883
## 0.5.0 - 2023-01-21 ## 0.5.0 - 2023-01-21
- `Field::content_type()` now returns `Option<&mime::Mime>`. [#2885]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2885]
[#2885]: https://github.com/actix/actix-web/pull/2885 [#2885]: https://github.com/actix/actix-web/pull/2885

View File

@ -1,7 +1,10 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.5.0" version = "0.5.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>",
]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -30,6 +33,7 @@ actix-web = { version = "4", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
httparse = "1.3" httparse = "1.3"
local-waker = "0.1" local-waker = "0.1"
log = "0.4" log = "0.4"
@ -39,15 +43,15 @@ serde = "1"
serde_json = "1" serde_json = "1"
serde_plain = "1" serde_plain = "1"
# TODO(MSRV 1.60): replace with dep: prefix # TODO(MSRV 1.60): replace with dep: prefix
tempfile-dep = { package = "tempfile", version = "3.3.0", optional = true } tempfile-dep = { package = "tempfile", version = "3.4", optional = true }
tokio = { version = "1.13.1", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"
actix-multipart-rfc7578 = "0.10" actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0" actix-test = "0.1"
awc = "3.0.1" awc = "3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
tokio = { version = "1.18.5", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -33,7 +33,7 @@ impl<'t> FieldReader<'t> for Bytes {
limits: &'t mut Limits, limits: &'t mut Limits,
) -> Self::Future { ) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let mut buf = BytesMut::new(); let mut buf = BytesMut::with_capacity(131_072);
while let Some(chunk) = field.try_next().await? { while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), true)?; limits.try_consume_limits(chunk.len(), true)?;

View File

@ -10,7 +10,7 @@ use std::{
use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest}; use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use futures_util::{TryFutureExt, TryStreamExt as _}; use futures_util::{TryFutureExt as _, TryStreamExt as _};
use crate::{Field, Multipart, MultipartError}; use crate::{Field, Multipart, MultipartError};
@ -80,7 +80,7 @@ where
DuplicateField::Deny => { DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField( return Box::pin(ready(Err(MultipartError::DuplicateField(
field.name().to_string(), field.name().to_owned(),
)))) ))))
} }
@ -89,7 +89,7 @@ where
} }
Box::pin(async move { Box::pin(async move {
let field_name = field.name().to_string(); let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?; let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t)); state.insert(field_name, Box::new(t));
Ok(()) Ok(())
@ -117,11 +117,11 @@ where
Box::pin(async move { Box::pin(async move {
// Note: Vec GroupReader always allows duplicates // Note: Vec GroupReader always allows duplicates
let field_name = field.name().to_string(); let field_name = field.name().to_owned();
let vec = state let vec = state
.entry(field_name) .entry(field_name)
.or_insert_with(|| Box::new(Vec::<T>::new())) .or_insert_with(|| Box::<Vec<T>>::default())
.downcast_mut::<Vec<T>>() .downcast_mut::<Vec<T>>()
.unwrap(); .unwrap();
@ -159,15 +159,16 @@ where
DuplicateField::Deny => { DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField( return Box::pin(ready(Err(MultipartError::DuplicateField(
field.name().to_string(), field.name().to_owned(),
)))) ))))
} }
DuplicateField::Replace => {} DuplicateField::Replace => {}
} }
} }
Box::pin(async move { Box::pin(async move {
let field_name = field.name().to_string(); let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?; let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t)); state.insert(field_name, Box::new(t));
Ok(()) Ok(())
@ -182,8 +183,9 @@ where
} }
} }
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. You should use /// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor.
/// the [`macro@MultipartForm`] to implement this for your struct. ///
/// You should use the [`macro@MultipartForm`] macro to derive this for your struct.
pub trait MultipartCollect: Sized { pub trait MultipartCollect: Sized {
/// An optional limit in bytes to be applied a given field name. Note this limit will be shared /// An optional limit in bytes to be applied a given field name. Note this limit will be shared
/// across all fields sharing the same name. /// across all fields sharing the same name.
@ -205,13 +207,13 @@ pub trait MultipartCollect: Sized {
#[doc(hidden)] #[doc(hidden)]
pub enum DuplicateField { pub enum DuplicateField {
/// Additional fields are not processed /// Additional fields are not processed.
Ignore, Ignore,
/// An error will be raised /// An error will be raised.
Deny, Deny,
/// All fields will be processed, the last one will replace all previous /// All fields will be processed, the last one will replace all previous.
Replace, Replace,
} }
@ -270,10 +272,10 @@ impl Limits {
/// Typed `multipart/form-data` extractor. /// Typed `multipart/form-data` extractor.
/// ///
/// To extract typed data from a multipart stream, the inner type `T` must implement the /// To extract typed data from a multipart stream, the inner type `T` must implement the
/// [`MultipartCollect`] trait, you should use the [`macro@MultipartForm`] macro to derive this /// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this
/// for your struct. /// for your struct.
/// ///
/// Use [`MultipartFormConfig`] to configure extraction options. /// Add a [`MultipartFormConfig`] to your app data to configure extraction.
#[derive(Deref, DerefMut)] #[derive(Deref, DerefMut)]
pub struct MultipartForm<T: MultipartCollect>(pub T); pub struct MultipartForm<T: MultipartCollect>(pub T);
@ -338,6 +340,8 @@ type MultipartFormErrorHandler =
Option<Arc<dyn Fn(MultipartError, &HttpRequest) -> Error + Send + Sync>>; Option<Arc<dyn Fn(MultipartError, &HttpRequest) -> Error + Send + Sync>>;
/// [`struct@MultipartForm`] extractor configuration. /// [`struct@MultipartForm`] extractor configuration.
///
/// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors.
#[derive(Clone)] #[derive(Clone)]
pub struct MultipartFormConfig { pub struct MultipartFormConfig {
total_limit: usize, total_limit: usize,
@ -346,19 +350,19 @@ pub struct MultipartFormConfig {
} }
impl MultipartFormConfig { impl MultipartFormConfig {
/// Set maximum accepted payload size for the entire form. By default this limit is 50MiB. /// Sets maximum accepted payload size for the entire form. By default this limit is 50MiB.
pub fn total_limit(mut self, total_limit: usize) -> Self { pub fn total_limit(mut self, total_limit: usize) -> Self {
self.total_limit = total_limit; self.total_limit = total_limit;
self self
} }
/// Set maximum accepted data that will be read into memory. By default this limit is 2MiB. /// Sets maximum accepted data that will be read into memory. By default this limit is 2MiB.
pub fn memory_limit(mut self, memory_limit: usize) -> Self { pub fn memory_limit(mut self, memory_limit: usize) -> Self {
self.memory_limit = memory_limit; self.memory_limit = memory_limit;
self self
} }
/// Set custom error handler. /// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self pub fn error_handler<F>(mut self, f: F) -> Self
where where
F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static, F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static,
@ -367,7 +371,7 @@ impl MultipartFormConfig {
self self
} }
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall /// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config. /// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self { fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>() req.app_data::<Self>()
@ -384,7 +388,7 @@ const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig {
impl Default for MultipartFormConfig { impl Default for MultipartFormConfig {
fn default() -> Self { fn default() -> Self {
DEFAULT_CONFIG.clone() DEFAULT_CONFIG
} }
} }
@ -397,7 +401,7 @@ mod tests {
use awc::{Client, ClientResponse}; use awc::{Client, ClientResponse};
use super::MultipartForm; use super::MultipartForm;
use crate::form::{bytes::Bytes, tempfile::Tempfile, text::Text, MultipartFormConfig}; use crate::form::{bytes::Bytes, tempfile::TempFile, text::Text, MultipartFormConfig};
pub async fn send_form( pub async fn send_form(
srv: &TestServer, srv: &TestServer,
@ -611,7 +615,7 @@ mod tests {
#[derive(MultipartForm)] #[derive(MultipartForm)]
struct TestFileUploadLimits { struct TestFileUploadLimits {
field: Tempfile, field: TempFile,
} }
async fn test_upload_limits_memory( async fn test_upload_limits_memory(

View File

@ -16,13 +16,13 @@ use tokio::io::AsyncWriteExt;
use super::FieldErrorHandler; use super::FieldErrorHandler;
use crate::{ use crate::{
form::{tempfile::TempfileError::FileIo, FieldReader, Limits}, form::{FieldReader, Limits},
Field, MultipartError, Field, MultipartError,
}; };
/// Write the field to a temporary file on disk. /// Write the field to a temporary file on disk.
#[derive(Debug)] #[derive(Debug)]
pub struct Tempfile { pub struct TempFile {
/// The temporary file on disk. /// The temporary file on disk.
pub file: NamedTempFile, pub file: NamedTempFile,
@ -36,7 +36,7 @@ pub struct Tempfile {
pub size: usize, pub size: usize,
} }
impl<'t> FieldReader<'t> for Tempfile { impl<'t> FieldReader<'t> for TempFile {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>; type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field( fn read_field(
@ -45,34 +45,31 @@ impl<'t> FieldReader<'t> for Tempfile {
limits: &'t mut Limits, limits: &'t mut Limits,
) -> Self::Future { ) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let config = TempfileConfig::from_req(req); let config = TempFileConfig::from_req(req);
let field_name = field.name().to_owned(); let field_name = field.name().to_owned();
let mut size = 0; let mut size = 0;
let file = config let file = config.create_tempfile().map_err(|err| {
.create_tempfile() config.map_error(req, &field_name, TempFileError::FileIo(err))
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?; })?;
let mut file_async = tokio::fs::File::from_std( let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| {
file.reopen() config.map_error(req, &field_name, TempFileError::FileIo(err))
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?, })?);
);
while let Some(chunk) = field.try_next().await? { while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), false)?; limits.try_consume_limits(chunk.len(), false)?;
size += chunk.len(); size += chunk.len();
file_async file_async.write_all(chunk.as_ref()).await.map_err(|err| {
.write_all(chunk.as_ref()) config.map_error(req, &field_name, TempFileError::FileIo(err))
.await })?;
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?;
} }
file_async file_async.flush().await.map_err(|err| {
.flush() config.map_error(req, &field_name, TempFileError::FileIo(err))
.await })?;
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?;
Ok(Tempfile { Ok(TempFile {
file, file,
content_type: field.content_type().map(ToOwned::to_owned), content_type: field.content_type().map(ToOwned::to_owned),
file_name: field file_name: field
@ -87,28 +84,28 @@ impl<'t> FieldReader<'t> for Tempfile {
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum TempfileError { pub enum TempFileError {
/// File I/O Error /// File I/O Error
#[display(fmt = "File I/O error: {}", _0)] #[display(fmt = "File I/O error: {}", _0)]
FileIo(std::io::Error), FileIo(std::io::Error),
} }
impl ResponseError for TempfileError { impl ResponseError for TempFileError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
} }
} }
/// Configuration for the [`Tempfile`] field reader. /// Configuration for the [`TempFile`] field reader.
#[derive(Clone)] #[derive(Clone)]
pub struct TempfileConfig { pub struct TempFileConfig {
err_handler: FieldErrorHandler<TempfileError>, err_handler: FieldErrorHandler<TempFileError>,
directory: Option<PathBuf>, directory: Option<PathBuf>,
} }
impl TempfileConfig { impl TempFileConfig {
fn create_tempfile(&self) -> io::Result<NamedTempFile> { fn create_tempfile(&self) -> io::Result<NamedTempFile> {
if let Some(dir) = self.directory.as_deref() { if let Some(ref dir) = self.directory {
NamedTempFile::new_in(dir) NamedTempFile::new_in(dir)
} else { } else {
NamedTempFile::new() NamedTempFile::new()
@ -116,21 +113,17 @@ impl TempfileConfig {
} }
} }
const DEFAULT_CONFIG: TempfileConfig = TempfileConfig { impl TempFileConfig {
err_handler: None, /// Sets custom error handler.
directory: None,
};
impl TempfileConfig {
pub fn error_handler<F>(mut self, f: F) -> Self pub fn error_handler<F>(mut self, f: F) -> Self
where where
F: Fn(TempfileError, &HttpRequest) -> Error + Send + Sync + 'static, F: Fn(TempFileError, &HttpRequest) -> Error + Send + Sync + 'static,
{ {
self.err_handler = Some(Arc::new(f)); self.err_handler = Some(Arc::new(f));
self self
} }
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall /// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config. /// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self { fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>() req.app_data::<Self>()
@ -142,10 +135,10 @@ impl TempfileConfig {
&self, &self,
req: &HttpRequest, req: &HttpRequest,
field_name: &str, field_name: &str,
err: TempfileError, err: TempFileError,
) -> MultipartError { ) -> MultipartError {
let source = if let Some(err_handler) = self.err_handler.as_ref() { let source = if let Some(ref err_handler) = self.err_handler {
(*err_handler)(err, req) (err_handler)(err, req)
} else { } else {
err.into() err.into()
}; };
@ -165,7 +158,12 @@ impl TempfileConfig {
} }
} }
impl Default for TempfileConfig { const DEFAULT_CONFIG: TempFileConfig = TempFileConfig {
err_handler: None,
directory: None,
};
impl Default for TempFileConfig {
fn default() -> Self { fn default() -> Self {
DEFAULT_CONFIG DEFAULT_CONFIG
} }
@ -178,11 +176,11 @@ mod tests {
use actix_multipart_rfc7578::client::multipart; use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
use crate::form::{tempfile::Tempfile, tests::send_form, MultipartForm}; use crate::form::{tempfile::TempFile, tests::send_form, MultipartForm};
#[derive(MultipartForm)] #[derive(MultipartForm)]
struct FileForm { struct FileForm {
file: Tempfile, file: TempFile,
} }
async fn test_file_route(form: MultipartForm<FileForm>) -> impl Responder { async fn test_file_route(form: MultipartForm<FileForm>) -> impl Responder {

View File

@ -21,6 +21,7 @@ use crate::{
pub struct Text<T: DeserializeOwned>(pub T); pub struct Text<T: DeserializeOwned>(pub T);
impl<T: DeserializeOwned> Text<T> { impl<T: DeserializeOwned> Text<T> {
/// Unwraps into inner value.
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.0 self.0
} }
@ -41,7 +42,7 @@ where
let valid = if let Some(mime) = field.content_type() { let valid = if let Some(mime) = field.content_type() {
mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN) mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN)
} else { } else {
// https://www.rfc-editor.org/rfc/rfc7578#section-4.4 // https://datatracker.ietf.org/doc/html/rfc7578#section-4.4
// content type defaults to text/plain, so None should be considered valid // content type defaults to text/plain, so None should be considered valid
true true
}; };
@ -100,12 +101,8 @@ pub struct TextConfig {
validate_content_type: bool, validate_content_type: bool,
} }
const DEFAULT_CONFIG: TextConfig = TextConfig {
err_handler: None,
validate_content_type: true,
};
impl TextConfig { impl TextConfig {
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self pub fn error_handler<F>(mut self, f: F) -> Self
where where
F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static, F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static,
@ -114,7 +111,7 @@ impl TextConfig {
self self
} }
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall /// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config. /// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self { fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>() req.app_data::<Self>()
@ -123,8 +120,8 @@ impl TextConfig {
} }
fn map_error(&self, req: &HttpRequest, err: TextError) -> Error { fn map_error(&self, req: &HttpRequest, err: TextError) -> Error {
if let Some(err_handler) = self.err_handler.as_ref() { if let Some(ref err_handler) = self.err_handler {
(*err_handler)(err, req) (err_handler)(err, req)
} else { } else {
err.into() err.into()
} }
@ -140,6 +137,11 @@ impl TextConfig {
} }
} }
const DEFAULT_CONFIG: TextConfig = TextConfig {
err_handler: None,
validate_content_type: true,
};
impl Default for TextConfig { impl Default for TextConfig {
fn default() -> Self { fn default() -> Self {
DEFAULT_CONFIG DEFAULT_CONFIG