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
- Added `MultipartForm` typed data extractor. [#2883]
[#2880]: https://github.com/actix/actix-web/pull/2880
[#2883]: https://github.com/actix/actix-web/pull/2883
## 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.
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2885]
[#2885]: https://github.com/actix/actix-web/pull/2885

View File

@ -1,7 +1,10 @@
[package]
name = "actix-multipart"
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"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
@ -30,6 +33,7 @@ actix-web = { version = "4", default-features = false }
bytes = "1"
derive_more = "0.99.5"
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"
local-waker = "0.1"
log = "0.4"
@ -39,15 +43,15 @@ serde = "1"
serde_json = "1"
serde_plain = "1"
# TODO(MSRV 1.60): replace with dep: prefix
tempfile-dep = { package = "tempfile", version = "3.3.0", optional = true }
tokio = { version = "1.13.1", features = ["sync"] }
tempfile-dep = { package = "tempfile", version = "3.4", optional = true }
tokio = { version = "1.18.5", features = ["sync"] }
[dev-dependencies]
actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1.0"
awc = "3.0.1"
actix-test = "0.1"
awc = "3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
tokio = { version = "1.18.5", features = ["sync"] }
tokio-stream = "0.1"

View File

@ -33,7 +33,7 @@ impl<'t> FieldReader<'t> for Bytes {
limits: &'t mut Limits,
) -> Self::Future {
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? {
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 derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture;
use futures_util::{TryFutureExt, TryStreamExt as _};
use futures_util::{TryFutureExt as _, TryStreamExt as _};
use crate::{Field, Multipart, MultipartError};
@ -80,7 +80,7 @@ where
DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField(
field.name().to_string(),
field.name().to_owned(),
))))
}
@ -89,7 +89,7 @@ where
}
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?;
state.insert(field_name, Box::new(t));
Ok(())
@ -117,11 +117,11 @@ where
Box::pin(async move {
// Note: Vec GroupReader always allows duplicates
let field_name = field.name().to_string();
let field_name = field.name().to_owned();
let vec = state
.entry(field_name)
.or_insert_with(|| Box::new(Vec::<T>::new()))
.or_insert_with(|| Box::<Vec<T>>::default())
.downcast_mut::<Vec<T>>()
.unwrap();
@ -159,15 +159,16 @@ where
DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField(
field.name().to_string(),
field.name().to_owned(),
))))
}
DuplicateField::Replace => {}
}
}
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?;
state.insert(field_name, Box::new(t));
Ok(())
@ -182,8 +183,9 @@ where
}
}
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. You should use
/// the [`macro@MultipartForm`] to implement this for your struct.
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor.
///
/// You should use the [`macro@MultipartForm`] macro to derive this for your struct.
pub trait MultipartCollect: Sized {
/// 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.
@ -205,13 +207,13 @@ pub trait MultipartCollect: Sized {
#[doc(hidden)]
pub enum DuplicateField {
/// Additional fields are not processed
/// Additional fields are not processed.
Ignore,
/// An error will be raised
/// An error will be raised.
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,
}
@ -270,10 +272,10 @@ impl Limits {
/// Typed `multipart/form-data` extractor.
///
/// 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.
///
/// Use [`MultipartFormConfig`] to configure extraction options.
/// Add a [`MultipartFormConfig`] to your app data to configure extraction.
#[derive(Deref, DerefMut)]
pub struct MultipartForm<T: MultipartCollect>(pub T);
@ -338,6 +340,8 @@ type MultipartFormErrorHandler =
Option<Arc<dyn Fn(MultipartError, &HttpRequest) -> Error + Send + Sync>>;
/// [`struct@MultipartForm`] extractor configuration.
///
/// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors.
#[derive(Clone)]
pub struct MultipartFormConfig {
total_limit: usize,
@ -346,19 +350,19 @@ pub struct 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 {
self.total_limit = total_limit;
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 {
self.memory_limit = memory_limit;
self
}
/// Set custom error handler.
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static,
@ -367,7 +371,7 @@ impl MultipartFormConfig {
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.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
@ -384,7 +388,7 @@ const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig {
impl Default for MultipartFormConfig {
fn default() -> Self {
DEFAULT_CONFIG.clone()
DEFAULT_CONFIG
}
}
@ -397,7 +401,7 @@ mod tests {
use awc::{Client, ClientResponse};
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(
srv: &TestServer,
@ -611,7 +615,7 @@ mod tests {
#[derive(MultipartForm)]
struct TestFileUploadLimits {
field: Tempfile,
field: TempFile,
}
async fn test_upload_limits_memory(

View File

@ -16,13 +16,13 @@ use tokio::io::AsyncWriteExt;
use super::FieldErrorHandler;
use crate::{
form::{tempfile::TempfileError::FileIo, FieldReader, Limits},
form::{FieldReader, Limits},
Field, MultipartError,
};
/// Write the field to a temporary file on disk.
#[derive(Debug)]
pub struct Tempfile {
pub struct TempFile {
/// The temporary file on disk.
pub file: NamedTempFile,
@ -36,7 +36,7 @@ pub struct Tempfile {
pub size: usize,
}
impl<'t> FieldReader<'t> for Tempfile {
impl<'t> FieldReader<'t> for TempFile {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field(
@ -45,34 +45,31 @@ impl<'t> FieldReader<'t> for Tempfile {
limits: &'t mut Limits,
) -> Self::Future {
Box::pin(async move {
let config = TempfileConfig::from_req(req);
let config = TempFileConfig::from_req(req);
let field_name = field.name().to_owned();
let mut size = 0;
let file = config
.create_tempfile()
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?;
let file = config.create_tempfile().map_err(|err| {
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?;
let mut file_async = tokio::fs::File::from_std(
file.reopen()
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?,
);
let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| {
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?);
while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), false)?;
size += chunk.len();
file_async
.write_all(chunk.as_ref())
.await
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?;
file_async.write_all(chunk.as_ref()).await.map_err(|err| {
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?;
}
file_async
.flush()
.await
.map_err(|err| config.map_error(req, &field_name, FileIo(err)))?;
file_async.flush().await.map_err(|err| {
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?;
Ok(Tempfile {
Ok(TempFile {
file,
content_type: field.content_type().map(ToOwned::to_owned),
file_name: field
@ -87,28 +84,28 @@ impl<'t> FieldReader<'t> for Tempfile {
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum TempfileError {
pub enum TempFileError {
/// File I/O Error
#[display(fmt = "File I/O error: {}", _0)]
FileIo(std::io::Error),
}
impl ResponseError for TempfileError {
impl ResponseError for TempFileError {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
/// Configuration for the [`Tempfile`] field reader.
/// Configuration for the [`TempFile`] field reader.
#[derive(Clone)]
pub struct TempfileConfig {
err_handler: FieldErrorHandler<TempfileError>,
pub struct TempFileConfig {
err_handler: FieldErrorHandler<TempFileError>,
directory: Option<PathBuf>,
}
impl TempfileConfig {
impl TempFileConfig {
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)
} else {
NamedTempFile::new()
@ -116,21 +113,17 @@ impl TempfileConfig {
}
}
const DEFAULT_CONFIG: TempfileConfig = TempfileConfig {
err_handler: None,
directory: None,
};
impl TempfileConfig {
impl TempFileConfig {
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
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
}
/// 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.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
@ -142,10 +135,10 @@ impl TempfileConfig {
&self,
req: &HttpRequest,
field_name: &str,
err: TempfileError,
err: TempFileError,
) -> MultipartError {
let source = if let Some(err_handler) = self.err_handler.as_ref() {
(*err_handler)(err, req)
let source = if let Some(ref err_handler) = self.err_handler {
(err_handler)(err, req)
} else {
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 {
DEFAULT_CONFIG
}
@ -178,11 +176,11 @@ mod tests {
use actix_multipart_rfc7578::client::multipart;
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)]
struct FileForm {
file: Tempfile,
file: TempFile,
}
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);
impl<T: DeserializeOwned> Text<T> {
/// Unwraps into inner value.
pub fn into_inner(self) -> T {
self.0
}
@ -41,7 +42,7 @@ where
let valid = if let Some(mime) = field.content_type() {
mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN)
} 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
true
};
@ -100,12 +101,8 @@ pub struct TextConfig {
validate_content_type: bool,
}
const DEFAULT_CONFIG: TextConfig = TextConfig {
err_handler: None,
validate_content_type: true,
};
impl TextConfig {
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static,
@ -114,7 +111,7 @@ impl TextConfig {
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.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
@ -123,8 +120,8 @@ impl TextConfig {
}
fn map_error(&self, req: &HttpRequest, err: TextError) -> Error {
if let Some(err_handler) = self.err_handler.as_ref() {
(*err_handler)(err, req)
if let Some(ref err_handler) = self.err_handler {
(err_handler)(err, req)
} else {
err.into()
}
@ -140,6 +137,11 @@ impl TextConfig {
}
}
const DEFAULT_CONFIG: TextConfig = TextConfig {
err_handler: None,
validate_content_type: true,
};
impl Default for TextConfig {
fn default() -> Self {
DEFAULT_CONFIG