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