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;
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    /// [mr-tier2]: https://docs.rs/mod-rand/latest/mod_rand/tier2/fn.unique_name.html
121    pub fn new() -> io::Result<Self> {
122        let name = unique_name(12);
123        let pid = std::process::id();
124        let path = std::env::temp_dir().join(format!(".tmp-{pid}-{name}"));
125        std::fs::create_dir(&path)?;
126        Ok(Self {
127            path,
128            cleanup_on_drop: true,
129        })
130    }
131
132    /// Create a new temporary directory with the given prefix.
133    ///
134    /// The final basename is `{prefix}-{12-char-name}`. The prefix is
135    /// joined verbatim and is the caller's responsibility to sanitize.
136    ///
137    /// # Errors
138    ///
139    /// Returns the underlying [`io::Error`] from
140    /// [`std::fs::create_dir`] if the directory cannot be created.
141    ///
142    /// # Example
143    ///
144    /// ```no_run
145    /// use mod_tempdir::TempDir;
146    ///
147    /// let dir = TempDir::with_prefix("my-app").unwrap();
148    /// assert!(dir
149    ///     .path()
150    ///     .file_name()
151    ///     .unwrap()
152    ///     .to_string_lossy()
153    ///     .starts_with("my-app-"));
154    /// ```
155    pub fn with_prefix(prefix: &str) -> io::Result<Self> {
156        let name = unique_name(12);
157        let path = std::env::temp_dir().join(format!("{prefix}-{name}"));
158        std::fs::create_dir(&path)?;
159        Ok(Self {
160            path,
161            cleanup_on_drop: true,
162        })
163    }
164
165    /// Return the path of this temporary directory.
166    pub fn path(&self) -> &Path {
167        &self.path
168    }
169
170    /// Consume this `TempDir` and return the path, disabling cleanup
171    /// on drop. The directory and its contents will persist.
172    ///
173    /// Use this when you want to inspect contents after a test fails.
174    pub fn persist(mut self) -> PathBuf {
175        self.cleanup_on_drop = false;
176        self.path.clone()
177    }
178
179    /// Return `true` if the directory will be deleted on drop.
180    pub fn cleanup_on_drop(&self) -> bool {
181        self.cleanup_on_drop
182    }
183}
184
185impl Drop for TempDir {
186    fn drop(&mut self) {
187        if self.cleanup_on_drop {
188            // Cleanup is best-effort and must not panic in Drop. Any
189            // filesystem error (file in use, permission denied) is
190            // intentionally swallowed per REPS section 5.
191            let _ = std::fs::remove_dir_all(&self.path);
192        }
193    }
194}
195
196#[cfg(feature = "mod-rand")]
197#[inline]
198pub(crate) fn unique_name(len: usize) -> String {
199    mod_rand::tier2::unique_name(len)
200}
201
202#[cfg(not(feature = "mod-rand"))]
203pub(crate) fn unique_name(len: usize) -> String {
204    static COUNTER: AtomicU64 = AtomicU64::new(0);
205    const ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
206
207    let pid = std::process::id() as u64;
208    let nanos = SystemTime::now()
209        .duration_since(UNIX_EPOCH)
210        .map(|d| d.as_nanos() as u64)
211        .unwrap_or(0);
212    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
213
214    // Placeholder mixing. The `mod-rand` feature replaces this entire
215    // function with `mod_rand::tier2::unique_name`.
216    let mut state = pid.wrapping_mul(0x9E3779B97F4A7C15)
217        ^ nanos.wrapping_mul(0xBF58476D1CE4E5B9)
218        ^ counter.wrapping_mul(0x94D049BB133111EB);
219
220    let mut out = String::with_capacity(len);
221    while out.len() < len {
222        out.push(ALPHABET[(state & 31) as usize] as char);
223        state >>= 5;
224        if state == 0 {
225            state = nanos.wrapping_mul(counter.wrapping_add(1));
226        }
227    }
228    out
229}
230
231/// Internal test hook. **Not part of the stable public API.** This
232/// symbol exists only to let this crate's integration tests exercise
233/// the name generator without paying for a filesystem syscall per
234/// sample. External code must not call it; it may be renamed or
235/// removed in any release, including a patch.
236///
237/// Only compiled when the `mod-rand` feature is enabled, since that is
238/// the only test file that needs it.
239#[cfg(feature = "mod-rand")]
240#[doc(hidden)]
241pub fn __unique_name_for_tests(len: usize) -> String {
242    unique_name(len)
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn creates_dir() {
251        let dir = TempDir::new().unwrap();
252        assert!(dir.path().exists());
253        assert!(dir.path().is_dir());
254    }
255
256    #[test]
257    fn auto_cleanup() {
258        let path = {
259            let dir = TempDir::new().unwrap();
260            dir.path().to_path_buf()
261        };
262        // After drop, the directory should no longer exist.
263        assert!(!path.exists());
264    }
265
266    #[test]
267    fn persist_disables_cleanup() {
268        let dir = TempDir::new().unwrap();
269        let path = dir.persist();
270        assert!(path.exists());
271        // Clean up manually since persist was used.
272        std::fs::remove_dir_all(&path).unwrap();
273    }
274
275    #[test]
276    fn with_prefix_works() {
277        let dir = TempDir::with_prefix("test").unwrap();
278        let name = dir.path().file_name().unwrap().to_string_lossy();
279        assert!(name.starts_with("test-"));
280    }
281
282    #[test]
283    fn two_dirs_unique() {
284        let a = TempDir::new().unwrap();
285        let b = TempDir::new().unwrap();
286        assert_ne!(a.path(), b.path());
287    }
288}