rocket/fs/
temp_file.rs

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