rocket_community/fs/
temp_file.rs

1use std::path::{Path, PathBuf};
2use std::{io, mem};
3
4use crate::data::{self, Capped, Data, FromData, Limits, N};
5use crate::form::{error::Errors, DataField, FromFormField, ValueField};
6use crate::fs::FileName;
7use crate::http::{ContentType, Status};
8use crate::outcome::IntoOutcome;
9use crate::Request;
10
11use either::Either;
12use tempfile::{NamedTempFile, TempPath};
13use tokio::fs::{self, File};
14use tokio::io::{AsyncBufRead, BufReader};
15use tokio::task;
16
17/// A data and form guard that streams data into a temporary file.
18///
19/// `TempFile` is a data and form field (both value and data fields) guard that
20/// streams incoming data into file in a temporary location. The file is deleted
21/// when the `TempFile` handle is dropped unless it is persisted with
22/// [`TempFile::persist_to()`] or copied with [`TempFile::copy_to()`].
23///
24/// # Hazards
25///
26/// Temporary files are cleaned by system file cleaners periodically. While an
27/// attempt is made not to delete temporary files in use, _detection_ of when a
28/// temporary file is being used is unreliable. As a result, a time-of-check to
29/// time-of-use race condition from the creation of a `TempFile` to the
30/// persistence of the `TempFile` may occur. Specifically, the following
31/// sequence may occur:
32///
33/// 1. A `TempFile` is created at random path `foo`.
34/// 2. The system cleaner removes the file at path `foo`.
35/// 3. Another application creates a file at path `foo`.
36/// 4. The `TempFile`, ostensibly at path `foo`, is persisted unexpectedly
37///    with contents different from those in step 1.
38///
39/// To safe-guard against this issue, you should ensure that your temporary file
40/// cleaner, if any, does not delete files too eagerly.
41///
42/// # Configuration
43///
44/// `TempFile` is configured via the following [`config`](crate::config)
45/// parameters:
46///
47/// | Name               | Default             | Description                             |
48/// |--------------------|---------------------|-----------------------------------------|
49/// | `temp_dir`         | [`env::temp_dir()`] | Directory for temporary file storage.   |
50/// | `limits.file`      | 1MiB                | Default limit for all file extensions.  |
51/// | `limits.file/$ext` | _N/A_               | Limit for files with extension `$ext`.  |
52///
53/// [`env::temp_dir()`]: std::env::temp_dir()
54///
55/// When used as a form guard, the extension `$ext` is identified by the form
56/// field's `Content-Type` ([`ContentType::extension()`]). When used as a data
57/// guard, the extension is identified by the Content-Type of the request, if
58/// any. If there is no Content-Type, the limit `file` is used.
59///
60/// # Cappable
61///
62/// A data stream can be partially read into a `TempFile` even if the incoming
63/// stream exceeds the data limit via the [`Capped<TempFile>`] data and form
64/// guard.
65///
66/// # Examples
67///
68/// **Data Guard**
69///
70/// ```rust
71/// # extern crate rocket_community as rocket;
72/// # use rocket::post;
73/// use rocket::fs::TempFile;
74///
75/// #[post("/upload", data = "<file>")]
76/// async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> {
77///     file.persist_to("/tmp/complete/file.txt").await?;
78///     Ok(())
79/// }
80/// ```
81///
82/// **Form Field**
83///
84/// ```rust
85/// # #[macro_use] extern crate rocket_community as rocket;
86/// use rocket::fs::TempFile;
87/// use rocket::form::Form;
88///
89/// #[derive(FromForm)]
90/// struct Upload<'f> {
91///     upload: TempFile<'f>
92/// }
93///
94/// #[post("/form", data = "<form>")]
95/// async fn upload(mut form: Form<Upload<'_>>) -> std::io::Result<()> {
96///     form.upload.persist_to("/tmp/complete/file.txt").await?;
97///     Ok(())
98/// }
99/// ```
100///
101/// See also the [`Capped`] documentation for an example of `Capped<TempFile>`
102/// as a data guard.
103#[derive(Debug)]
104pub enum TempFile<'v> {
105    #[doc(hidden)]
106    File {
107        file_name: Option<&'v FileName>,
108        content_type: Option<ContentType>,
109        path: Either<TempPath, PathBuf>,
110        len: u64,
111    },
112    #[doc(hidden)]
113    Buffered { content: &'v [u8] },
114}
115
116impl<'v> TempFile<'v> {
117    /// Persists the temporary file, moving it to `path`. If a file exists at
118    /// the target path, `self` will atomically replace it. `self.path()` is
119    /// updated to `path`.
120    ///
121    /// This method _does not_ create a copy of `self`, nor a new link to the
122    /// contents of `self`: it renames the temporary file to `path` and marks it
123    /// as non-temporary. As a result, this method _cannot_ be used to create
124    /// multiple copies of `self`. To create multiple links, use
125    /// [`std::fs::hard_link()`] with `path` as the `src` _after_ calling this
126    /// method.
127    ///
128    /// # Cross-Device Persistence
129    ///
130    /// Attempting to persist a temporary file across logical devices (or mount
131    /// points) will result in an error. This is a limitation of the underlying
132    /// OS. Your options are thus:
133    ///
134    ///   1. Store temporary file in the same logical device.
135    ///
136    ///      Change the `temp_dir` configuration parameter to be in the same
137    ///      logical device as the permanent location. This is the preferred
138    ///      solution.
139    ///
140    ///   2. Copy the temporary file using [`TempFile::copy_to()`] or
141    ///      [`TempFile::move_copy_to()`] instead.
142    ///
143    ///      This is a _full copy_ of the file, creating a duplicate version of
144    ///      the file at the destination. This should be avoided for performance
145    ///      reasons.
146    ///
147    /// # Example
148    ///
149    /// ```rust
150    /// # #[macro_use] extern crate rocket_community as rocket;
151    /// use rocket::fs::TempFile;
152    ///
153    /// #[post("/", data = "<file>")]
154    /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> {
155    ///     # assert!(file.path().is_none());
156    ///     # let some_path = std::env::temp_dir().join("some-persist.txt");
157    ///     file.persist_to(&some_path).await?;
158    ///     assert_eq!(file.path(), Some(&*some_path));
159    ///
160    ///     Ok(())
161    /// }
162    /// # let file = TempFile::Buffered { content: "hi".as_bytes() };
163    /// # rocket::async_test(handle(file)).unwrap();
164    /// ```
165    pub async fn persist_to<P>(&mut self, path: P) -> io::Result<()>
166    where
167        P: AsRef<Path>,
168    {
169        let new_path = path.as_ref().to_path_buf();
170        match self {
171            TempFile::File { path: either, .. } => {
172                let path = mem::replace(either, Either::Right(new_path.clone()));
173                match path {
174                    Either::Left(temp) => {
175                        let result = task::spawn_blocking(move || temp.persist(new_path))
176                            .await
177                            .map_err(|_| {
178                                io::Error::new(io::ErrorKind::BrokenPipe, "spawn_block")
179                            })?;
180
181                        if let Err(e) = result {
182                            *either = Either::Left(e.path);
183                            return Err(e.error);
184                        }
185                    }
186                    Either::Right(prev) => {
187                        if let Err(e) = fs::rename(&prev, new_path).await {
188                            *either = Either::Right(prev);
189                            return Err(e);
190                        }
191                    }
192                }
193            }
194            TempFile::Buffered { content } => {
195                fs::write(&new_path, &content).await?;
196                *self = TempFile::File {
197                    file_name: None,
198                    content_type: None,
199                    path: Either::Right(new_path),
200                    len: content.len() as u64,
201                };
202            }
203        }
204
205        Ok(())
206    }
207
208    /// Persists the temporary file at its temporary path and creates a full
209    /// copy at `path`. The `self.path()` is _not_ updated, unless no temporary
210    /// file existed prior, and the temporary file is _not_ removed. Thus, there
211    /// will be _two_ files with the same contents.
212    ///
213    /// Unlike [`TempFile::persist_to()`], this method does not incur
214    /// cross-device limitations, at the performance cost of a full copy. Prefer
215    /// to use `persist_to()` with a valid `temp_dir` configuration parameter if
216    /// no more than one copy of a file is required.
217    ///
218    /// # Example
219    ///
220    /// ```rust
221    /// # #[macro_use] extern crate rocket_community as rocket;
222    /// use rocket::fs::TempFile;
223    ///
224    /// #[post("/", data = "<file>")]
225    /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> {
226    ///     # assert!(file.path().is_none());
227    ///     # let some_path = std::env::temp_dir().join("some-file.txt");
228    ///     file.copy_to(&some_path).await?;
229    ///     # assert_eq!(file.path(), Some(&*some_path));
230    ///     # let some_other_path = std::env::temp_dir().join("some-other.txt");
231    ///     file.copy_to(&some_other_path).await?;
232    ///     assert_eq!(file.path(), Some(&*some_path));
233    ///     # assert_eq!(std::fs::read(some_path).unwrap(), b"hi");
234    ///     # assert_eq!(std::fs::read(some_other_path).unwrap(), b"hi");
235    ///
236    ///     Ok(())
237    /// }
238    /// # let file = TempFile::Buffered { content: b"hi" };
239    /// # rocket::async_test(handle(file)).unwrap();
240    /// ```
241    pub async fn copy_to<P>(&mut self, path: P) -> io::Result<()>
242    where
243        P: AsRef<Path>,
244    {
245        match self {
246            TempFile::File { path: either, .. } => {
247                let old_path = mem::replace(either, Either::Right(either.to_path_buf()));
248                match old_path {
249                    Either::Left(temp) => {
250                        let result =
251                            task::spawn_blocking(move || temp.keep())
252                                .await
253                                .map_err(|_| {
254                                    io::Error::new(io::ErrorKind::BrokenPipe, "spawn_block")
255                                })?;
256
257                        if let Err(e) = result {
258                            *either = Either::Left(e.path);
259                            return Err(e.error);
260                        }
261                    }
262                    Either::Right(_) => { /* do nada */ }
263                };
264
265                tokio::fs::copy(&either, path).await?;
266            }
267            TempFile::Buffered { content } => {
268                let path = path.as_ref();
269                fs::write(&path, &content).await?;
270                *self = TempFile::File {
271                    file_name: None,
272                    content_type: None,
273                    path: Either::Right(path.to_path_buf()),
274                    len: content.len() as u64,
275                };
276            }
277        }
278
279        Ok(())
280    }
281
282    /// Persists the temporary file at its temporary path, creates a full copy
283    /// at `path`, and then deletes the temporary file. `self.path()` is updated
284    /// to `path`.
285    ///
286    /// Like [`TempFile::copy_to()`] and unlike [`TempFile::persist_to()`], this
287    /// method does not incur cross-device limitations, at the performance cost
288    /// of a full copy and file deletion. Prefer to use `persist_to()` with a
289    /// valid `temp_dir` configuration parameter if no more than one copy of a
290    /// file is required.
291    ///
292    /// # Example
293    ///
294    /// ```rust
295    /// # #[macro_use] extern crate rocket_community as rocket;
296    /// use rocket::fs::TempFile;
297    ///
298    /// #[post("/", data = "<file>")]
299    /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> {
300    ///     # assert!(file.path().is_none());
301    ///     # let some_path = std::env::temp_dir().join("some-copy.txt");
302    ///     file.move_copy_to(&some_path).await?;
303    ///     # assert_eq!(file.path(), Some(&*some_path));
304    ///
305    ///     Ok(())
306    /// }
307    /// # let file = TempFile::Buffered { content: "hi".as_bytes() };
308    /// # rocket::async_test(handle(file)).unwrap();
309    /// ```
310    pub async fn move_copy_to<P>(&mut self, path: P) -> io::Result<()>
311    where
312        P: AsRef<Path>,
313    {
314        let dest = path.as_ref();
315        self.copy_to(dest).await?;
316
317        if let TempFile::File { path, .. } = self {
318            fs::remove_file(&path).await?;
319            *path = Either::Right(dest.to_path_buf());
320        }
321
322        Ok(())
323    }
324
325    /// Open the file for reading, returning an `async` stream of the file.
326    ///
327    /// This method should be used sparingly. `TempFile` is intended to be used
328    /// when the incoming data is destined to be stored on disk. If the incoming
329    /// data is intended to be streamed elsewhere, prefer to implement a custom
330    /// form guard via [`FromFormField`] that directly streams the incoming data
331    /// to the ultimate destination.
332    ///
333    /// # Example
334    ///
335    /// ```rust
336    /// # #[macro_use] extern crate rocket_community as rocket;
337    /// use rocket::fs::TempFile;
338    /// use rocket::tokio::io;
339    ///
340    /// #[post("/", data = "<file>")]
341    /// async fn handle(file: TempFile<'_>) -> std::io::Result<()> {
342    ///     let mut stream = file.open().await?;
343    ///     io::copy(&mut stream, &mut io::stdout()).await?;
344    ///     Ok(())
345    /// }
346    /// # let file = TempFile::Buffered { content: "hi".as_bytes() };
347    /// # rocket::async_test(handle(file)).unwrap();
348    /// ```
349    pub async fn open(&self) -> io::Result<impl AsyncBufRead + '_> {
350        use tokio_util::either::Either;
351
352        match self {
353            TempFile::File { path, .. } => {
354                let path = match path {
355                    either::Either::Left(p) => p.as_ref(),
356                    either::Either::Right(p) => p.as_path(),
357                };
358
359                let reader = BufReader::new(File::open(path).await?);
360                Ok(Either::Left(reader))
361            }
362            TempFile::Buffered { content } => Ok(Either::Right(*content)),
363        }
364    }
365
366    /// Returns whether the file is empty.
367    ///
368    /// This is equivalent to `file.len() == 0`.
369    ///
370    /// This method does not perform any system calls.
371    ///
372    /// ```rust
373    /// # #[macro_use] extern crate rocket_community as rocket;
374    /// use rocket::fs::TempFile;
375    ///
376    /// #[post("/", data = "<file>")]
377    /// fn handler(file: TempFile<'_>) {
378    ///     if file.is_empty() {
379    ///         assert_eq!(file.len(), 0);
380    ///     }
381    /// }
382    /// ```
383    pub fn is_empty(&self) -> bool {
384        self.len() == 0
385    }
386
387    /// Returns the size, in bytes, of the file.
388    ///
389    /// This method does not perform any system calls.
390    ///
391    /// ```rust
392    /// # #[macro_use] extern crate rocket_community as rocket;
393    /// use rocket::fs::TempFile;
394    ///
395    /// #[post("/", data = "<file>")]
396    /// fn handler(file: TempFile<'_>) {
397    ///     let file_len = file.len();
398    /// }
399    /// ```
400    pub fn len(&self) -> u64 {
401        match self {
402            TempFile::File { len, .. } => *len,
403            TempFile::Buffered { content } => content.len() as u64,
404        }
405    }
406
407    /// Returns the path to the file if it is known.
408    ///
409    /// Once a file is persisted with [`TempFile::persist_to()`], this method is
410    /// guaranteed to return `Some`. Prior to this point, however, this method
411    /// may return `Some` or `None`, depending on whether the file is on disk or
412    /// partially buffered in memory.
413    ///
414    /// ```rust
415    /// # #[macro_use] extern crate rocket_community as rocket;
416    /// use rocket::fs::TempFile;
417    ///
418    /// #[post("/", data = "<file>")]
419    /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> {
420    ///     # assert!(file.path().is_none());
421    ///     # let some_path = std::env::temp_dir().join("some-path.txt");
422    ///     file.persist_to(&some_path).await?;
423    ///     assert_eq!(file.path(), Some(&*some_path));
424    ///     # assert_eq!(std::fs::read(some_path).unwrap(), b"hi");
425    ///
426    ///     Ok(())
427    /// }
428    /// # let file = TempFile::Buffered { content: b"hi" };
429    /// # rocket::async_test(handle(file)).unwrap();
430    /// ```
431    pub fn path(&self) -> Option<&Path> {
432        match self {
433            TempFile::File {
434                path: Either::Left(p),
435                ..
436            } => Some(p.as_ref()),
437            TempFile::File {
438                path: Either::Right(p),
439                ..
440            } => Some(p.as_path()),
441            TempFile::Buffered { .. } => None,
442        }
443    }
444
445    /// Returns the sanitized file name as specified in the form field.
446    ///
447    /// A multipart data form field can optionally specify the name of a file. A
448    /// browser will typically send the actual name of a user's selected file in
449    /// this field, but clients are also able to specify _any_ name, including
450    /// invalid or dangerous file names. This method returns a sanitized version
451    /// of that value, if it was specified, suitable and safe for use as a
452    /// permanent file name.
453    ///
454    /// Note that you will likely want to prepend or append random or
455    /// user-specific components to the name to avoid collisions; UUIDs make for
456    /// a good "random" data.
457    ///
458    /// See [`FileName::as_str()`] for specifics on sanitization.
459    ///
460    /// ```rust
461    /// # #[macro_use] extern crate rocket_community as rocket;
462    /// use rocket::fs::TempFile;
463    ///
464    /// #[post("/", data = "<file>")]
465    /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> {
466    ///     # let some_dir = std::env::temp_dir();
467    ///     if let Some(name) = file.name() {
468    ///         // Because of Rocket's sanitization, this is safe.
469    ///         file.persist_to(&some_dir.join(name)).await?;
470    ///     }
471    ///
472    ///     Ok(())
473    /// }
474    /// ```
475    pub fn name(&self) -> Option<&str> {
476        self.raw_name().and_then(|f| f.as_str())
477    }
478
479    /// Returns the raw name of the file as specified in the form field.
480    ///
481    /// ```rust
482    /// # #[macro_use] extern crate rocket_community as rocket;
483    /// use rocket::fs::TempFile;
484    ///
485    /// #[post("/", data = "<file>")]
486    /// async fn handle(mut file: TempFile<'_>) {
487    ///     let raw_name = file.raw_name();
488    /// }
489    /// ```
490    pub fn raw_name(&self) -> Option<&FileName> {
491        match *self {
492            TempFile::File { file_name, .. } => file_name,
493            TempFile::Buffered { .. } => None,
494        }
495    }
496
497    /// Returns the Content-Type of the file as specified in the form field.
498    ///
499    /// A multipart data form field can optionally specify the content-type of a
500    /// file. A browser will typically sniff the file's extension to set the
501    /// content-type. This method returns that value, if it was specified.
502    ///
503    /// ```rust
504    /// # #[macro_use] extern crate rocket_community as rocket;
505    /// use rocket::fs::TempFile;
506    ///
507    /// #[post("/", data = "<file>")]
508    /// fn handle(file: TempFile<'_>) {
509    ///     let content_type = file.content_type();
510    /// }
511    /// ```
512    pub fn content_type(&self) -> Option<&ContentType> {
513        match self {
514            TempFile::File { content_type, .. } => content_type.as_ref(),
515            TempFile::Buffered { .. } => None,
516        }
517    }
518
519    async fn from<'a>(
520        req: &Request<'_>,
521        data: Data<'_>,
522        file_name: Option<&'a FileName>,
523        content_type: Option<ContentType>,
524    ) -> io::Result<Capped<TempFile<'a>>> {
525        let limit = content_type
526            .as_ref()
527            .and_then(|ct| ct.extension())
528            .and_then(|ext| req.limits().find(["file", ext.as_str()]))
529            .or_else(|| req.limits().get("file"))
530            .unwrap_or(Limits::FILE);
531
532        let temp_dir = req.rocket().config().temp_dir.relative();
533        let file = task::spawn_blocking(move || NamedTempFile::new_in(temp_dir));
534        let file = file.await;
535        let file = file.map_err(|_| io::Error::other("spawn_block panic"))??;
536        let (file, temp_path) = file.into_parts();
537
538        let mut file = File::from_std(file);
539        let fut = data
540            .open(limit)
541            .stream_to(tokio::io::BufWriter::new(&mut file));
542        let n = fut.await;
543        let n = n?;
544        let temp_file = TempFile::File {
545            content_type,
546            file_name,
547            path: Either::Left(temp_path),
548            len: n.written,
549        };
550
551        Ok(Capped::new(temp_file, n))
552    }
553}
554
555#[crate::async_trait]
556impl<'v> FromFormField<'v> for Capped<TempFile<'v>> {
557    fn from_value(field: ValueField<'v>) -> Result<Self, Errors<'v>> {
558        let n = N {
559            written: field.value.len() as u64,
560            complete: true,
561        };
562        Ok(Capped::new(
563            TempFile::Buffered {
564                content: field.value.as_bytes(),
565            },
566            n,
567        ))
568    }
569
570    async fn from_data(f: DataField<'v, '_>) -> Result<Self, Errors<'v>> {
571        Ok(TempFile::from(f.request, f.data, f.file_name, Some(f.content_type)).await?)
572    }
573}
574
575#[crate::async_trait]
576impl<'r> FromData<'r> for Capped<TempFile<'_>> {
577    type Error = io::Error;
578
579    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> {
580        let has_form = |ty: &ContentType| ty.is_form_data() || ty.is_form();
581        if req.content_type().is_some_and(has_form) {
582            warn!(
583                request.content_type = req.content_type().map(display),
584                "Request contains a form that is not being processed.\n\
585                Bare `TempFile` data guard writes raw, unprocessed streams to disk\n\
586                Perhaps you meant to use `Form<TempFile<'_>>` instead?"
587            );
588        }
589
590        TempFile::from(req, data, None, req.content_type().cloned())
591            .await
592            .or_error(Status::BadRequest)
593    }
594}
595
596impl_strict_from_form_field_from_capped!(TempFile<'v>);
597impl_strict_from_data_from_capped!(TempFile<'_>);