1 | //! temp-dir |
2 | //! ======== |
3 | //! [](https://crates.io/crates/temp-dir) |
4 | //! [](https://gitlab.com/leonhard-llc/ops/-/raw/main/temp-dir/LICENSE) |
5 | //! [](https://github.com/rust-secure-code/safety-dance/) |
6 | //! [](https://gitlab.com/leonhard-llc/ops/-/pipelines) |
7 | //! |
8 | //! Provides a `TempDir` struct. |
9 | //! |
10 | //! # Features |
11 | //! - Makes a directory in a system temporary directory |
12 | //! - Recursively deletes the directory and its contents on drop |
13 | //! - Deletes symbolic links and does not follow them |
14 | //! - Optional name prefix |
15 | //! - Depends only on `std` |
16 | //! - `forbid(unsafe_code)` |
17 | //! - 100% test coverage |
18 | //! |
19 | //! # Limitations |
20 | //! - Not security-hardened. |
21 | //! For example, directory and file names are predictable. |
22 | //! - This crate uses |
23 | //! [`std::fs::remove_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html) |
24 | //! which may be unreliable on Windows. |
25 | //! See [rust#29497](https://github.com/rust-lang/rust/issues/29497) and |
26 | //! [`remove_dir_all`](https://crates.io/crates/remove_dir_all) crate. |
27 | //! |
28 | //! # Alternatives |
29 | //! - [`tempdir`](https://crates.io/crates/tempdir) |
30 | //! - Unmaintained |
31 | //! - Popular and mature |
32 | //! - Heavy dependencies (rand, winapi) |
33 | //! - [`tempfile`](https://crates.io/crates/tempfile) |
34 | //! - Popular and mature |
35 | //! - Contains `unsafe`, dependencies full of `unsafe` |
36 | //! - Heavy dependencies (libc, winapi, rand, etc.) |
37 | //! - [`test_dir`](https://crates.io/crates/test_dir) |
38 | //! - Has a handy `TestDir` struct |
39 | //! - Incomplete documentation |
40 | //! - [`temp_testdir`](https://crates.io/crates/temp_testdir) |
41 | //! - Incomplete documentation |
42 | //! - [`mktemp`](https://crates.io/crates/mktemp) |
43 | //! - Sets directory mode 0700 on unix |
44 | //! - Contains `unsafe` |
45 | //! - No readme or online docs |
46 | //! |
47 | //! # Related Crates |
48 | //! - [`temp-file`](https://crates.io/crates/temp-file) |
49 | //! |
50 | //! # Example |
51 | //! ```rust |
52 | //! use temp_dir::TempDir; |
53 | //! let d = TempDir::new().unwrap(); |
54 | //! // Prints "/tmp/t1a9b-0". |
55 | //! println!("{:?}" , d.path()); |
56 | //! let f = d.child("file1" ); |
57 | //! // Prints "/tmp/t1a9b-0/file1". |
58 | //! println!("{:?}" , f); |
59 | //! std::fs::write(&f, b"abc" ).unwrap(); |
60 | //! assert_eq!( |
61 | //! "abc" , |
62 | //! std::fs::read_to_string(&f).unwrap(), |
63 | //! ); |
64 | //! // Prints "/tmp/t1a9b-1". |
65 | //! println!( |
66 | //! "{:?}" , TempDir::new().unwrap().path()); |
67 | //! ``` |
68 | //! |
69 | //! # Cargo Geiger Safety Report |
70 | //! # Changelog |
71 | //! - v0.1.14 - `AsRef<Path>` |
72 | //! - v0.1.13 - Update docs. |
73 | //! - v0.1.12 - Work when the directory already exists. |
74 | //! - v0.1.11 |
75 | //! - Return `std::io::Error` instead of `String`. |
76 | //! - Add |
77 | //! [`cleanup`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.cleanup). |
78 | //! - v0.1.10 - Implement `Eq`, `Ord`, `Hash` |
79 | //! - v0.1.9 - Increase test coverage |
80 | //! - v0.1.8 - Add [`leak`](https://docs.rs/temp-dir/latest/temp_dir/struct.TempDir.html#method.leak). |
81 | //! - v0.1.7 - Update docs: |
82 | //! Warn about `std::fs::remove_dir_all` being unreliable on Windows. |
83 | //! Warn about predictable directory and file names. |
84 | //! Thanks to Reddit user |
85 | //! [burntsushi](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/gruo5iu/). |
86 | //! - v0.1.6 - Add |
87 | //! [`TempDir::panic_on_cleanup_error`](https://docs.rs/temp-dir/latest/temp_dir/struct.TempDir.html#method.panic_on_cleanup_error). |
88 | //! Thanks to Reddit users |
89 | //! [`KhorneLordOfChaos`](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/grsb5s3/) |
90 | //! and |
91 | //! [`dpc_pw`](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/gru26df/) |
92 | //! for their comments. |
93 | //! - v0.1.5 - Explain how it handles symbolic links. |
94 | //! Thanks to Reddit user Mai4eeze for this |
95 | //! [idea](https://www.reddit.com/r/rust/comments/ma6y0x/tempdir_simple_temporary_directory_with_cleanup/grsoz2g/). |
96 | //! - v0.1.4 - Update docs |
97 | //! - v0.1.3 - Minor code cleanup, update docs |
98 | //! - v0.1.2 - Update docs |
99 | //! - v0.1.1 - Fix license |
100 | //! - v0.1.0 - Initial version |
101 | #![forbid (unsafe_code)] |
102 | use core::sync::atomic::{AtomicU32, Ordering}; |
103 | use std::io::ErrorKind; |
104 | use std::path::{Path, PathBuf}; |
105 | use std::sync::atomic::AtomicBool; |
106 | |
107 | #[doc (hidden)] |
108 | pub static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0); |
109 | #[doc (hidden)] |
110 | pub static INTERNAL_RETRY: AtomicBool = AtomicBool::new(true); |
111 | |
112 | /// The path of an existing writable directory in a system temporary directory. |
113 | /// |
114 | /// Drop the struct to delete the directory and everything under it. |
115 | /// Deletes symbolic links and does not follow them. |
116 | /// |
117 | /// Ignores any error while deleting. |
118 | /// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error). |
119 | /// |
120 | /// # Example |
121 | /// ```rust |
122 | /// use temp_dir::TempDir; |
123 | /// let d = TempDir::new().unwrap(); |
124 | /// // Prints "/tmp/t1a9b-0". |
125 | /// println!("{:?}" , d.path()); |
126 | /// let f = d.child("file1" ); |
127 | /// // Prints "/tmp/t1a9b-0/file1". |
128 | /// println!("{:?}" , f); |
129 | /// std::fs::write(&f, b"abc" ).unwrap(); |
130 | /// assert_eq!( |
131 | /// "abc" , |
132 | /// std::fs::read_to_string(&f).unwrap(), |
133 | /// ); |
134 | /// // Prints "/tmp/t1a9b-1". |
135 | /// println!("{:?}" , TempDir::new().unwrap().path()); |
136 | /// ``` |
137 | #[derive (Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)] |
138 | pub struct TempDir { |
139 | path_buf: Option<PathBuf>, |
140 | panic_on_delete_err: bool, |
141 | } |
142 | impl TempDir { |
143 | fn remove_dir(path: &Path) -> Result<(), std::io::Error> { |
144 | match std::fs::remove_dir_all(path) { |
145 | Ok(()) => Ok(()), |
146 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), |
147 | Err(e) => Err(std::io::Error::new( |
148 | e.kind(), |
149 | format!("error removing directory and contents {path:?}: {e}" ), |
150 | )), |
151 | } |
152 | } |
153 | |
154 | /// Create a new empty directory in a system temporary directory. |
155 | /// |
156 | /// Drop the struct to delete the directory and everything under it. |
157 | /// Deletes symbolic links and does not follow them. |
158 | /// |
159 | /// Ignores any error while deleting. |
160 | /// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error). |
161 | /// |
162 | /// # Errors |
163 | /// Returns `Err` when it fails to create the directory. |
164 | /// |
165 | /// # Example |
166 | /// ```rust |
167 | /// // Prints "/tmp/t1a9b-0". |
168 | /// println!("{:?}" , temp_dir::TempDir::new().unwrap().path()); |
169 | /// ``` |
170 | pub fn new() -> Result<Self, std::io::Error> { |
171 | // Prefix with 't' to avoid name collisions with `temp-file` crate. |
172 | Self::with_prefix("t" ) |
173 | } |
174 | |
175 | /// Create a new empty directory in a system temporary directory. |
176 | /// Use `prefix` as the first part of the directory's name. |
177 | /// |
178 | /// Drop the struct to delete the directory and everything under it. |
179 | /// Deletes symbolic links and does not follow them. |
180 | /// |
181 | /// Ignores any error while deleting. |
182 | /// See [`TempDir::panic_on_cleanup_error`](struct.TempDir.html#method.panic_on_cleanup_error). |
183 | /// |
184 | /// # Errors |
185 | /// Returns `Err` when it fails to create the directory. |
186 | /// |
187 | /// # Example |
188 | /// ```rust |
189 | /// // Prints "/tmp/ok1a9b-0". |
190 | /// println!("{:?}" , temp_dir::TempDir::with_prefix("ok" ).unwrap().path()); |
191 | /// ``` |
192 | pub fn with_prefix(prefix: impl AsRef<str>) -> Result<Self, std::io::Error> { |
193 | loop { |
194 | let path_buf = std::env::temp_dir().join(format!( |
195 | " {}{:x}- {:x}" , |
196 | prefix.as_ref(), |
197 | std::process::id(), |
198 | INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), |
199 | )); |
200 | match std::fs::create_dir(&path_buf) { |
201 | Err(e) |
202 | if e.kind() == ErrorKind::AlreadyExists |
203 | && INTERNAL_RETRY.load(Ordering::Acquire) => {} |
204 | Err(e) => { |
205 | return Err(std::io::Error::new( |
206 | e.kind(), |
207 | format!("error creating directory {path_buf:?}: {e}" ), |
208 | )) |
209 | } |
210 | Ok(()) => { |
211 | return Ok(Self { |
212 | path_buf: Some(path_buf), |
213 | panic_on_delete_err: false, |
214 | }) |
215 | } |
216 | } |
217 | } |
218 | } |
219 | |
220 | /// Remove the directory and its contents now. |
221 | /// |
222 | /// # Errors |
223 | /// Returns an error if the directory exists and we fail to remove it and its contents. |
224 | #[allow (clippy::missing_panics_doc)] |
225 | pub fn cleanup(mut self) -> Result<(), std::io::Error> { |
226 | Self::remove_dir(&self.path_buf.take().unwrap()) |
227 | } |
228 | |
229 | /// Make the struct panic on drop if it hits an error while |
230 | /// removing the directory or its contents. |
231 | #[must_use ] |
232 | pub fn panic_on_cleanup_error(mut self) -> Self { |
233 | self.panic_on_delete_err = true; |
234 | self |
235 | } |
236 | |
237 | /// Do not delete the directory or its contents. |
238 | /// |
239 | /// This is useful when debugging a test. |
240 | pub fn leak(mut self) { |
241 | self.path_buf.take(); |
242 | } |
243 | |
244 | /// The path to the directory. |
245 | #[must_use ] |
246 | #[allow (clippy::missing_panics_doc)] |
247 | pub fn path(&self) -> &Path { |
248 | self.path_buf.as_ref().unwrap() |
249 | } |
250 | |
251 | /// The path to `name` under the directory. |
252 | #[must_use ] |
253 | #[allow (clippy::missing_panics_doc)] |
254 | pub fn child(&self, name: impl AsRef<str>) -> PathBuf { |
255 | let mut result = self.path_buf.as_ref().unwrap().clone(); |
256 | result.push(name.as_ref()); |
257 | result |
258 | } |
259 | } |
260 | impl Drop for TempDir { |
261 | fn drop(&mut self) { |
262 | if let Some(path: PathBuf) = self.path_buf.take() { |
263 | let result: Result<(), Error> = Self::remove_dir(&path); |
264 | if self.panic_on_delete_err { |
265 | if let Err(e: Error) = result { |
266 | panic!("{}" , e); |
267 | } |
268 | } |
269 | } |
270 | } |
271 | } |
272 | impl AsRef<Path> for TempDir { |
273 | fn as_ref(&self) -> &Path { |
274 | self.path() |
275 | } |
276 | } |
277 | |