Skip to main content

mod_tempdir/
named_file.rs

1//! Temporary file management. See [`NamedTempFile`].
2//!
3//! Companion module to the crate root, which owns [`crate::TempDir`].
4//! Both types share the internal [`crate::unique_name`] generator so
5//! the `mod-rand` feature controls naming for files and directories
6//! in lockstep.
7
8use std::io;
9use std::path::{Path, PathBuf};
10
11use crate::unique_name;
12
13/// A temporary file that auto-deletes when dropped.
14///
15/// Companion to [`crate::TempDir`]. Where `TempDir` manages a
16/// directory, `NamedTempFile` manages a single zero-byte file at a
17/// fresh path under the OS temp location. The caller reopens the
18/// path with [`std::fs::OpenOptions`] (or any other API) when ready
19/// to write or read.
20///
21/// The default basename is `.tmpfile-{pid}-{name12}`, intentionally
22/// distinct from [`TempDir`](crate::TempDir)'s `.tmp-{pid}-{name12}`
23/// so an operator inspecting the OS temp location can tell the two
24/// apart at a glance. The 12 trailing characters use the same
25/// Crockford base32 generator as `TempDir`, so the optional
26/// `mod-rand` feature controls both types in lockstep. The embedded
27/// PID lets [`cleanup_orphans`](crate::cleanup_orphans) identify
28/// files left behind by crashed processes.
29///
30/// # Example
31///
32/// ```no_run
33/// use mod_tempdir::NamedTempFile;
34/// use std::io::Write;
35///
36/// let f = NamedTempFile::new().unwrap();
37/// let mut handle = std::fs::OpenOptions::new()
38///     .write(true)
39///     .open(f.path())
40///     .unwrap();
41/// handle.write_all(b"hello").unwrap();
42/// drop(handle);
43/// // `f` is deleted automatically when it goes out of scope.
44/// ```
45///
46/// # Cleanup semantics
47///
48/// Drop calls [`std::fs::remove_file`] best-effort. A failure (file
49/// already gone, permission denied, or a still-open handle on
50/// Windows) is intentionally swallowed: a `Drop` impl must not
51/// panic. Use [`NamedTempFile::persist`] to keep the file alive past
52/// drop.
53///
54/// # Windows handle-lock caveat
55///
56/// On Windows, [`std::fs::remove_file`] returns
57/// `ERROR_SHARING_VIOLATION` (surfaced in Rust as
58/// [`std::io::ErrorKind::PermissionDenied`]) if any process still
59/// holds an open handle to the file at the moment of Drop. The
60/// library does not retry. Drop must not block, and retries cannot
61/// force-close a caller-owned handle. The file is left on disk in
62/// that case. Close any handles you open against
63/// [`NamedTempFile::path`] before the `NamedTempFile` drops to
64/// guarantee cleanup.
65#[derive(Debug)]
66pub struct NamedTempFile {
67    path: PathBuf,
68    cleanup_on_drop: bool,
69}
70
71impl NamedTempFile {
72    /// Create a new temporary file in the system's temp location
73    /// (`/tmp` on Linux/macOS, `%TEMP%` on Windows).
74    ///
75    /// The basename is `.tmpfile-{pid}-{name12}` where `{pid}` is
76    /// the current process ID (used by
77    /// [`cleanup_orphans`](crate::cleanup_orphans) to identify
78    /// entries left behind by crashed processes) and `{name12}` is a
79    /// 12-character Crockford base32 string from the shared name
80    /// generator. The file is materialized via
81    /// [`std::fs::File::create`]; the returned `File` handle is
82    /// closed before this function returns, so the caller starts
83    /// from a clean slate.
84    ///
85    /// With the `mod-rand` feature enabled, the name fragment comes
86    /// from `mod_rand::tier2::unique_name`. Without it, from the
87    /// same internal process-unique mixer as
88    /// [`TempDir::new`](crate::TempDir::new).
89    ///
90    /// # Errors
91    ///
92    /// Returns the underlying [`io::Error`] from
93    /// [`std::fs::File::create`] if the file cannot be created.
94    ///
95    /// # Example
96    ///
97    /// ```no_run
98    /// use mod_tempdir::NamedTempFile;
99    ///
100    /// let f = NamedTempFile::new().unwrap();
101    /// assert!(f.path().is_file());
102    /// ```
103    pub fn new() -> io::Result<Self> {
104        let name = unique_name(12);
105        let pid = std::process::id();
106        let path = std::env::temp_dir().join(format!(".tmpfile-{pid}-{name}"));
107        std::fs::File::create(&path)?;
108        Ok(Self {
109            path,
110            cleanup_on_drop: true,
111        })
112    }
113
114    /// Create a new temporary file with the given prefix.
115    ///
116    /// The final basename is `{prefix}-{12-char-name}`. The prefix
117    /// is joined verbatim and is the caller's responsibility to
118    /// sanitize.
119    ///
120    /// # Errors
121    ///
122    /// Returns the underlying [`io::Error`] from
123    /// [`std::fs::File::create`] if the file cannot be created.
124    ///
125    /// # Example
126    ///
127    /// ```no_run
128    /// use mod_tempdir::NamedTempFile;
129    ///
130    /// let f = NamedTempFile::with_prefix("my-fixture").unwrap();
131    /// assert!(f
132    ///     .path()
133    ///     .file_name()
134    ///     .unwrap()
135    ///     .to_string_lossy()
136    ///     .starts_with("my-fixture-"));
137    /// ```
138    pub fn with_prefix(prefix: &str) -> io::Result<Self> {
139        let name = unique_name(12);
140        let path = std::env::temp_dir().join(format!("{prefix}-{name}"));
141        std::fs::File::create(&path)?;
142        Ok(Self {
143            path,
144            cleanup_on_drop: true,
145        })
146    }
147
148    /// Return the path of this temporary file.
149    pub fn path(&self) -> &Path {
150        &self.path
151    }
152
153    /// Consume this `NamedTempFile` and return the path, disabling
154    /// cleanup on drop. The file will persist.
155    ///
156    /// Use this when you want to inspect contents after a test
157    /// fails.
158    pub fn persist(mut self) -> PathBuf {
159        self.cleanup_on_drop = false;
160        self.path.clone()
161    }
162
163    /// Return `true` if the file will be deleted on drop.
164    pub fn cleanup_on_drop(&self) -> bool {
165        self.cleanup_on_drop
166    }
167}
168
169impl Drop for NamedTempFile {
170    fn drop(&mut self) {
171        if self.cleanup_on_drop {
172            // Cleanup is best-effort and must not panic in Drop.
173            // Filesystem errors (file in use on Windows, permission
174            // denied, file already gone) are intentionally swallowed
175            // per REPS section 5.
176            let _ = std::fs::remove_file(&self.path);
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn creates_file() {
187        let f = NamedTempFile::new().unwrap();
188        assert!(f.path().exists());
189        assert!(f.path().is_file());
190    }
191
192    #[test]
193    fn auto_cleanup() {
194        let path = {
195            let f = NamedTempFile::new().unwrap();
196            f.path().to_path_buf()
197        };
198        assert!(!path.exists());
199    }
200
201    #[test]
202    fn persist_disables_cleanup() {
203        let f = NamedTempFile::new().unwrap();
204        let path = f.persist();
205        assert!(path.exists());
206        std::fs::remove_file(&path).unwrap();
207    }
208
209    #[test]
210    fn with_prefix_works() {
211        let f = NamedTempFile::with_prefix("named").unwrap();
212        let name = f.path().file_name().unwrap().to_string_lossy();
213        assert!(name.starts_with("named-"));
214    }
215
216    #[test]
217    fn two_files_unique() {
218        let a = NamedTempFile::new().unwrap();
219        let b = NamedTempFile::new().unwrap();
220        assert_ne!(a.path(), b.path());
221    }
222}