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<'_>);