actix_multipart/form/
tempfile.rs

1//! Writes a field to a temporary file on disk.
2
3use 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/// Write the field to a temporary file on disk.
24#[derive(Debug)]
25pub struct TempFile {
26    /// The temporary file on disk.
27    pub file: NamedTempFile,
28
29    /// The value of the `content-type` header.
30    pub content_type: Option<Mime>,
31
32    /// The `filename` value in the `content-disposition` header.
33    pub file_name: Option<String>,
34
35    /// The size in bytes of the file.
36    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    /// File I/O Error
85    #[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/// Configuration for the [`TempFile`] field reader.
96#[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    /// Sets custom error handler.
114    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    /// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
123    /// back to the default payload config.
124    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    /// Sets the directory that temp files will be created in.
144    ///
145    /// The default temporary file location is platform dependent.
146    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}