actix_easy_multipart/
tempfile.rs

1//! Writes a field to a temporary file on disk.
2use 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/// Write the field to a temporary file on disk.
17#[derive(Debug)]
18pub struct Tempfile {
19    /// The temporary file on disk.
20    pub file: NamedTempFile,
21    /// The value of the `content-type` header.
22    pub content_type: Option<Mime>,
23    /// The `filename` value in the `content-disposition` header.
24    pub file_name: Option<String>,
25    /// The size in bytes of the file.
26    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    /// IO Error
81    #[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/// Configuration for the [`Tempfile`] field reader.
92#[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    /// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
113    /// back to the default payload config.
114    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    /// Set the directory tempfiles will be created in.
133    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}