async_tempfile/
tempfile.rs

1//! # async-tempfile
2//!
3//! Provides the [`TempFile`] struct, an asynchronous wrapper based on `tokio::fs` for temporary
4//! files that will be automatically deleted when the last reference to the struct is dropped.
5//!
6//! ```
7//! use async_tempfile::TempFile;
8//!
9//! #[tokio::main]
10//! async fn main() {
11//!     let parent = TempFile::new().await.unwrap();
12//!
13//!     // The cloned reference will not delete the file when dropped.
14//!     {
15//!         let nested = parent.open_rw().await.unwrap();
16//!         assert_eq!(nested.file_path(), parent.file_path());
17//!         assert!(nested.file_path().is_file());
18//!     }
19//!
20//!     // The file still exists; it will be deleted when `parent` is dropped.
21//!     assert!(parent.file_path().is_file());
22//! }
23//! ```
24//!
25//! ## Features
26//!
27//! * `uuid` - (Default) Enables random file name generation based on the [`uuid`](https://crates.io/crates/uuid) crate.
28//!            Provides the `new` and `new_in`, as well as the `new_with_uuid*` group of methods.
29
30// Document crate features on docs.rs.
31#![cfg_attr(docsrs, feature(doc_cfg))]
32// Required for dropping the file.
33use std::borrow::{Borrow, BorrowMut};
34use std::fmt::{Debug, Formatter};
35use std::io::{IoSlice, SeekFrom};
36use std::mem::ManuallyDrop;
37use std::ops::{Deref, DerefMut};
38use std::path::{Path, PathBuf};
39use std::pin::Pin;
40use std::sync::Arc;
41use std::task::{Context, Poll};
42use tokio::fs::{File, OpenOptions};
43use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
44
45#[cfg(not(feature = "uuid"))]
46use crate::random_name::RandomName;
47use crate::Error;
48use crate::Ownership;
49#[cfg(feature = "uuid")]
50use uuid::Uuid;
51
52const FILE_PREFIX: &str = "atmp_";
53
54/// A named temporary file that will be cleaned automatically
55/// after the last reference to it is dropped.
56pub struct TempFile {
57    /// A local reference to the file. Used to write to or read from the file.
58    file: ManuallyDrop<File>,
59
60    /// A shared pointer to the owned (or non-owned) file.
61    /// The `Arc` ensures that the enclosed file is kept alive
62    /// until all references to it are dropped.
63    core: ManuallyDrop<Arc<TempFileCore>>,
64}
65
66/// The instance that tracks the temporary file.
67/// If dropped, the file will be deleted.
68struct TempFileCore {
69    /// The path of the contained file.
70    path: PathBuf,
71
72    /// Pointer to the file to keep it alive.
73    file: ManuallyDrop<File>,
74
75    /// A hacky approach to allow for "non-owned" files.
76    /// If set to `Ownership::Owned`, the file specified in `path` will be deleted
77    /// when this instance is dropped. If set to `Ownership::Borrowed`, the file will be kept.
78    ownership: Ownership,
79}
80
81impl TempFile {
82    /// Creates a new temporary file in the default location.
83    /// When the instance goes out of scope, the file will be deleted.
84    ///
85    /// ## Example
86    ///
87    /// ```
88    /// # use async_tempfile::{TempFile, Error};
89    /// # use tokio::fs;
90    /// # let _ = tokio_test::block_on(async {
91    /// let file = TempFile::new().await?;
92    ///
93    /// // The file exists.
94    /// let file_path = file.file_path().clone();
95    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
96    ///
97    /// // Deletes the file.
98    /// drop(file);
99    ///
100    /// // The file was removed.
101    /// assert!(fs::metadata(file_path).await.is_err());
102    /// # Ok::<(), Error>(())
103    /// # });
104    /// ```
105    pub async fn new() -> Result<Self, Error> {
106        Self::new_in(Self::default_dir()).await
107    }
108
109    /// Creates a new temporary file in the default location.
110    /// When the instance goes out of scope, the file will be deleted.
111    ///
112    /// ## Arguments
113    ///
114    /// * `name` - The name of the file to create in the default temporary directory.
115    ///
116    /// ## Example
117    ///
118    /// ```
119    /// # use async_tempfile::{TempFile, Error};
120    /// # use tokio::fs;
121    /// # let _ = tokio_test::block_on(async {
122    /// let file = TempFile::new_with_name("temporary.file").await?;
123    ///
124    /// // The file exists.
125    /// let file_path = file.file_path().clone();
126    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
127    ///
128    /// // Deletes the file.
129    /// drop(file);
130    ///
131    /// // The file was removed.
132    /// assert!(fs::metadata(file_path).await.is_err());
133    /// # Ok::<(), Error>(())
134    /// # });
135    /// ```
136    pub async fn new_with_name<N: AsRef<str>>(name: N) -> Result<Self, Error> {
137        Self::new_with_name_in(name, Self::default_dir()).await
138    }
139
140    /// Creates a new temporary file in the default location.
141    /// When the instance goes out of scope, the file will be deleted.
142    ///
143    /// ## Arguments
144    ///
145    /// * `uuid` - A UUID to use as a suffix to the file name.
146    ///
147    /// ## Example
148    ///
149    /// ```
150    /// # use async_tempfile::{TempFile, Error};
151    /// # use tokio::fs;
152    /// # let _ = tokio_test::block_on(async {
153    /// let id = uuid::Uuid::new_v4();
154    /// let file = TempFile::new_with_uuid(id).await?;
155    ///
156    /// // The file exists.
157    /// let file_path = file.file_path().clone();
158    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
159    ///
160    /// // Deletes the file.
161    /// drop(file);
162    ///
163    /// // The file was removed.
164    /// assert!(fs::metadata(file_path).await.is_err());
165    /// # Ok::<(), Error>(())
166    /// # });
167    /// ```
168    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
169    #[cfg(feature = "uuid")]
170    pub async fn new_with_uuid(uuid: Uuid) -> Result<Self, Error> {
171        Self::new_with_uuid_in(uuid, Self::default_dir()).await
172    }
173
174    /// Creates a new temporary file in the specified location.
175    /// When the instance goes out of scope, the file will be deleted.
176    ///
177    /// ## Crate Features
178    ///
179    /// * `uuid` - When the `uuid` crate feature is enabled, a random UUIDv4 is used to
180    ///   generate the temporary file name.
181    ///
182    /// ## Arguments
183    ///
184    /// * `dir` - The directory to create the file in.
185    ///
186    /// ## Example
187    ///
188    /// ```
189    /// # use async_tempfile::{TempFile, Error};
190    /// # use tokio::fs;
191    /// # let _ = tokio_test::block_on(async {
192    /// let path = std::env::temp_dir();
193    /// let file = TempFile::new_in(path).await?;
194    ///
195    /// // The file exists.
196    /// let file_path = file.file_path().clone();
197    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
198    ///
199    /// // Deletes the file.
200    /// drop(file);
201    ///
202    /// // The file was removed.
203    /// assert!(fs::metadata(file_path).await.is_err());
204    /// # Ok::<(), Error>(())
205    /// # });
206    pub async fn new_in<P: Borrow<Path>>(dir: P) -> Result<Self, Error> {
207        #[cfg(feature = "uuid")]
208        {
209            let id = Uuid::new_v4();
210            Self::new_with_uuid_in(id, dir).await
211        }
212
213        #[cfg(not(feature = "uuid"))]
214        {
215            let name = RandomName::new(FILE_PREFIX);
216            Self::new_with_name_in(name, dir).await
217        }
218    }
219
220    /// Creates a new temporary file in the specified location.
221    /// When the instance goes out of scope, the file will be deleted.
222    ///
223    /// ## Arguments
224    ///
225    /// * `dir` - The directory to create the file in.
226    /// * `name` - The file name to use.
227    ///
228    /// ## Example
229    ///
230    /// ```
231    /// # use async_tempfile::{TempFile, Error};
232    /// # use tokio::fs;
233    /// # let _ = tokio_test::block_on(async {
234    /// let path = std::env::temp_dir();
235    /// let file = TempFile::new_with_name_in("temporary.file", path).await?;
236    ///
237    /// // The file exists.
238    /// let file_path = file.file_path().clone();
239    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
240    ///
241    /// // Deletes the file.
242    /// drop(file);
243    ///
244    /// // The file was removed.
245    /// assert!(fs::metadata(file_path).await.is_err());
246    /// # Ok::<(), Error>(())
247    /// # });
248    /// ```
249    pub async fn new_with_name_in<N: AsRef<str>, P: Borrow<Path>>(
250        name: N,
251        dir: P,
252    ) -> Result<Self, Error> {
253        let dir = dir.borrow();
254        if !dir.is_dir() {
255            return Err(Error::InvalidDirectory);
256        }
257        let file_name = name.as_ref();
258        let mut path = PathBuf::from(dir);
259        path.push(file_name);
260        Self::new_internal(path, Ownership::Owned).await
261    }
262
263    /// Creates a new temporary file in the specified location.
264    /// When the instance goes out of scope, the file will be deleted.
265    ///
266    /// ## Arguments
267    ///
268    /// * `dir` - The directory to create the file in.
269    /// * `uuid` - A UUID to use as a suffix to the file name.
270    ///
271    /// ## Example
272    ///
273    /// ```
274    /// # use async_tempfile::{TempFile, Error};
275    /// # use tokio::fs;
276    /// # let _ = tokio_test::block_on(async {
277    /// let path = std::env::temp_dir();
278    /// let id = uuid::Uuid::new_v4();
279    /// let file = TempFile::new_with_uuid_in(id, path).await?;
280    ///
281    /// // The file exists.
282    /// let file_path = file.file_path().clone();
283    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
284    ///
285    /// // Deletes the file.
286    /// drop(file);
287    ///
288    /// // The file was removed.
289    /// assert!(fs::metadata(file_path).await.is_err());
290    /// # Ok::<(), Error>(())
291    /// # });
292    /// ```
293    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
294    #[cfg(feature = "uuid")]
295    pub async fn new_with_uuid_in<P: Borrow<Path>>(uuid: Uuid, dir: P) -> Result<Self, Error> {
296        let file_name = format!("{}{}", FILE_PREFIX, uuid);
297        Self::new_with_name_in(file_name, dir).await
298    }
299
300    /// Wraps a new instance of this type around an existing file.
301    /// If `ownership` is set to [`Ownership::Borrowed`], this method does not take ownership of
302    /// the file, i.e. the file will not be deleted when the instance is dropped.
303    ///
304    /// ## Arguments
305    ///
306    /// * `path` - The path of the file to wrap.
307    /// * `ownership` - The ownership of the file.
308    pub async fn from_existing<P: Borrow<Path>>(
309        path: P,
310        ownership: Ownership,
311    ) -> Result<Self, Error> {
312        if !path.borrow().is_file() {
313            return Err(Error::InvalidFile);
314        }
315        Self::new_internal(path, ownership).await
316    }
317
318    /// Returns the path of the underlying temporary file.
319    pub fn file_path(&self) -> &PathBuf {
320        &self.core.path
321    }
322
323    /// Opens a new TempFile instance in read-write mode.
324    pub async fn open_rw(&self) -> Result<TempFile, Error> {
325        let file = OpenOptions::new()
326            .read(true)
327            .write(true)
328            .open(&self.core.path)
329            .await?;
330        Ok(TempFile {
331            core: self.core.clone(),
332            file: ManuallyDrop::new(file),
333        })
334    }
335
336    /// Opens a new TempFile instance in read-only mode.
337    pub async fn open_ro(&self) -> Result<TempFile, Error> {
338        let file = OpenOptions::new()
339            .read(true)
340            .write(false)
341            .open(&self.core.path)
342            .await?;
343        Ok(TempFile {
344            core: self.core.clone(),
345            file: ManuallyDrop::new(file),
346        })
347    }
348
349    /// Creates a new TempFile instance that shares the same underlying
350    /// file handle as the existing TempFile instance.
351    /// Reads, writes, and seeks will affect both TempFile instances simultaneously.
352    #[allow(dead_code)]
353    pub async fn try_clone(&self) -> Result<TempFile, Error> {
354        Ok(TempFile {
355            core: self.core.clone(),
356            file: ManuallyDrop::new(self.file.try_clone().await?),
357        })
358    }
359
360    /// Determines the ownership of the temporary file.
361    /// ### Example
362    /// ```
363    /// # use async_tempfile::{Ownership, TempFile};
364    /// # let _ = tokio_test::block_on(async {
365    /// let file = TempFile::new().await?;
366    /// assert_eq!(file.ownership(), Ownership::Owned);
367    /// # drop(file);
368    /// # Ok::<(), Box<dyn std::error::Error>>(())
369    /// # });
370    /// ```
371    pub fn ownership(&self) -> Ownership {
372        self.core.ownership
373    }
374
375    /// Asynchronously drops the TempFile, ensuring any resources are properly released.
376    /// This is useful for explicitly managing the lifecycle of the TempFile
377    /// in an asynchronous context.
378    ///
379    /// ## Example
380    ///
381    /// ```rust
382    /// # use async_tempfile::{TempFile, Error};
383    /// # let _ = tokio_test::block_on(async {
384    /// let file = TempFile::new().await?;
385    /// let path = file.file_path().to_path_buf();
386    /// assert!(path.is_file());
387    ///
388    /// file.drop_async().await; // Explicitly drop the TempFile
389    ///
390    /// assert!(!path.exists());
391    /// # Ok::<(), Error>(())
392    /// # });
393    /// ```
394    pub async fn drop_async(self) {
395        tokio::task::spawn_blocking(move || drop(self)).await.ok();
396    }
397    
398    async fn new_internal<P: Borrow<Path>>(path: P, ownership: Ownership) -> Result<Self, Error> {
399        let path = path.borrow();
400
401        let core = TempFileCore {
402            file: ManuallyDrop::new(
403                OpenOptions::new()
404                    .create(ownership == Ownership::Owned)
405                    .read(false)
406                    .write(true)
407                    .open(path)
408                    .await?,
409            ),
410            ownership,
411            path: PathBuf::from(path),
412        };
413
414        let file = OpenOptions::new().read(true).write(true).open(path).await?;
415        Ok(Self {
416            file: ManuallyDrop::new(file),
417            core: ManuallyDrop::new(Arc::new(core)),
418        })
419    }
420
421    /// Gets the default temporary file directory.
422    #[inline(always)]
423    fn default_dir() -> PathBuf {
424        std::env::temp_dir()
425    }
426}
427
428/// Ensures the file handles are closed before the core reference is freed.
429/// If the core reference would be freed while handles are still open, it is
430/// possible that the underlying file cannot be deleted.
431impl Drop for TempFile {
432    fn drop(&mut self) {
433        // Ensure all file handles are closed before we attempt to delete the file itself via core.
434        drop(unsafe { ManuallyDrop::take(&mut self.file) });
435        drop(unsafe { ManuallyDrop::take(&mut self.core) });
436    }
437}
438
439/// Ensures that the underlying file is deleted if this is an owned instance.
440/// If the underlying file is not owned, this operation does nothing.
441impl Drop for TempFileCore {
442    fn drop(&mut self) {
443        // Ensure we don't drop borrowed files.
444        if self.ownership != Ownership::Owned {
445            return;
446        }
447
448        // Closing the file handle first, as otherwise the file might not be deleted.
449        drop(unsafe { ManuallyDrop::take(&mut self.file) });
450
451        // TODO: Use asynchronous variant if running in an async context.
452        // Note that if TempFile is used from the executor's handle,
453        //      this may block the executor itself.
454        let _ = std::fs::remove_file(&self.path);
455    }
456}
457
458impl Debug for TempFileCore {
459    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
460        write!(f, "{:?}", self.path)
461    }
462}
463
464impl Debug for TempFile {
465    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
466        write!(f, "{:?}", self.core)
467    }
468}
469
470/// Allows implicit treatment of TempFile as a File.
471impl Deref for TempFile {
472    type Target = File;
473
474    fn deref(&self) -> &Self::Target {
475        &self.file
476    }
477}
478
479/// Allows implicit treatment of TempFile as a mutable File.
480impl DerefMut for TempFile {
481    fn deref_mut(&mut self) -> &mut File {
482        &mut self.file
483    }
484}
485
486impl Borrow<File> for TempFile {
487    fn borrow(&self) -> &File {
488        &self.file
489    }
490}
491
492impl BorrowMut<File> for TempFile {
493    fn borrow_mut(&mut self) -> &mut File {
494        &mut self.file
495    }
496}
497
498impl AsRef<File> for TempFile {
499    fn as_ref(&self) -> &File {
500        &self.file
501    }
502}
503
504/// Forwarding AsyncWrite to the embedded File
505impl AsyncWrite for TempFile {
506    fn poll_write(
507        mut self: Pin<&mut Self>,
508        cx: &mut Context<'_>,
509        buf: &[u8],
510    ) -> Poll<Result<usize, std::io::Error>> {
511        Pin::new(self.file.deref_mut()).poll_write(cx, buf)
512    }
513
514    fn poll_flush(
515        mut self: Pin<&mut Self>,
516        cx: &mut Context<'_>,
517    ) -> Poll<Result<(), std::io::Error>> {
518        Pin::new(self.file.deref_mut()).poll_flush(cx)
519    }
520
521    fn poll_shutdown(
522        mut self: Pin<&mut Self>,
523        cx: &mut Context<'_>,
524    ) -> Poll<Result<(), std::io::Error>> {
525        Pin::new(self.file.deref_mut()).poll_shutdown(cx)
526    }
527
528    fn poll_write_vectored(
529        mut self: Pin<&mut Self>,
530        cx: &mut Context<'_>,
531        bufs: &[IoSlice<'_>],
532    ) -> Poll<Result<usize, std::io::Error>> {
533        Pin::new(self.file.deref_mut()).poll_write_vectored(cx, bufs)
534    }
535}
536
537/// Forwarding AsyncWrite to the embedded TempFile
538impl AsyncRead for TempFile {
539    fn poll_read(
540        mut self: Pin<&mut Self>,
541        cx: &mut Context<'_>,
542        buf: &mut ReadBuf<'_>,
543    ) -> Poll<std::io::Result<()>> {
544        Pin::new(self.file.deref_mut()).poll_read(cx, buf)
545    }
546}
547
548/// Forwarding AsyncSeek to the embedded File
549impl AsyncSeek for TempFile {
550    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> {
551        Pin::new(self.file.deref_mut()).start_seek(position)
552    }
553
554    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<u64>> {
555        Pin::new(self.file.deref_mut()).poll_complete(cx)
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use crate::random_name::RandomName;
563
564    #[test]
565    fn test_random_name() {
566        let name = RandomName::new(FILE_PREFIX);
567        assert!(name.as_ref().starts_with(FILE_PREFIX))
568    }
569}