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}