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}