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//! - [`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)]
102use core::sync::atomic::{AtomicU32, Ordering};
103use std::io::ErrorKind;
104use std::path::{Path, PathBuf};
105use std::sync::atomic::AtomicBool;
106
107#[doc(hidden)]
108pub static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0);
109#[doc(hidden)]
110pub 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)]
138pub struct TempDir {
139 path_buf: Option<PathBuf>,
140 panic_on_delete_err: bool,
141}
142impl 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}
260impl 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}
272impl AsRef<Path> for TempDir {
273 fn as_ref(&self) -> &Path {
274 self.path()
275 }
276}
277