actix_easy_multipart/
tempfile.rs1use crate::tempfile::TempfileError::FileIo;
3use crate::Field;
4use crate::{field_mime, FieldReader, Limits};
5use actix_web::http::StatusCode;
6use actix_web::{web, HttpRequest, ResponseError};
7use derive_more::{Display, Error};
8use futures_core::future::LocalBoxFuture;
9use futures_util::{FutureExt, TryStreamExt};
10use mime::Mime;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use tempfile::NamedTempFile;
14use tokio::io::AsyncWriteExt;
15
16#[derive(Debug)]
18pub struct Tempfile {
19 pub file: NamedTempFile,
21 pub content_type: Option<Mime>,
23 pub file_name: Option<String>,
25 pub size: usize,
27}
28
29impl<'t> FieldReader<'t> for Tempfile {
30 type Future = LocalBoxFuture<'t, Result<Self, crate::Error>>;
31
32 fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future {
33 async move {
34 let config = TempfileConfig::from_req(req);
35 let field_name = field.name().to_owned();
36 let mut size = 0;
37
38 let file = if let Some(dir) = &config.directory {
39 NamedTempFile::new_in(dir)
40 } else {
41 NamedTempFile::new()
42 }
43 .map_err(|e| config.map_error(req, &field_name, FileIo(e)))?;
44
45 let mut file_async = tokio::fs::File::from_std(
46 file.reopen()
47 .map_err(|e| config.map_error(req, &field_name, FileIo(e)))?,
48 );
49
50 while let Some(chunk) = field.try_next().await? {
51 limits.try_consume_limits(chunk.len(), false)?;
52 size += chunk.len();
53 file_async
54 .write_all(chunk.as_ref())
55 .await
56 .map_err(|e| config.map_error(req, &field_name, FileIo(e)))?;
57 }
58 file_async
59 .flush()
60 .await
61 .map_err(|e| config.map_error(req, &field_name, FileIo(e)))?;
62
63 Ok(Tempfile {
64 file,
65 content_type: field_mime(&field),
66 file_name: field
67 .content_disposition()
68 .get_filename()
69 .map(str::to_owned),
70 size,
71 })
72 }
73 .boxed_local()
74 }
75}
76
77#[derive(Debug, Display, Error)]
78#[non_exhaustive]
79pub enum TempfileError {
80 #[display(fmt = "File I/O error: {}", _0)]
82 FileIo(std::io::Error),
83}
84
85impl ResponseError for TempfileError {
86 fn status_code(&self) -> StatusCode {
87 StatusCode::INTERNAL_SERVER_ERROR
88 }
89}
90
91#[derive(Clone)]
93pub struct TempfileConfig {
94 err_handler: Option<Arc<dyn Fn(TempfileError, &HttpRequest) -> actix_web::Error + Send + Sync>>,
95 directory: Option<PathBuf>,
96}
97
98const DEFAULT_CONFIG: TempfileConfig = TempfileConfig {
99 err_handler: None,
100 directory: None,
101};
102
103impl TempfileConfig {
104 pub fn error_handler<F>(mut self, f: F) -> Self
105 where
106 F: Fn(TempfileError, &HttpRequest) -> actix_web::Error + Send + Sync + 'static,
107 {
108 self.err_handler = Some(Arc::new(f));
109 self
110 }
111
112 fn from_req(req: &HttpRequest) -> &Self {
115 req.app_data::<Self>()
116 .or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
117 .unwrap_or(&DEFAULT_CONFIG)
118 }
119
120 fn map_error(&self, req: &HttpRequest, field_name: &str, err: TempfileError) -> crate::Error {
121 let source = if let Some(err_handler) = self.err_handler.as_ref() {
122 (*err_handler)(err, req)
123 } else {
124 err.into()
125 };
126 crate::Error::Field {
127 field_name: field_name.to_owned(),
128 source,
129 }
130 }
131
132 pub fn directory<P: AsRef<Path>>(mut self, dir: P) -> Self {
134 self.directory = Some(dir.as_ref().to_owned());
135 self
136 }
137}
138
139impl Default for TempfileConfig {
140 fn default() -> Self {
141 DEFAULT_CONFIG
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use crate::tempfile::Tempfile;
148 use crate::tests::send_form;
149 use crate::MultipartForm;
150 use actix_multipart_rfc7578::client::multipart;
151 use actix_web::http::StatusCode;
152 use actix_web::{web, App, HttpResponse, Responder};
153 use std::io::{Cursor, Read};
154
155 #[derive(MultipartForm)]
156 struct FileForm {
157 file: Tempfile,
158 }
159
160 async fn test_file_route(form: MultipartForm<FileForm>) -> impl Responder {
161 let mut form = form.into_inner();
162 let mut contents = String::new();
163 form.file.file.read_to_string(&mut contents).unwrap();
164 assert_eq!(contents, "Hello, world!");
165 assert_eq!(form.file.file_name.unwrap(), "testfile.txt");
166 assert_eq!(form.file.content_type.unwrap(), mime::TEXT_PLAIN);
167 HttpResponse::Ok().finish()
168 }
169
170 #[actix_rt::test]
171 async fn test_file_upload() {
172 let srv = actix_test::start(|| App::new().route("/", web::post().to(test_file_route)));
173
174 let mut form = multipart::Form::default();
175 let bytes = Cursor::new("Hello, world!");
176 form.add_reader_file_with_mime("file", bytes, "testfile.txt", mime::TEXT_PLAIN);
177 let response = send_form(&srv, form, "/").await;
178 assert_eq!(response.status(), StatusCode::OK);
179 }
180}