async_tempfile/tempdir.rs
1#[cfg(not(feature = "uuid"))]
2use crate::RandomName;
3use crate::{AtomicOwnership, Error, Ownership, PersistError};
4use std::borrow::Borrow;
5use std::fmt::{Debug, Formatter};
6use std::io::ErrorKind;
7use std::ops::Deref;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10#[cfg(feature = "uuid")]
11use uuid::Uuid;
12
13pub(crate) const DIR_PREFIX: &str = "atmpd_";
14
15/// Maximum number of attempts to find a free name when creating a directory
16/// with a randomly generated, collision-resistant name.
17const MAX_NAME_ATTEMPTS: usize = 16;
18
19/// How the underlying directory should be created.
20#[derive(Copy, Clone, Eq, PartialEq)]
21enum DirCreateMode {
22 /// Create a brand-new directory, failing if it already exists. Used for
23 /// unpredictable, auto-generated names so two temporaries never share a dir.
24 Exclusive,
25 /// Create the directory and any missing parents (idempotent if it exists).
26 /// Used for user-supplied names, preserving historic behavior.
27 CreateAll,
28 /// Do not create; the directory is expected to already exist. Used by
29 /// `from_existing`.
30 OpenExisting,
31}
32
33/// A named temporary directory that will be cleaned automatically
34/// after the last reference to it is dropped.
35pub struct TempDir {
36 /// A local copy of the directory path. Unlike [`crate::TempFile`]'s file
37 /// handle there is no OS resource to release here, so field drop order does
38 /// not affect deletion; the field is laid out before `core` purely to mirror
39 /// the `TempFile` layout.
40 dir: PathBuf,
41
42 /// A shared pointer to the owned (or non-owned) directory.
43 /// The `Arc` ensures that the enclosed dir is kept alive
44 /// until all references to it are dropped.
45 core: Arc<TempDirCore>,
46}
47
48/// The instance that tracks the temporary directory.
49/// If dropped, the directory will be deleted.
50struct TempDirCore {
51 /// The path of the contained directory.
52 path: PathBuf,
53
54 /// Whether the directory specified in `path` is owned (and deleted on drop)
55 /// or merely borrowed. Stored atomically because it is read from `Drop`,
56 /// which must never block on a lock, and mutated by
57 /// `keep`/`persist`/`drop_async`.
58 ownership: AtomicOwnership,
59}
60
61impl TempDir {
62 /// Creates a new temporary directory in the default location.
63 /// When the instance goes out of scope, the directory will be deleted.
64 ///
65 /// ## Example
66 ///
67 /// ```
68 /// # use async_tempfile::{TempDir, Error};
69 /// # use tokio::fs;
70 /// # let _ = tokio_test::block_on(async {
71 /// let dir = TempDir::new().await?;
72 ///
73 /// // The file exists.
74 /// let dir_path = dir.dir_path().clone();
75 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
76 ///
77 /// // Deletes the directory.
78 /// drop(dir);
79 ///
80 /// // The directory was removed.
81 /// assert!(fs::metadata(dir_path).await.is_err());
82 /// # Ok::<(), Error>(())
83 /// # });
84 /// ```
85 pub async fn new() -> Result<Self, Error> {
86 Self::new_in(Self::default_dir()).await
87 }
88
89 /// Creates a new temporary directory in the default location.
90 /// When the instance goes out of scope, the directory will be deleted.
91 ///
92 /// ## Arguments
93 ///
94 /// * `name` - The name of the directory to create in the default temporary directory root.
95 ///
96 /// ## Example
97 ///
98 /// ```
99 /// # use async_tempfile::{TempDir, Error};
100 /// # use tokio::fs;
101 /// # let _ = tokio_test::block_on(async {
102 /// let dir = TempDir::new_with_name("new_with_name_example.dir").await?;
103 ///
104 /// // The directory exists.
105 /// let dir_path = dir.dir_path().clone();
106 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
107 ///
108 /// // Deletes the directory.
109 /// drop(dir);
110 ///
111 /// // The directory was removed.
112 /// assert!(fs::metadata(dir_path).await.is_err());
113 /// # Ok::<(), Error>(())
114 /// # });
115 /// ```
116 pub async fn new_with_name<N: AsRef<str>>(name: N) -> Result<Self, Error> {
117 Self::new_with_name_in(name, Self::default_dir()).await
118 }
119
120 /// Creates a new temporary directory in the default location.
121 /// When the instance goes out of scope, the directory will be deleted.
122 ///
123 /// ## Arguments
124 ///
125 /// * `uuid` - A UUID to use as a suffix to the directory name.
126 ///
127 /// ## Example
128 ///
129 /// ```
130 /// # use async_tempfile::{TempDir, Error};
131 /// # use tokio::fs;
132 /// # let _ = tokio_test::block_on(async {
133 /// let id = uuid::Uuid::new_v4();
134 /// let dir = TempDir::new_with_uuid(id).await?;
135 ///
136 /// // The directory exists.
137 /// let dir_path = dir.dir_path().clone();
138 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
139 ///
140 /// // Deletes the directory.
141 /// drop(dir);
142 ///
143 /// // The directory was removed.
144 /// assert!(fs::metadata(dir_path).await.is_err());
145 /// # Ok::<(), Error>(())
146 /// # });
147 /// ```
148 #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
149 #[cfg(feature = "uuid")]
150 pub async fn new_with_uuid(uuid: Uuid) -> Result<Self, Error> {
151 Self::new_with_uuid_in(uuid, Self::default_dir()).await
152 }
153
154 /// Creates a new temporary directory in the specified location.
155 /// When the instance goes out of scope, the directory will be deleted.
156 ///
157 /// The directory is created with a collision-resistant, unpredictable name
158 /// using an exclusive create, so it never reuses an existing directory.
159 ///
160 /// ## Crate Features
161 ///
162 /// * `uuid` - When the `uuid` crate feature is enabled, a random UUIDv4 is used to
163 /// generate the temporary directory name.
164 ///
165 /// ## Arguments
166 ///
167 /// * `dir` - The directory to create the directory in.
168 ///
169 /// ## Example
170 ///
171 /// ```
172 /// # use async_tempfile::{TempDir, Error};
173 /// # use tokio::fs;
174 /// # let _ = tokio_test::block_on(async {
175 /// let path = std::env::temp_dir();
176 /// let dir = TempDir::new_in(path).await?;
177 ///
178 /// // The directory exists.
179 /// let dir_path = dir.dir_path().clone();
180 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
181 ///
182 /// // Deletes the directory.
183 /// drop(dir);
184 ///
185 /// // The directory was removed.
186 /// assert!(fs::metadata(dir_path).await.is_err());
187 /// # Ok::<(), Error>(())
188 /// # });
189 pub async fn new_in<P: Borrow<Path>>(root_dir: P) -> Result<Self, Error> {
190 Self::create_with_affixes(root_dir.borrow(), DIR_PREFIX, "").await
191 }
192
193 /// Creates a new temporary directory in the specified location.
194 /// When the instance goes out of scope, the directory will be deleted.
195 ///
196 /// ## Arguments
197 ///
198 /// * `dir` - The root directory to create the directory in.
199 /// * `name` - The directory name to use.
200 ///
201 /// ## Example
202 ///
203 /// ```
204 /// # use async_tempfile::{TempDir, Error};
205 /// # use tokio::fs;
206 /// # let _ = tokio_test::block_on(async {
207 /// let path = std::env::temp_dir();
208 /// let dir = TempDir::new_with_name_in("new_with_name_in_example.dir", path).await?;
209 ///
210 /// // The directory exists.
211 /// let dir_path = dir.dir_path().clone();
212 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
213 ///
214 /// // Deletes the directory.
215 /// drop(dir);
216 ///
217 /// // The directory was removed.
218 /// assert!(fs::metadata(dir_path).await.is_err());
219 /// # Ok::<(), Error>(())
220 /// # });
221 /// ```
222 pub async fn new_with_name_in<N: AsRef<str>, P: Borrow<Path>>(
223 name: N,
224 root_dir: P,
225 ) -> Result<Self, Error> {
226 let root = root_dir.borrow();
227 if !crate::path_is_dir(root).await {
228 return Err(Error::InvalidDirectory);
229 }
230 let path = root.join(name.as_ref());
231 Self::new_internal(path, Ownership::Owned, DirCreateMode::CreateAll).await
232 }
233
234 /// Creates a new directory file in the specified location.
235 /// When the instance goes out of scope, the directory will be deleted.
236 ///
237 /// ## Arguments
238 ///
239 /// * `dir` - The root directory to create the directory in.
240 /// * `uuid` - A UUID to use as a suffix to the directory name.
241 ///
242 /// ## Example
243 ///
244 /// ```
245 /// # use async_tempfile::{TempDir, Error};
246 /// # use tokio::fs;
247 /// # let _ = tokio_test::block_on(async {
248 /// let path = std::env::temp_dir();
249 /// let id = uuid::Uuid::new_v4();
250 /// let dir = TempDir::new_with_uuid_in(id, path).await?;
251 ///
252 /// // The directory exists.
253 /// let dir_path = dir.dir_path().clone();
254 /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
255 ///
256 /// // Deletes the directory.
257 /// drop(dir);
258 ///
259 /// // The directory was removed.
260 /// assert!(fs::metadata(dir_path).await.is_err());
261 /// # Ok::<(), Error>(())
262 /// # });
263 /// ```
264 #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
265 #[cfg(feature = "uuid")]
266 pub async fn new_with_uuid_in<P: Borrow<Path>>(uuid: Uuid, root_dir: P) -> Result<Self, Error> {
267 let dir_name = format!("{DIR_PREFIX}{uuid}");
268 Self::new_with_name_in(dir_name, root_dir).await
269 }
270
271 /// Wraps a new instance of this type around an existing directory.
272 /// If `ownership` is set to [`Ownership::Borrowed`], this method does not take ownership of
273 /// the file, i.e. the directory will not be deleted when the instance is dropped.
274 ///
275 /// ## Arguments
276 ///
277 /// * `path` - The path of the directory to wrap.
278 /// * `ownership` - The ownership of the directory.
279 pub async fn from_existing(path: PathBuf, ownership: Ownership) -> Result<Self, Error> {
280 if !crate::path_is_dir(&path).await {
281 return Err(Error::InvalidDirectory);
282 }
283 Self::new_internal(path, ownership, DirCreateMode::OpenExisting).await
284 }
285
286 /// Creates a builder for configuring a new temporary directory
287 /// (prefix, suffix, root) before creating it.
288 ///
289 /// ## Example
290 ///
291 /// ```
292 /// # use async_tempfile::{TempDir, Error};
293 /// # let _ = tokio_test::block_on(async {
294 /// let dir = TempDir::builder()
295 /// .prefix("build_")
296 /// .create()
297 /// .await?;
298 ///
299 /// let name = dir.dir_path().file_name().unwrap().to_string_lossy().into_owned();
300 /// assert!(name.starts_with("build_"));
301 /// # Ok::<(), Error>(())
302 /// # });
303 /// ```
304 pub fn builder() -> crate::TempDirBuilder {
305 crate::TempDirBuilder::new()
306 }
307
308 /// Returns the path of the underlying temporary directory.
309 pub fn dir_path(&self) -> &PathBuf {
310 &self.core.path
311 }
312
313 /// Creates a new [`TempDir`] instance that shares the same underlying
314 /// directory as the existing [`TempDir`] instance. The directory is removed
315 /// once the last of the shared instances is dropped.
316 pub async fn try_clone(&self) -> Result<TempDir, Error> {
317 Ok(TempDir {
318 core: self.core.clone(),
319 dir: self.dir.clone(),
320 })
321 }
322
323 /// Determines the ownership of the temporary directory.
324 /// ### Example
325 /// ```
326 /// # use async_tempfile::{Ownership, TempDir};
327 /// # let _ = tokio_test::block_on(async {
328 /// let dir = TempDir::new().await?;
329 /// assert_eq!(dir.ownership(), Ownership::Owned);
330 /// # drop(dir);
331 /// # Ok::<(), Box<dyn std::error::Error>>(())
332 /// # });
333 /// ```
334 pub fn ownership(&self) -> Ownership {
335 self.core.ownership.get()
336 }
337
338 /// Disables automatic deletion and returns the path of the underlying
339 /// directory, turning the temporary directory into a permanent one.
340 ///
341 /// This affects every clone that shares the same underlying directory: none
342 /// of them will delete it when dropped.
343 ///
344 /// ## Example
345 ///
346 /// ```
347 /// # use async_tempfile::{TempDir, Error};
348 /// # use tokio::fs;
349 /// # let _ = tokio_test::block_on(async {
350 /// let dir = TempDir::new().await?;
351 /// let path = dir.keep();
352 ///
353 /// // The directory still exists after the handle is dropped.
354 /// assert!(fs::metadata(path.clone()).await.is_ok());
355 /// # fs::remove_dir_all(path).await.ok();
356 /// # Ok::<(), Error>(())
357 /// # });
358 /// ```
359 pub fn keep(self) -> PathBuf {
360 let path = self.core.path.clone();
361 self.core.ownership.set_borrowed();
362 path
363 }
364
365 /// Persists the temporary directory by moving it to `target`, returning the
366 /// new path. The directory will no longer be deleted automatically.
367 ///
368 /// The move is performed with [`tokio::fs::rename`] and therefore must stay
369 /// on the same filesystem (a cross-device move returns an error).
370 ///
371 /// On failure the temporary directory is **not** deleted: it is left at its
372 /// original location and that path is returned in [`PersistError::path`], so
373 /// no data is lost. The caller may re-wrap it with [`TempDir::from_existing`]
374 /// to restore automatic cleanup, or delete it.
375 ///
376 /// ## Arguments
377 ///
378 /// * `target` - The destination path to move the directory to.
379 pub async fn persist<P: AsRef<Path>>(self, target: P) -> Result<PathBuf, PersistError> {
380 let target = target.as_ref().to_path_buf();
381 match tokio::fs::rename(&self.core.path, &target).await {
382 Ok(()) => {
383 self.core.ownership.set_borrowed();
384 Ok(target)
385 }
386 Err(e) => {
387 // Preserve the caller's data: leave the directory in place and
388 // report where it is, rather than deleting it on the way out.
389 self.core.ownership.set_borrowed();
390 Err(PersistError {
391 error: Error::Io(e),
392 path: self.core.path.clone(),
393 })
394 }
395 }
396 }
397
398 /// Asynchronously drops the [`TempDir`] instance, removing the directory via
399 /// [`tokio::fs::remove_dir_all`] without blocking the runtime when this is
400 /// the last reference to an owned directory.
401 ///
402 /// The synchronous `Drop` remains armed as a backstop, so a cancelled or
403 /// panicking `drop_async` still cleans up the directory.
404 ///
405 /// ## Example
406 /// ```
407 /// # use async_tempfile::TempDir;
408 /// # let _ = tokio_test::block_on(async {
409 /// let dir = TempDir::new().await?;
410 ///
411 /// // Drop the directory asynchronously.
412 /// dir.drop_async().await;
413 ///
414 /// // The directory is now removed.
415 /// # Ok::<(), Box<dyn std::error::Error>>(())
416 /// # });
417 /// ```
418 pub async fn drop_async(self) {
419 let TempDir { dir, core } = self;
420 drop(dir);
421
422 let Some(core) = Arc::into_inner(core) else {
423 return;
424 };
425
426 if core.ownership.is_owned() {
427 // Still marked owned: a cancellation or panic at the await point
428 // leaves `core`'s synchronous `Drop` to delete the directory.
429 match tokio::fs::remove_dir_all(&core.path).await {
430 Ok(()) => core.ownership.set_borrowed(),
431 Err(e) if e.kind() == ErrorKind::NotFound => core.ownership.set_borrowed(),
432 // Leave armed: the synchronous `Drop` below retries removal.
433 Err(_) => {}
434 }
435 }
436
437 drop(core);
438 }
439
440 /// Closes the directory, removing it (and its contents) when this is the
441 /// last reference to an owned directory, and **returns the removal result**
442 /// so the caller can observe a failure - unlike the implicit `Drop`, which
443 /// has no way to report one. This is the synchronous sibling of
444 /// [`drop_async`](Self::drop_async); prefer `drop_async` inside an async
445 /// context to avoid blocking the runtime on the removal syscalls.
446 ///
447 /// If other clones still reference the directory, cleanup is left to them
448 /// and `Ok(())` is returned. On error the error is returned and no further
449 /// automatic removal is attempted (a synchronous retry of the same failing
450 /// syscall would be pointless). Because `remove_dir_all` is not atomic, a
451 /// failure may leave the directory partially emptied; the caller owns
452 /// whatever remains and may inspect, retry, or remove it.
453 ///
454 /// ## Example
455 ///
456 /// ```rust
457 /// # use async_tempfile::{TempDir, Error};
458 /// # let _ = tokio_test::block_on(async {
459 /// let dir = TempDir::new().await?;
460 /// let path = dir.dir_path().to_path_buf();
461 ///
462 /// dir.close()?; // Explicitly close, surfacing any removal error.
463 ///
464 /// assert!(!path.exists());
465 /// # Ok::<(), Error>(())
466 /// # });
467 /// ```
468 pub fn close(self) -> std::io::Result<()> {
469 let TempDir { dir, core } = self;
470 drop(dir);
471
472 // Only the sole owner removes the directory; otherwise the remaining
473 // references' `Drop` impls handle cleanup.
474 let Some(core) = Arc::into_inner(core) else {
475 return Ok(());
476 };
477
478 if core.ownership.is_owned() {
479 match std::fs::remove_dir_all(&core.path) {
480 Ok(()) => core.ownership.set_borrowed(),
481 Err(e) if e.kind() == ErrorKind::NotFound => core.ownership.set_borrowed(),
482 // Disarm and surface the error: leaving `core` armed would make
483 // the trailing `Drop` retry the identical syscall for nothing.
484 // Whatever remains is left for the caller to handle.
485 Err(e) => {
486 core.ownership.set_borrowed();
487 return Err(e);
488 }
489 }
490 }
491
492 Ok(())
493 }
494
495 /// Creates a directory named `{prefix}{random}{suffix}` with an
496 /// unpredictable, collision-resistant random core, using an exclusive create
497 /// and retrying on the (very unlikely) collision. Shared by `new_in` and
498 /// [`crate::TempDirBuilder`].
499 pub(crate) async fn create_with_affixes(
500 root: &Path,
501 prefix: &str,
502 suffix: &str,
503 ) -> Result<Self, Error> {
504 if !crate::path_is_dir(root).await {
505 return Err(Error::InvalidDirectory);
506 }
507 // Affixes are name fragments, not paths: a separator would let the
508 // composed name escape `root` (`../` traversal, or an absolute prefix
509 // replacing it via `Path::join`).
510 if !crate::affix_is_safe(prefix) || !crate::affix_is_safe(suffix) {
511 return Err(Error::InvalidAffix);
512 }
513 let mut last_err = None;
514 for _ in 0..MAX_NAME_ATTEMPTS {
515 let name = format!("{prefix}{}{suffix}", Self::random_core_name());
516 match Self::new_internal(root.join(name), Ownership::Owned, DirCreateMode::Exclusive)
517 .await
518 {
519 Ok(dir) => return Ok(dir),
520 Err(Error::Io(e)) if e.kind() == ErrorKind::AlreadyExists => {
521 last_err = Some(Error::Io(e));
522 }
523 Err(e) => return Err(e),
524 }
525 }
526 Err(last_err.unwrap_or(Error::InvalidDirectory))
527 }
528
529 /// Generates the unpredictable, collision-resistant random core of a name,
530 /// without any prefix or suffix.
531 fn random_core_name() -> String {
532 #[cfg(feature = "uuid")]
533 {
534 Uuid::new_v4().to_string()
535 }
536
537 #[cfg(not(feature = "uuid"))]
538 {
539 RandomName::new("").as_str().to_string()
540 }
541 }
542
543 async fn new_internal<P: Borrow<Path>>(
544 path: P,
545 ownership: Ownership,
546 mode: DirCreateMode,
547 ) -> Result<Self, Error> {
548 let path = path.borrow();
549
550 match mode {
551 DirCreateMode::Exclusive => tokio::fs::create_dir(path).await?,
552 DirCreateMode::CreateAll => tokio::fs::create_dir_all(path).await?,
553 DirCreateMode::OpenExisting => {}
554 }
555
556 let core = TempDirCore {
557 ownership: AtomicOwnership::new(ownership),
558 path: PathBuf::from(path),
559 };
560
561 Ok(Self {
562 dir: PathBuf::from(path),
563 core: Arc::new(core),
564 })
565 }
566
567 /// Gets the default temporary file directory.
568 #[inline(always)]
569 fn default_dir() -> PathBuf {
570 std::env::temp_dir()
571 }
572}
573
574/// Ensures that the underlying directory is deleted if this is an owned instance.
575/// If the underlying directory is not owned, this operation does nothing.
576impl Drop for TempDirCore {
577 fn drop(&mut self) {
578 // Ensure we don't drop borrowed directories. Read via the lock-free
579 // atomic: `Drop` may run on a runtime worker thread and must never block.
580 if !self.ownership.is_owned() {
581 return;
582 }
583
584 // Synchronous on purpose: `Drop` must not re-enter the async runtime.
585 // Using remove_dir_all to delete all content recursively. `drop_async`
586 // provides an async deletion path at an explicit await point.
587 let _ = std::fs::remove_dir_all(&self.path);
588 }
589}
590
591impl Debug for TempDirCore {
592 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
593 write!(f, "{:?}", self.path)
594 }
595}
596
597impl Debug for TempDir {
598 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
599 write!(f, "{:?}", self.core)
600 }
601}
602
603/// Allows implicit treatment of TempDir as a Path.
604impl Deref for TempDir {
605 type Target = Path;
606
607 fn deref(&self) -> &Self::Target {
608 &self.dir
609 }
610}
611
612impl Borrow<Path> for TempDir {
613 fn borrow(&self) -> &Path {
614 &self.dir
615 }
616}
617
618impl Borrow<Path> for &TempDir {
619 fn borrow(&self) -> &Path {
620 &self.dir
621 }
622}
623
624impl AsRef<Path> for TempDir {
625 fn as_ref(&self) -> &Path {
626 &self.dir
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::TempFile;
634
635 #[tokio::test]
636 async fn test_new() -> Result<(), Error> {
637 let dir = TempDir::new().await?;
638
639 // The directory exists.
640 let dir_path = dir.dir_path().clone();
641 assert!(tokio::fs::metadata(dir_path.clone()).await.is_ok());
642
643 // Deletes the directory.
644 drop(dir);
645
646 assert!(tokio::fs::metadata(dir_path).await.is_err());
647 Ok(())
648 }
649
650 #[tokio::test]
651 #[cfg(not(target_os = "windows"))]
652 async fn test_files_in_dir() -> Result<(), Error> {
653 let dir = TempDir::new().await?;
654 let file = TempFile::new_in(&dir).await?;
655 let file2 = TempFile::new_in(&dir).await?;
656
657 // The directory exists.
658 let dir_path = dir.dir_path().clone();
659 assert!(tokio::fs::metadata(dir_path.clone()).await.is_ok());
660
661 // The files exist.
662 let file_path = file.file_path().clone();
663 let file_path2 = file2.file_path().clone();
664 assert!(tokio::fs::metadata(file_path.clone()).await.is_ok());
665 assert!(tokio::fs::metadata(file_path2.clone()).await.is_ok());
666
667 // Deletes the directory.
668 drop(dir);
669
670 // The files are gone (even though they are still open).
671 // TODO: This may cause trouble on Windows as Windows locks files when open.
672 assert!(tokio::fs::metadata(file_path).await.is_err());
673 assert!(tokio::fs::metadata(file_path2).await.is_err());
674
675 // The directory is gone.
676 assert!(tokio::fs::metadata(dir_path).await.is_err());
677 Ok(())
678 }
679}