Skip to main content

temp_dir/
lib.rs

1//! temp-dir
2//! ========
3//! [![crates.io version](https://img.shields.io/crates/v/temp-dir.svg)](https://crates.io/crates/temp-dir)
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-dir/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 `TempDir` struct.
9//!
10//! # Features
11//! - Makes a directory in a system temporary directory
12//! - Recursively deletes the directory and its contents on drop
13//! - Deletes symbolic links and does not follow them
14//! - Optional name prefix
15//! - Depends only on `std`
16//! - `forbid(unsafe_code)`
17//! - 100% test coverage
18//!
19//! # Limitations
20//! - Not security-hardened.
21//!   For example, directory and file names are predictable.
22//! - This crate uses
23//!   [`std::fs::remove_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html)
24//!   which may be unreliable on Windows.
25//!   See [rust#29497](https://github.com/rust-lang/rust/issues/29497) and
26//!   [`remove_dir_all`](https://crates.io/crates/remove_dir_all) crate.
27//!
28//! # Alternatives
29//! - [`tempdir`](https://crates.io/crates/tempdir)
30//!   - Unmaintained
31//!   - Popular and mature
32//!   - Heavy dependencies (rand, winapi)
33//! - [`tempfile`](https://crates.io/crates/tempfile)
34//!   - Popular and mature
35//!   - Contains `unsafe`, dependencies full of `unsafe`
36//!   - Heavy dependencies (libc, winapi, rand, etc.)
37//! - [`test_dir`](https://crates.io/crates/test_dir)
38//!   - Has a handy `TestDir` struct
39//!   - Incomplete documentation
40//! - [`temp_testdir`](https://crates.io/crates/temp_testdir)
41//!   - Incomplete documentation
42//! - [`mktemp`](https://crates.io/crates/mktemp)
43//!   - Sets directory mode 0700 on unix
44//!   - Contains `unsafe`
45//!   - No readme or online docs
46//!
47//! # Related Crates
48//! - [`temp-file`](https://crates.io/crates/temp-file)
49//!
50//! # Example
51//! ```rust
52//! use temp_dir::TempDir;
53//! let d = TempDir::new().unwrap();
54//! // Prints "/tmp/t1a9b-0".
55//! println!("{:?}", d.path());
56//! let f = d.child("file1");
57//! // Prints "/tmp/t1a9b-0/file1".
58//! println!("{:?}", f);
59//! std::fs::write(&f, b"abc").unwrap();
60//! assert_eq!(
61//!     "abc",
62//!     std::fs::read_to_string(&f).unwrap(),
63//! );
64//! // Prints "/tmp/t1a9b-1".
65//! println!(
66//!     "{:?}", TempDir::new().unwrap().path());
67//! ```
68//!
69//! # Cargo Geiger Safety Report
70//! # Changelog
71//! - v0.2.0 - Implement `AsRef<Path>` on `&TempDir` not `TempDir`, to eliminate footgun.
72//! - v0.1.16 - `dont_delete_on_drop()`.  Thanks to [A L Manning](https://gitlab.com/A-Manning) for [discussion](https://gitlab.com/leonhard-llc/ops/-/merge_requests/5).
73//! - v0.1.15 - Remove a dev dependency.
74//! - v0.1.14 - `AsRef<Path>`
75//! - v0.1.13 - Update docs.
76//! - v0.1.12 - Work when the directory already exists.
77//! - v0.1.11
78//!   - Return `std::io::Error` instead of `String`.
79//!   - Add
80//!     [`cleanup`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.cleanup).
81//! - v0.1.10 - Implement `Eq`, `Ord`, `Hash`
82//! - v0.1.9 - Increase test coverage
83//! - v0.1.8 - Add [`leak`](https://docs.rs/temp-dir/latest/temp_dir/struct.TempDir.html#method.leak).
84//! - v0.1.7 - Update docs:
85//!   Warn about `std::fs::remove_dir_all` being unreliable on Windows.
86//!   Warn about predictable directory and file names.
87//!   Thanks to Reddit user
88//!   [burntsushi](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/gruo5iu/).
89//! - v0.1.6 - Add
90//!   [`TempDir::panic_on_cleanup_error`](https://docs.rs/temp-dir/latest/temp_dir/struct.TempDir.html#method.panic_on_cleanup_error).
91//!   Thanks to Reddit users
92//!   [`KhorneLordOfChaos`](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/grsb5s3/)
93//!   and
94//!   [`dpc_pw`](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/gru26df/)
95//!   for their comments.
96//! - v0.1.5 - Explain how it handles symbolic links.
97//!   Thanks to Reddit user Mai4eeze for this
98//!   [idea](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/grsoz2g/).
99//! - v0.1.4 - Update docs
100//! - v0.1.3 - Minor code cleanup, update docs
101//! - v0.1.2 - Update docs
102//! - v0.1.1 - Fix license
103//! - v0.1.0 - Initial version
104#![forbid(unsafe_code)]
105#![allow(clippy::unnecessary_debug_formatting)]
106use core::sync::atomic::{AtomicU32, Ordering};
107use std::io::ErrorKind;
108use std::path::{Path, PathBuf};
109use std::sync::atomic::AtomicBool;
110
111#[doc(hidden)]
112pub static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0);
113#[doc(hidden)]
114pub static INTERNAL_RETRY: AtomicBool = AtomicBool::new(true);
115
116/// The path of an existing writable directory in a system temporary directory.
117///
118/// Drop the struct to delete the directory and everything under it.
119/// Deletes symbolic links and does not follow them.
120///
121/// Ignores any error while deleting.
122/// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error).
123///
124/// # Example
125/// ```rust
126/// use temp_dir::TempDir;
127/// let d = TempDir::new().unwrap();
128/// // Prints "/tmp/t1a9b-0".
129/// println!("{:?}", d.path());
130/// let f = d.child("file1");
131/// // Prints "/tmp/t1a9b-0/file1".
132/// println!("{:?}", f);
133/// std::fs::write(&f, b"abc").unwrap();
134/// assert_eq!(
135///     "abc",
136///     std::fs::read_to_string(&f).unwrap(),
137/// );
138/// // Prints "/tmp/t1a9b-1".
139/// println!("{:?}", TempDir::new().unwrap().path());
140/// ```
141#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)]
142pub struct TempDir {
143    delete_on_drop: bool,
144    panic_on_delete_err: bool,
145    path_buf: PathBuf,
146}
147impl TempDir {
148    fn remove_dir(path: &Path) -> Result<(), std::io::Error> {
149        match std::fs::remove_dir_all(path) {
150            Ok(()) => Ok(()),
151            Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
152            Err(e) => Err(std::io::Error::new(
153                e.kind(),
154                format!("error removing directory and contents {path:?}: {e}"),
155            )),
156        }
157    }
158
159    /// Create a new empty directory in a system temporary directory.
160    ///
161    /// Drop the struct to delete the directory and everything under it.
162    /// Deletes symbolic links and does not follow them.
163    ///
164    /// Ignores any error while deleting.
165    /// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error).
166    ///
167    /// # Errors
168    /// Returns `Err` when it fails to create the directory.
169    ///
170    /// # Example
171    /// ```rust
172    /// // Prints "/tmp/t1a9b-0".
173    /// println!("{:?}", temp_dir::TempDir::new().unwrap().path());
174    /// ```
175    pub fn new() -> Result<Self, std::io::Error> {
176        // Prefix with 't' to avoid name collisions with `temp-file` crate.
177        Self::with_prefix("t")
178    }
179
180    /// Create a new empty directory in a system temporary directory.
181    /// Use `prefix` as the first part of the directory's name.
182    ///
183    /// Drop the struct to delete the directory and everything under it.
184    /// Deletes symbolic links and does not follow them.
185    ///
186    /// Ignores any error while deleting.
187    /// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error).
188    ///
189    /// # Errors
190    /// Returns `Err` when it fails to create the directory.
191    ///
192    /// # Example
193    /// ```rust
194    /// // Prints "/tmp/ok1a9b-0".
195    /// println!("{:?}", temp_dir::TempDir::with_prefix("ok").unwrap().path());
196    /// ```
197    pub fn with_prefix(prefix: impl AsRef<str>) -> Result<Self, std::io::Error> {
198        loop {
199            let path_buf = std::env::temp_dir().join(format!(
200                "{}{:x}-{:x}",
201                prefix.as_ref(),
202                std::process::id(),
203                INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel),
204            ));
205            match std::fs::create_dir(&path_buf) {
206                Err(e)
207                    if e.kind() == ErrorKind::AlreadyExists
208                        && INTERNAL_RETRY.load(Ordering::Acquire) => {}
209                Err(e) => {
210                    return Err(std::io::Error::new(
211                        e.kind(),
212                        format!("error creating directory {path_buf:?}: {e}"),
213                    ))
214                }
215                Ok(()) => {
216                    return Ok(Self {
217                        delete_on_drop: true,
218                        panic_on_delete_err: false,
219                        path_buf,
220                    })
221                }
222            }
223        }
224    }
225
226    /// Remove the directory and its contents now.
227    ///
228    /// # Errors
229    /// Returns an error if the directory exists and we fail to remove it and its contents.
230    #[allow(clippy::missing_panics_doc)]
231    pub fn cleanup(self) -> Result<(), std::io::Error> {
232        Self::remove_dir(&self.path_buf)
233    }
234
235    /// Make the struct panic on drop if it hits an error while
236    /// removing the directory or its contents.
237    #[must_use]
238    pub fn panic_on_cleanup_error(mut self) -> Self {
239        self.panic_on_delete_err = true;
240        self
241    }
242
243    /// Do not delete the directory or its contents.
244    ///
245    /// This is useful when debugging a test.
246    pub fn leak(mut self) {
247        self.delete_on_drop = false;
248    }
249
250    /// Do not delete the directory or its contents on Drop.
251    #[must_use]
252    pub fn dont_delete_on_drop(mut self) -> Self {
253        self.delete_on_drop = false;
254        self
255    }
256
257    /// The path to the directory.
258    #[must_use]
259    #[allow(clippy::missing_panics_doc)]
260    pub fn path(&self) -> &Path {
261        &self.path_buf
262    }
263
264    /// The path to `name` under the directory.
265    #[must_use]
266    #[allow(clippy::missing_panics_doc)]
267    pub fn child(&self, name: impl AsRef<str>) -> PathBuf {
268        let mut result = self.path_buf.clone();
269        result.push(name.as_ref());
270        result
271    }
272}
273impl Drop for TempDir {
274    fn drop(&mut self) {
275        if self.delete_on_drop {
276            let result = Self::remove_dir(&self.path_buf);
277            if self.panic_on_delete_err {
278                if let Err(e) = result {
279                    panic!("{}", e);
280                }
281            }
282        }
283    }
284}
285impl AsRef<Path> for &TempDir {
286    fn as_ref(&self) -> &Path {
287        self.path()
288    }
289}