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}