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