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}