Skip to main content

mod_tempdir/
lib.rs

1//! # mod-tempdir
2//!
3//! Temporary directory and file management for Rust. Auto-cleanup on
4//! Drop, collision-resistant naming, cross-platform paths.
5//!
6//! Two types and one orphan-cleanup function:
7//!
8//! * [`TempDir`]: a directory created under the OS temp location,
9//!   recursively deleted on Drop.
10//! * [`NamedTempFile`]: a single file created under the OS temp
11//!   location, deleted on Drop.
12//! * [`cleanup_orphans`]: sweeps the OS temp directory for entries
13//!   left behind by crashed processes and removes those that are
14//!   both PID-dead and older than a caller-supplied age threshold.
15//!
16//! Both types share the same name-generation pipeline, the same
17//! `with_prefix` / `persist` / `cleanup_on_drop` API shape, and the
18//! same silent best-effort Drop semantics.
19//!
20//! Designed as a `tempfile` replacement at MSRV 1.75. The default
21//! build has zero runtime dependencies outside `std`. An optional
22//! `mod-rand` feature swaps the built-in name mixer for
23//! `mod_rand::tier2::unique_name`, which produces a uniformly
24//! distributed name from a SplitMix + Stafford-finisher pipeline.
25//!
26//! ## Quick example
27//!
28//! ```no_run
29//! use mod_tempdir::{NamedTempFile, TempDir};
30//!
31//! let dir = TempDir::new().unwrap();
32//! // ... use dir.path() to do work ...
33//!
34//! let file = NamedTempFile::new().unwrap();
35//! // ... use file.path() to write into the file ...
36//!
37//! // Both are deleted automatically when they go out of scope.
38//! ```
39//!
40//! ## Feature flags
41//!
42//! * `mod-rand` (off by default): use [`mod_rand::tier2::unique_name`][mr-tier2]
43//!   for naming. The alphabet is Crockford base32 on both paths, so
44//!   any caller pattern-matching on the directory or file basename
45//!   keeps working unchanged when the feature is toggled. Applies to
46//!   both [`TempDir`] and [`NamedTempFile`].
47//!
48//! [mr-tier2]: https://docs.rs/mod-rand/latest/mod_rand/tier2/fn.unique_name.html
49//!
50//! To enable in `Cargo.toml`:
51//!
52//! ```toml
53//! mod-tempdir = { version = "0.9", features = ["mod-rand"] }
54//! ```
55//!
56//! ## Cleanup semantics
57//!
58//! `Drop::drop` removes the directory via
59//! [`std::fs::remove_dir_all`] (for [`TempDir`]) or the file via
60//! [`std::fs::remove_file`] (for [`NamedTempFile`]). Failures during
61//! cleanup (file in use, permission denied, network filesystem
62//! hiccup) are intentionally silent: a `Drop` impl must not panic.
63//! Use `persist()` to keep the entry alive past drop if you need to
64//! inspect it. See [`NamedTempFile`] for a Windows-specific note
65//! about open file handles.
66
67#![cfg_attr(docsrs, feature(doc_cfg))]
68#![warn(missing_docs)]
69#![warn(rust_2018_idioms)]
70
71mod cleanup;
72mod named_file;
73
74pub use cleanup::cleanup_orphans;
75pub use named_file::{NamedTempFile, PersistAtomicError};
76
77use std::io;
78use std::path::{Path, PathBuf};
79
80#[cfg(not(feature = "mod-rand"))]
81use std::sync::atomic::{AtomicU64, Ordering};
82#[cfg(not(feature = "mod-rand"))]
83use std::time::{SystemTime, UNIX_EPOCH};
84
85/// A temporary directory that auto-deletes when dropped.
86///
87/// # Example
88///
89/// ```no_run
90/// use mod_tempdir::TempDir;
91///
92/// let dir = TempDir::new().unwrap();
93/// let file_path = dir.path().join("test.txt");
94/// std::fs::write(&file_path, b"hello").unwrap();
95/// // dir and its contents are deleted at end of scope
96/// ```
97#[derive(Debug)]
98pub struct TempDir {
99    path: PathBuf,
100    cleanup_on_drop: bool,
101}
102
103impl TempDir {
104    /// Create a new temporary directory in the system's temp location
105    /// (`/tmp` on Linux/macOS, `%TEMP%` on Windows).
106    ///
107    /// The basename is `.tmp-{pid}-{name12}` where `{pid}` is the
108    /// current process ID (used by [`cleanup_orphans`] to identify
109    /// entries left behind by crashed processes) and `{name12}` is a
110    /// 12-character Crockford base32 string from the shared name
111    /// generator. With the `mod-rand` feature enabled, the name
112    /// fragment comes from [`mod_rand::tier2::unique_name`][mr-tier2];
113    /// without it, from an internal process-unique mixer.
114    ///
115    /// # Errors
116    ///
117    /// Returns the underlying [`io::Error`] from
118    /// [`std::fs::create_dir`] if the directory cannot be created.
119    ///
120    /// # Example
121    ///
122    /// ```no_run
123    /// use mod_tempdir::TempDir;
124    ///
125    /// let dir = TempDir::new().unwrap();
126    /// assert!(dir.path().is_dir());
127    /// ```
128    ///
129    /// [mr-tier2]: https://docs.rs/mod-rand/latest/mod_rand/tier2/fn.unique_name.html
130    pub fn new() -> io::Result<Self> {
131        let name = unique_name(12);
132        let pid = std::process::id();
133        let path = std::env::temp_dir().join(format!(".tmp-{pid}-{name}"));
134        std::fs::create_dir(&path)?;
135        Ok(Self {
136            path,
137            cleanup_on_drop: true,
138        })
139    }
140
141    /// Create a new temporary directory with the given prefix.
142    ///
143    /// The final basename is `{prefix}-{12-char-name}`. The prefix is
144    /// joined verbatim and is the caller's responsibility to sanitize.
145    ///
146    /// # Errors
147    ///
148    /// Returns the underlying [`io::Error`] from
149    /// [`std::fs::create_dir`] if the directory cannot be created.
150    ///
151    /// # Example
152    ///
153    /// ```no_run
154    /// use mod_tempdir::TempDir;
155    ///
156    /// let dir = TempDir::with_prefix("my-app").unwrap();
157    /// assert!(dir
158    ///     .path()
159    ///     .file_name()
160    ///     .unwrap()
161    ///     .to_string_lossy()
162    ///     .starts_with("my-app-"));
163    /// ```
164    pub fn with_prefix(prefix: &str) -> io::Result<Self> {
165        let name = unique_name(12);
166        let path = std::env::temp_dir().join(format!("{prefix}-{name}"));
167        std::fs::create_dir(&path)?;
168        Ok(Self {
169            path,
170            cleanup_on_drop: true,
171        })
172    }
173
174    /// Return the path of this temporary directory.
175    ///
176    /// # Example
177    ///
178    /// ```no_run
179    /// use mod_tempdir::TempDir;
180    ///
181    /// let dir = TempDir::new().unwrap();
182    /// let log = dir.path().join("output.log");
183    /// std::fs::write(&log, b"hello").unwrap();
184    /// ```
185    pub fn path(&self) -> &Path {
186        &self.path
187    }
188
189    /// Consume this `TempDir` and return the path, disabling cleanup
190    /// on drop. The directory and its contents will persist.
191    ///
192    /// Use this when you want to inspect contents after a test fails.
193    ///
194    /// # Example
195    ///
196    /// ```no_run
197    /// use mod_tempdir::TempDir;
198    ///
199    /// let dir = TempDir::new().unwrap();
200    /// let kept = dir.persist();
201    /// // `kept` survives past the original `dir` going out of scope.
202    /// # std::fs::remove_dir_all(&kept).unwrap();
203    /// ```
204    pub fn persist(mut self) -> PathBuf {
205        self.cleanup_on_drop = false;
206        self.path.clone()
207    }
208
209    /// Return `true` if the directory will be deleted on drop.
210    ///
211    /// # Example
212    ///
213    /// ```no_run
214    /// use mod_tempdir::TempDir;
215    ///
216    /// let dir = TempDir::new().unwrap();
217    /// assert!(dir.cleanup_on_drop());
218    /// ```
219    pub fn cleanup_on_drop(&self) -> bool {
220        self.cleanup_on_drop
221    }
222}
223
224impl Drop for TempDir {
225    fn drop(&mut self) {
226        if self.cleanup_on_drop {
227            // Cleanup is best-effort and must not panic in Drop. Any
228            // filesystem error (file in use, permission denied) is
229            // intentionally swallowed per REPS section 5.
230            let _ = std::fs::remove_dir_all(&self.path);
231        }
232    }
233}
234
235#[cfg(feature = "mod-rand")]
236#[inline]
237pub(crate) fn unique_name(len: usize) -> String {
238    mod_rand::tier2::unique_name(len)
239}
240
241#[cfg(not(feature = "mod-rand"))]
242pub(crate) fn unique_name(len: usize) -> String {
243    static COUNTER: AtomicU64 = AtomicU64::new(0);
244    const ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
245
246    let pid = std::process::id() as u64;
247    let nanos = SystemTime::now()
248        .duration_since(UNIX_EPOCH)
249        .map(|d| d.as_nanos() as u64)
250        .unwrap_or(0);
251    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
252
253    // Placeholder mixing. The `mod-rand` feature replaces this entire
254    // function with `mod_rand::tier2::unique_name`.
255    let mut state = pid.wrapping_mul(0x9E3779B97F4A7C15)
256        ^ nanos.wrapping_mul(0xBF58476D1CE4E5B9)
257        ^ counter.wrapping_mul(0x94D049BB133111EB);
258
259    let mut out = String::with_capacity(len);
260    while out.len() < len {
261        out.push(ALPHABET[(state & 31) as usize] as char);
262        state >>= 5;
263        if state == 0 {
264            state = nanos.wrapping_mul(counter.wrapping_add(1));
265        }
266    }
267    out
268}
269
270/// Internal test hook. **Not part of the stable public API.** This
271/// symbol exists only to let this crate's integration tests exercise
272/// the name generator without paying for a filesystem syscall per
273/// sample. External code must not call it; it may be renamed or
274/// removed in any release, including a patch.
275///
276/// Only compiled when the `mod-rand` feature is enabled, since that is
277/// the only test file that needs it.
278#[cfg(feature = "mod-rand")]
279#[doc(hidden)]
280pub fn __unique_name_for_tests(len: usize) -> String {
281    unique_name(len)
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn creates_dir() {
290        let dir = TempDir::new().unwrap();
291        assert!(dir.path().exists());
292        assert!(dir.path().is_dir());
293    }
294
295    #[test]
296    fn auto_cleanup() {
297        let path = {
298            let dir = TempDir::new().unwrap();
299            dir.path().to_path_buf()
300        };
301        // After drop, the directory should no longer exist.
302        assert!(!path.exists());
303    }
304
305    #[test]
306    fn persist_disables_cleanup() {
307        let dir = TempDir::new().unwrap();
308        let path = dir.persist();
309        assert!(path.exists());
310        // Clean up manually since persist was used.
311        std::fs::remove_dir_all(&path).unwrap();
312    }
313
314    #[test]
315    fn with_prefix_works() {
316        let dir = TempDir::with_prefix("test").unwrap();
317        let name = dir.path().file_name().unwrap().to_string_lossy();
318        assert!(name.starts_with("test-"));
319    }
320
321    #[test]
322    fn two_dirs_unique() {
323        let a = TempDir::new().unwrap();
324        let b = TempDir::new().unwrap();
325        assert_ne!(a.path(), b.path());
326    }
327}