async_tempfile/
tempdir.rs

1#[cfg(not(feature = "uuid"))]
2use crate::RandomName;
3use crate::{Error, Ownership};
4use std::borrow::Borrow;
5use std::fmt::{Debug, Formatter};
6use std::mem::ManuallyDrop;
7use std::ops::Deref;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10#[cfg(feature = "uuid")]
11use uuid::Uuid;
12
13const DIR_PREFIX: &str = "atmpd_";
14
15/// A named temporary directory that will be cleaned automatically
16/// after the last reference to it is dropped.
17pub struct TempDir {
18    /// A local reference to the directory.
19    dir: ManuallyDrop<PathBuf>,
20
21    /// A shared pointer to the owned (or non-owned) directory.
22    /// The `Arc` ensures that the enclosed dir is kept alive
23    /// until all references to it are dropped.
24    core: ManuallyDrop<Arc<TempDirCore>>,
25}
26
27/// The instance that tracks the temporary file.
28/// If dropped, the file will be deleted.
29struct TempDirCore {
30    /// The path of the contained file.
31    path: PathBuf,
32
33    /// A hacky approach to allow for "non-owned" files.
34    /// If set to `Ownership::Owned`, the file specified in `path` will be deleted
35    /// when this instance is dropped. If set to `Ownership::Borrowed`, the file will be kept.
36    ownership: Ownership,
37}
38
39impl TempDir {
40    /// Creates a new temporary directory in the default location.
41    /// When the instance goes out of scope, the directory will be deleted.
42    ///
43    /// ## Example
44    ///
45    /// ```
46    /// # use async_tempfile::{TempDir, Error};
47    /// # use tokio::fs;
48    /// # let _ = tokio_test::block_on(async {
49    /// let dir = TempDir::new().await?;
50    ///
51    /// // The file exists.
52    /// let dir_path = dir.dir_path().clone();
53    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
54    ///
55    /// // Deletes the directory.
56    /// drop(dir);
57    ///
58    /// // The directory was removed.
59    /// assert!(fs::metadata(dir_path).await.is_err());
60    /// # Ok::<(), Error>(())
61    /// # });
62    /// ```
63    pub async fn new() -> Result<Self, Error> {
64        Self::new_in(Self::default_dir()).await
65    }
66
67    /// Creates a new temporary directory in the default location.
68    /// When the instance goes out of scope, the directory will be deleted.
69    ///
70    /// ## Arguments
71    ///
72    /// * `name` - The name of the directory to create in the default temporary directory root.
73    ///
74    /// ## Example
75    ///
76    /// ```
77    /// # use async_tempfile::{TempDir, Error};
78    /// # use tokio::fs;
79    /// # let _ = tokio_test::block_on(async {
80    /// let dir = TempDir::new_with_name("temporary.dir").await?;
81    ///
82    /// // The directory exists.
83    /// let dir_path = dir.dir_path().clone();
84    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
85    ///
86    /// // Deletes the directory.
87    /// drop(dir);
88    ///
89    /// // The directory was removed.
90    /// assert!(fs::metadata(dir_path).await.is_err());
91    /// # Ok::<(), Error>(())
92    /// # });
93    /// ```
94    pub async fn new_with_name<N: AsRef<str>>(name: N) -> Result<Self, Error> {
95        Self::new_with_name_in(name, Self::default_dir()).await
96    }
97
98    /// Creates a new temporary directory in the default location.
99    /// When the instance goes out of scope, the directory will be deleted.
100    ///
101    /// ## Arguments
102    ///
103    /// * `uuid` - A UUID to use as a suffix to the directory name.
104    ///
105    /// ## Example
106    ///
107    /// ```
108    /// # use async_tempfile::{TempDir, Error};
109    /// # use tokio::fs;
110    /// # let _ = tokio_test::block_on(async {
111    /// let id = uuid::Uuid::new_v4();
112    /// let dir = TempDir::new_with_uuid(id).await?;
113    ///
114    /// // The directory exists.
115    /// let dir_path = dir.dir_path().clone();
116    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
117    ///
118    /// // Deletes the directory.
119    /// drop(dir);
120    ///
121    /// // The directory was removed.
122    /// assert!(fs::metadata(dir_path).await.is_err());
123    /// # Ok::<(), Error>(())
124    /// # });
125    /// ```
126    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
127    #[cfg(feature = "uuid")]
128    pub async fn new_with_uuid(uuid: Uuid) -> Result<Self, Error> {
129        Self::new_with_uuid_in(uuid, Self::default_dir()).await
130    }
131
132    /// Creates a new temporary directory in the specified location.
133    /// When the instance goes out of scope, the directory will be deleted.
134    ///
135    /// ## Crate Features
136    ///
137    /// * `uuid` - When the `uuid` crate feature is enabled, a random UUIDv4 is used to
138    ///   generate the temporary directory name.
139    ///
140    /// ## Arguments
141    ///
142    /// * `dir` - The directory to create the directory in.
143    ///
144    /// ## Example
145    ///
146    /// ```
147    /// # use async_tempfile::{TempDir, Error};
148    /// # use tokio::fs;
149    /// # let _ = tokio_test::block_on(async {
150    /// let path = std::env::temp_dir();
151    /// let dir = TempDir::new_in(path).await?;
152    ///
153    /// // The directory exists.
154    /// let dir_path = dir.dir_path().clone();
155    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
156    ///
157    /// // Deletes the directory.
158    /// drop(dir);
159    ///
160    /// // The directory was removed.
161    /// assert!(fs::metadata(dir_path).await.is_err());
162    /// # Ok::<(), Error>(())
163    /// # });
164    pub async fn new_in<P: Borrow<Path>>(root_dir: P) -> Result<Self, Error> {
165        #[cfg(feature = "uuid")]
166        {
167            let id = Uuid::new_v4();
168            Self::new_with_uuid_in(id, root_dir).await
169        }
170
171        #[cfg(not(feature = "uuid"))]
172        {
173            let name = RandomName::new(DIR_PREFIX);
174            Self::new_with_name_in(name, root_dir).await
175        }
176    }
177
178    /// Creates a new temporary directory in the specified location.
179    /// When the instance goes out of scope, the directory will be deleted.
180    ///
181    /// ## Arguments
182    ///
183    /// * `dir` - The root directory to create the directory in.
184    /// * `name` - The directory name to use.
185    ///
186    /// ## Example
187    ///
188    /// ```
189    /// # use async_tempfile::{TempDir, Error};
190    /// # use tokio::fs;
191    /// # let _ = tokio_test::block_on(async {
192    /// let path = std::env::temp_dir();
193    /// let dir = TempDir::new_with_name_in("temporary.dir", path).await?;
194    ///
195    /// // The directory exists.
196    /// let dir_path = dir.dir_path().clone();
197    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
198    ///
199    /// // Deletes the directory.
200    /// drop(dir);
201    ///
202    /// // The directory was removed.
203    /// assert!(fs::metadata(dir_path).await.is_err());
204    /// # Ok::<(), Error>(())
205    /// # });
206    /// ```
207    pub async fn new_with_name_in<N: AsRef<str>, P: Borrow<Path>>(
208        name: N,
209        root_dir: P,
210    ) -> Result<Self, Error> {
211        let dir = root_dir.borrow();
212        if !dir.is_dir() {
213            return Err(Error::InvalidDirectory);
214        }
215        let file_name = name.as_ref();
216        let mut path = PathBuf::from(dir);
217        path.push(file_name);
218        Self::new_internal(path, Ownership::Owned).await
219    }
220
221    /// Creates a new directory file in the specified location.
222    /// When the instance goes out of scope, the directory will be deleted.
223    ///
224    /// ## Arguments
225    ///
226    /// * `dir` - The root directory to create the directory in.
227    /// * `uuid` - A UUID to use as a suffix to the directory name.
228    ///
229    /// ## Example
230    ///
231    /// ```
232    /// # use async_tempfile::{TempDir, Error};
233    /// # use tokio::fs;
234    /// # let _ = tokio_test::block_on(async {
235    /// let path = std::env::temp_dir();
236    /// let id = uuid::Uuid::new_v4();
237    /// let dir = TempDir::new_with_uuid_in(id, path).await?;
238    ///
239    /// // The directory exists.
240    /// let dir_path = dir.dir_path().clone();
241    /// assert!(fs::metadata(dir_path.clone()).await.is_ok());
242    ///
243    /// // Deletes the directory.
244    /// drop(dir);
245    ///
246    /// // The directory was removed.
247    /// assert!(fs::metadata(dir_path).await.is_err());
248    /// # Ok::<(), Error>(())
249    /// # });
250    /// ```
251    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
252    #[cfg(feature = "uuid")]
253    pub async fn new_with_uuid_in<P: Borrow<Path>>(uuid: Uuid, root_dir: P) -> Result<Self, Error> {
254        let file_name = format!("{}{}", DIR_PREFIX, uuid);
255        Self::new_with_name_in(file_name, root_dir).await
256    }
257
258    /// Wraps a new instance of this type around an existing directory.
259    /// If `ownership` is set to [`Ownership::Borrowed`], this method does not take ownership of
260    /// the file, i.e. the directory will not be deleted when the instance is dropped.
261    ///
262    /// ## Arguments
263    ///
264    /// * `path` - The path of the directory to wrap.
265    /// * `ownership` - The ownership of the directory.
266    pub async fn from_existing(path: PathBuf, ownership: Ownership) -> Result<Self, Error> {
267        if !path.is_dir() {
268            return Err(Error::InvalidDirectory);
269        }
270        Self::new_internal(path, ownership).await
271    }
272
273    /// Returns the path of the underlying temporary directory.
274    pub fn dir_path(&self) -> &PathBuf {
275        &self.core.path
276    }
277
278    /// Creates a new [`TempDir`] instance that shares the same underlying
279    /// file handle as the existing [`TempDir`] instance.
280    /// Reads, writes, and seeks will affect both [`TempDir`] instances simultaneously.
281    #[allow(dead_code)]
282    pub async fn try_clone(&self) -> Result<TempDir, Error> {
283        Ok(TempDir {
284            core: self.core.clone(),
285            dir: self.dir.clone(),
286        })
287    }
288
289    /// Determines the ownership of the temporary directory.
290    /// ### Example
291    /// ```
292    /// # use async_tempfile::{Ownership, TempDir};
293    /// # let _ = tokio_test::block_on(async {
294    /// let dir = TempDir::new().await?;
295    /// assert_eq!(dir.ownership(), Ownership::Owned);
296    /// # drop(dir);
297    /// # Ok::<(), Box<dyn std::error::Error>>(())
298    /// # });
299    /// ```
300    pub fn ownership(&self) -> Ownership {
301        self.core.ownership
302    }
303
304    /// Asynchronously drops the [`TempDir`] instance by moving the drop operation 
305    /// to a blocking thread, avoiding potential blocking of the async runtime.
306    ///
307    /// This method is useful in cases where manually handling the blocking drop 
308    /// within an async context is required.
309    ///
310    /// ## Example
311    /// ```
312    /// # use async_tempfile::TempDir;
313    /// # let _ = tokio_test::block_on(async {
314    /// let dir = TempDir::new().await?;
315    ///
316    /// // Drop the directory asynchronously.
317    /// dir.drop_async().await;
318    ///
319    /// // The directory is now removed.
320    /// # Ok::<(), Box<dyn std::error::Error>>(())
321    /// # });
322    /// ```
323    ///
324    /// Note: This function spawns a blocking task for the drop operation.
325    pub async fn drop_async(self) {
326        tokio::task::spawn_blocking(move || drop(self)).await.ok();
327    }
328    
329    async fn new_internal<P: Borrow<Path>>(path: P, ownership: Ownership) -> Result<Self, Error> {
330        // Create the directory and all its parents.
331        tokio::fs::create_dir_all(path.borrow()).await?;
332
333        let core = TempDirCore {
334            ownership,
335            path: PathBuf::from(path.borrow()),
336        };
337
338        Ok(Self {
339            dir: ManuallyDrop::new(PathBuf::from(path.borrow())),
340            core: ManuallyDrop::new(Arc::new(core)),
341        })
342    }
343
344    /// Gets the default temporary file directory.
345    #[inline(always)]
346    fn default_dir() -> PathBuf {
347        std::env::temp_dir()
348    }
349}
350
351/// Ensures the file handles are closed before the core reference is freed.
352/// If the core reference would be freed while handles are still open, it is
353/// possible that the underlying file cannot be deleted.
354impl Drop for TempDir {
355    fn drop(&mut self) {
356        // Ensure all directory handles are closed before we attempt to delete the directory itself via core.
357        drop(unsafe { ManuallyDrop::take(&mut self.dir) });
358        drop(unsafe { ManuallyDrop::take(&mut self.core) });
359    }
360}
361
362/// Ensures that the underlying directory is deleted if this is an owned instance.
363/// If the underlying directory is not owned, this operation does nothing.
364impl Drop for TempDirCore {
365    /// See also [`TempDirCore::close`].
366    fn drop(&mut self) {
367        // Ensure we don't drop borrowed directories.
368        if self.ownership != Ownership::Owned {
369            return;
370        }
371
372        // TODO: Use asynchronous variant if running in an async context.
373        // Note that if TempDir is used from the executor's handle,
374        //      this may block the executor itself.
375        // Using remove_dir_all to delete all content recursively.
376        let _ = std::fs::remove_dir_all(&self.path);
377    }
378}
379
380impl Debug for TempDirCore {
381    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
382        write!(f, "{:?}", self.path)
383    }
384}
385
386impl Debug for TempDir {
387    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
388        write!(f, "{:?}", self.core)
389    }
390}
391
392/// Allows implicit treatment of TempDir as a Path.
393impl Deref for TempDir {
394    type Target = Path;
395
396    fn deref(&self) -> &Self::Target {
397        &self.dir
398    }
399}
400
401impl Borrow<Path> for TempDir {
402    fn borrow(&self) -> &Path {
403        &self.dir
404    }
405}
406
407impl Borrow<Path> for &TempDir {
408    fn borrow(&self) -> &Path {
409        &self.dir
410    }
411}
412
413impl AsRef<Path> for TempDir {
414    fn as_ref(&self) -> &Path {
415        &self.dir
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::TempFile;
423
424    #[tokio::test]
425    async fn test_new() -> Result<(), Error> {
426        let dir = TempDir::new().await?;
427
428        // The directory exists.
429        let dir_path = dir.dir_path().clone();
430        assert!(tokio::fs::metadata(dir_path.clone()).await.is_ok());
431
432        // Deletes the directory.
433        drop(dir);
434
435        assert!(tokio::fs::metadata(dir_path).await.is_err());
436        Ok(())
437    }
438
439    #[tokio::test]
440    #[cfg(not(target_os = "windows"))]
441    async fn test_files_in_dir() -> Result<(), Error> {
442        let dir = TempDir::new().await?;
443        let file = TempFile::new_in(&dir).await?;
444        let file2 = TempFile::new_in(&dir).await?;
445
446        // The directory exists.
447        let dir_path = dir.dir_path().clone();
448        assert!(tokio::fs::metadata(dir_path.clone()).await.is_ok());
449
450        // The files exist.
451        let file_path = file.file_path().clone();
452        let file_path2 = file2.file_path().clone();
453        assert!(tokio::fs::metadata(file_path.clone()).await.is_ok());
454        assert!(tokio::fs::metadata(file_path2.clone()).await.is_ok());
455
456        // Deletes the directory.
457        drop(dir);
458
459        // The files are gone (even though they are still open).
460        // TODO: This may cause trouble on Windows as Windows locks files when open.
461        assert!(tokio::fs::metadata(file_path).await.is_err());
462        assert!(tokio::fs::metadata(file_path2).await.is_err());
463
464        // The directory is gone.
465        assert!(tokio::fs::metadata(dir_path).await.is_err());
466        Ok(())
467    }
468}