Skip to main content

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}