| 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 | |