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