temp_file/
lib.rs

1//! temp-file
2//! =========
3//! [![crates.io version](https://img.shields.io/crates/v/temp-file.svg)](https://crates.io/crates/temp-file)
4//! [![license: Apache 2.0](https://gitlab.com/leonhard-llc/ops/-/raw/main/license-apache-2.0.svg)](https://gitlab.com/leonhard-llc/ops/-/raw/main/temp-file/LICENSE)
5//! [![unsafe forbidden](https://gitlab.com/leonhard-llc/ops/-/raw/main/unsafe-forbidden.svg)](https://github.com/rust-secure-code/safety-dance/)
6//! [![pipeline status](https://gitlab.com/leonhard-llc/ops/badges/main/pipeline.svg)](https://gitlab.com/leonhard-llc/ops/-/pipelines)
7//!
8//! Provides a `TempFile` struct.
9//!
10//! # Features
11//! - Makes a file in a system temporary directory
12//! - Deletes the file on drop
13//! - Optional name prefix, name suffix, contents, and directory.
14//! - Depends only on `std`
15//! - `forbid(unsafe_code)`
16//! - 100% test coverage
17//!
18//! # Limitations
19//! - Not security-hardened. See
20//!   [Secure Programming for Linux and Unix HOWTO - 7.10. Avoid Race Conditions](https://tldp.org/HOWTO/Secure-Programs-HOWTO/avoid-race.html)
21//!   and [`mkstemp`](https://linux.die.net/man/3/mkstemp).
22//! - Not tested on [Windows](https://gitlab.com/leonhard-llc/ops/-/issues/6).
23//!
24//! # Alternatives
25//! - [`tempfile`](https://crates.io/crates/tempfile)
26//!   - Popular and mature
27//!   - Supports some security-sensitive use cases
28//!   - Contains `unsafe`, dependencies full of `unsafe`
29//!   - Heavy dependencies (libc, winapi, rand, etc.)
30//! - [`test-temp-file`](https://crates.io/crates/test-temp-file)
31//!   - Depends on crates which contain `unsafe`
32//!   - Incomplete documentation
33//! - [`temp_file_name`](https://crates.io/crates/temp_file_name)
34//!   - Does not delete file
35//!   - Usage is not straightforward.  Missing example.
36//! - [`mktemp`](https://crates.io/crates/mktemp)
37//!   - Sets file mode 0600 on unix
38//!   - Contains `unsafe`
39//!   - No readme or online docs
40//!
41//! # Related Crates
42//! - [`temp-dir`](https://crates.io/crates/temp-dir)
43//!
44//! # Example
45//! ```rust
46//! let t = temp_file::with_contents(b"abc");
47//! // Prints "/tmp/1a9b0".
48//! println!("{:?}", t.path());
49//! assert_eq!(
50//!   "abc",
51//!   std::fs::read_to_string(t.path()).unwrap(),
52//! );
53//! // Prints "/tmp/1a9b1".
54//! println!("{:?}", temp_file::empty().path());
55//! ```
56//!
57//! # Cargo Geiger Safety Report
58//! # Changelog
59//! - v0.1.9 - `AsRef<Path>`
60//! - v0.1.8 - Work when the file already exists.
61//! - v0.1.7 - Add `in_dir`, `with_suffix`, and `TempFileBuilder`.
62//! - v0.1.6
63//!   - Return `std::io::Error` instead of `String`.
64//!   - Add
65//!     [`cleanup`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.cleanup).
66//! - v0.1.5 - Increase test coverage
67//! - v0.1.4 - Add
68//!   [`leak`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.leak)
69//!   and
70//!   [`panic_on_cleanup_error`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.panic_on_cleanup_error).
71//! - v0.1.3 - Update docs
72//! - v0.1.2 - Update example
73//! - v0.1.1 - Minor code cleanup, update docs
74//! - v0.1.0 - Initial version
75#![forbid(unsafe_code)]
76// TODO(mleonhard) Implement features requested of `tempfile` crate:
77//   https://github.com/Stebalien/tempfile/issues
78use core::sync::atomic::{AtomicU32, Ordering};
79use std::io::ErrorKind;
80use std::path::{Path, PathBuf};
81use std::sync::atomic::AtomicBool;
82
83#[doc(hidden)]
84pub static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0);
85#[doc(hidden)]
86pub static INTERNAL_RETRY: AtomicBool = AtomicBool::new(true);
87
88pub struct TempFileBuilder {
89    dir_path: Option<PathBuf>,
90    prefix: Option<String>,
91    suffix: Option<String>,
92}
93impl TempFileBuilder {
94    #[allow(clippy::new_without_default)]
95    #[must_use]
96    pub fn new() -> Self {
97        Self {
98            dir_path: None,
99            prefix: None,
100            suffix: None,
101        }
102    }
103
104    #[must_use]
105    pub fn in_dir(mut self, p: impl AsRef<Path>) -> Self {
106        self.dir_path = Some(p.as_ref().to_path_buf());
107        self
108    }
109
110    #[must_use]
111    pub fn prefix(mut self, s: impl AsRef<str>) -> Self {
112        self.prefix = Some(s.as_ref().to_string());
113        self
114    }
115
116    #[must_use]
117    pub fn suffix(mut self, s: impl AsRef<str>) -> Self {
118        self.suffix = Some(s.as_ref().to_string());
119        self
120    }
121
122    /// Creates the temp file.
123    ///
124    /// # Errors
125    /// Returns `Err` when it fails to create the file.
126    pub fn build(self) -> Result<TempFile, std::io::Error> {
127        TempFile::internal_new(
128            self.dir_path.as_deref(),
129            self.prefix.as_ref().map(AsRef::as_ref),
130            self.suffix.as_ref().map(AsRef::as_ref),
131        )
132    }
133}
134
135/// The path of an existing writable file in a system temporary directory.
136///
137/// Deletes the file on drop.  Ignores errors deleting the file.
138///
139/// # Example
140/// ```rust
141/// use temp_file::TempFile;
142/// let t = TempFile::new()
143///   .unwrap()
144///   .with_contents(b"abc")
145///   .unwrap();
146/// // Prints "/tmp/1a9b0".
147/// println!("{:?}", t.path());
148/// assert_eq!(
149///   "abc",
150///   std::fs::read_to_string(t.path()).unwrap(),
151/// );
152/// // Prints "/tmp/1a9b1".
153/// println!("{:?}", TempFile::new().unwrap().path());
154/// ```
155#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)]
156pub struct TempFile {
157    path_buf: PathBuf,
158    delete_on_drop: bool,
159    panic_on_delete_err: bool,
160}
161impl TempFile {
162    fn internal_new(
163        dir: Option<&Path>,
164        prefix: Option<&str>,
165        suffix: Option<&str>,
166    ) -> Result<Self, std::io::Error> {
167        let dir = dir.map_or_else(std::env::temp_dir, Path::to_path_buf);
168        loop {
169            let filename = format!(
170                "{}{:x}{:x}{}",
171                prefix.unwrap_or(""),
172                std::process::id(),
173                INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel),
174                suffix.unwrap_or(""),
175            );
176            let file_path = dir.join(filename);
177            let mut open_opts = std::fs::OpenOptions::new();
178            open_opts.create_new(true);
179            open_opts.write(true);
180            match open_opts.open(&file_path) {
181                Err(e)
182                    if e.kind() == ErrorKind::AlreadyExists
183                        && INTERNAL_RETRY.load(Ordering::Acquire) => {}
184                Err(e) => {
185                    return Err(std::io::Error::new(
186                        e.kind(),
187                        format!("error creating file {file_path:?}: {e}"),
188                    ))
189                }
190                Ok(_) => {
191                    return Ok(Self {
192                        path_buf: file_path,
193                        delete_on_drop: true,
194                        panic_on_delete_err: false,
195                    })
196                }
197            }
198        }
199    }
200
201    fn remove_file(path: &Path) -> Result<(), std::io::Error> {
202        match std::fs::remove_file(path) {
203            Ok(()) => Ok(()),
204            Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
205            Err(e) => Err(std::io::Error::new(
206                e.kind(),
207                format!("error removing file {path:?}: {e}"),
208            )),
209        }
210    }
211
212    /// Create a new empty file in a system temporary directory.
213    ///
214    /// Drop the returned struct to delete the file.
215    ///
216    /// # Errors
217    /// Returns `Err` when it fails to create the file.
218    ///
219    /// # Example
220    /// ```rust
221    /// // Prints "/tmp/1a9b0".
222    /// println!("{:?}", temp_file::TempFile::new().unwrap().path());
223    /// ```
224    pub fn new() -> Result<Self, std::io::Error> {
225        Self::internal_new(None, None, None)
226    }
227
228    /// Create a new empty file in `dir`.
229    ///
230    /// Drop the returned struct to delete the file.
231    ///
232    /// # Errors
233    /// Returns `Err` when it fails to create the file.
234    ///
235    /// # Example
236    /// ```
237    /// // Prints "/tmp/temp_uploads/1a9b0".
238    /// let dir = std::env::temp_dir().join("temp_uploads");
239    /// # std::fs::create_dir(&dir).unwrap();
240    /// println!("{:?}", temp_file::TempFile::in_dir(&dir).unwrap().path());
241    /// # std::fs::remove_dir(&dir).unwrap();
242    /// ```
243    pub fn in_dir(dir: impl AsRef<Path>) -> Result<Self, std::io::Error> {
244        Self::internal_new(Some(dir.as_ref()), None, None)
245    }
246
247    /// Create a new empty file in a system temporary directory.
248    /// Use `prefix` as the first part of the file's name.
249    ///
250    /// Drop the returned struct to delete the file.
251    ///
252    /// # Errors
253    /// Returns `Err` when it fails to create the file.
254    ///
255    /// # Example
256    /// ```rust
257    /// // Prints "/tmp/ok1a9b0".
258    /// println!("{:?}", temp_file::TempFile::with_prefix("ok").unwrap().path());
259    /// ```
260    pub fn with_prefix(prefix: impl AsRef<str>) -> Result<Self, std::io::Error> {
261        Self::internal_new(None, Some(prefix.as_ref()), None)
262    }
263
264    /// Create a new empty file in a system temporary directory.
265    /// Use `suffix` as the last part of the file's name.
266    ///
267    /// You can use this to give the filename a particular extension.
268    ///
269    /// Drop the returned struct to delete the file.
270    ///
271    /// # Errors
272    /// Returns `Err` when it fails to create the file.
273    ///
274    /// # Example
275    /// ```rust
276    /// // Prints "/tmp/1a9b0.txt".
277    /// println!("{:?}", temp_file::TempFile::with_suffix(".txt").unwrap().path());
278    /// ```
279    pub fn with_suffix(suffix: impl AsRef<str>) -> Result<Self, std::io::Error> {
280        Self::internal_new(None, None, Some(suffix.as_ref()))
281    }
282
283    /// Write `contents` to the file.
284    ///
285    /// # Errors
286    /// Returns `Err` when it fails to write all of `contents` to the file.
287    #[allow(clippy::missing_panics_doc)]
288    pub fn with_contents(self, contents: &[u8]) -> Result<Self, std::io::Error> {
289        let path = self.path_buf.as_path();
290        std::fs::write(path, contents).map_err(|e| {
291            std::io::Error::new(e.kind(), format!("error writing file {path:?}: {e}"))
292        })?;
293        Ok(self)
294    }
295
296    /// Remove the file now.  Do nothing later on drop.
297    ///
298    /// # Errors
299    /// Returns an error if the file exists and we fail to remove it.
300    #[allow(clippy::missing_panics_doc)]
301    pub fn cleanup(mut self) -> Result<(), std::io::Error> {
302        let result = Self::remove_file(&self.path_buf);
303        if result.is_ok() {
304            self.delete_on_drop = false;
305        }
306        result
307    }
308
309    /// Make the struct panic on drop if it hits an error while
310    /// removing the file.
311    #[must_use]
312    pub fn panic_on_cleanup_error(mut self) -> Self {
313        self.panic_on_delete_err = true;
314        self
315    }
316
317    /// Do not delete the file.
318    ///
319    /// This is useful when debugging a test.
320    pub fn leak(mut self) {
321        self.delete_on_drop = false;
322    }
323
324    /// The path to the file.
325    #[must_use]
326    #[allow(clippy::missing_panics_doc)]
327    pub fn path(&self) -> &Path {
328        self.path_buf.as_path()
329    }
330}
331impl Drop for TempFile {
332    fn drop(&mut self) {
333        if self.delete_on_drop {
334            let result = Self::remove_file(self.path_buf.as_path());
335            if self.panic_on_delete_err {
336                if let Err(e) = result {
337                    panic!("{}", e);
338                }
339            }
340        }
341    }
342}
343impl AsRef<Path> for TempFile {
344    fn as_ref(&self) -> &Path {
345        self.path()
346    }
347}
348
349/// Create a new empty file in a system temporary directory.
350///
351/// # Panics
352/// Panics if it cannot create the file.
353#[must_use]
354pub fn empty() -> TempFile {
355    TempFile::new().unwrap()
356}
357
358/// Create a new  file in a system temporary directory
359/// and writes `contents` to it.
360///
361/// # Panics
362/// Panics if it fails to create the file or fails to write all of `contents`.
363#[must_use]
364pub fn with_contents(contents: &[u8]) -> TempFile {
365    TempFile::new().unwrap().with_contents(contents).unwrap()
366}