async_tempfile/
lib.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.
33#![allow(unsafe_code)]
34
35mod errors;
36
37pub use errors::Error;
38use std::borrow::{Borrow, BorrowMut};
39use std::fmt::{Debug, Formatter};
40use std::io::{IoSlice, SeekFrom};
41use std::mem::ManuallyDrop;
42use std::ops::{Deref, DerefMut};
43use std::path::PathBuf;
44use std::pin::Pin;
45use std::sync::Arc;
46use std::task::{Context, Poll};
47use tokio::fs::{File, OpenOptions};
48use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
49
50#[cfg(feature = "uuid")]
51use uuid::Uuid;
52
53const FILE_PREFIX: &'static str = "atmp_";
54
55/// A named temporary file that will be cleaned automatically
56/// after the last reference to it is dropped.
57pub struct TempFile {
58    /// A local reference to the file. Used to write to or read from the file.
59    file: ManuallyDrop<File>,
60
61    /// A shared pointer to the owned (or non-owned) file.
62    /// The `Arc` ensures that the enclosed file is kept alive
63    /// until all references to it are dropped.
64    core: ManuallyDrop<Arc<Box<TempFileCore>>>,
65}
66
67/// Determines the ownership of a temporary file.
68#[derive(Debug, Eq, PartialEq, Copy, Clone)]
69pub enum Ownership {
70    /// The file is owned by [`TempFile`] and will be deleted when
71    /// the last reference to it is dropped.
72    Owned,
73    /// The file is borrowed by [`TempFile`] and will be left untouched
74    /// when the last reference to it is dropped.
75    Borrowed,
76}
77
78/// The instance that tracks the temporary file.
79/// If dropped, the file will be deleted.
80struct TempFileCore {
81    /// The path of the contained file.
82    path: PathBuf,
83
84    /// Pointer to the file to keep it alive.
85    file: ManuallyDrop<File>,
86
87    /// A hacky approach to allow for "non-owned" files.
88    /// If set to `Ownership::Owned`, the file specified in `path` will be deleted
89    /// when this instance is dropped. If set to `Ownership::Borrowed`, the file will be kept.
90    ownership: Ownership,
91}
92
93impl TempFile {
94    /// Creates a new temporary file in the default location.
95    /// When the instance goes out of scope, the file will be deleted.
96    ///
97    /// ## Example
98    ///
99    /// ```
100    /// # use async_tempfile::{TempFile, Error};
101    /// # use tokio::fs;
102    /// # let _ = tokio_test::block_on(async {
103    /// let file = TempFile::new().await?;
104    ///
105    /// // The file exists.
106    /// let file_path = file.file_path().clone();
107    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
108    ///
109    /// // Deletes the file.
110    /// drop(file);
111    ///
112    /// // The file was removed.
113    /// assert!(fs::metadata(file_path).await.is_err());
114    /// # Ok::<(), Error>(())
115    /// # });
116    /// ```
117    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
118    #[cfg(feature = "uuid")]
119    pub async fn new() -> Result<Self, Error> {
120        Self::new_in(Self::default_dir()).await
121    }
122
123    /// Creates a new temporary file in the default location.
124    /// When the instance goes out of scope, the file will be deleted.
125    ///
126    /// ## Arguments
127    ///
128    /// * `name` - The name of the file to create in the default temporary directory.
129    ///
130    /// ## Example
131    ///
132    /// ```
133    /// # use async_tempfile::{TempFile, Error};
134    /// # use tokio::fs;
135    /// # let _ = tokio_test::block_on(async {
136    /// let file = TempFile::new_with_name("temporary.file").await?;
137    ///
138    /// // The file exists.
139    /// let file_path = file.file_path().clone();
140    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
141    ///
142    /// // Deletes the file.
143    /// drop(file);
144    ///
145    /// // The file was removed.
146    /// assert!(fs::metadata(file_path).await.is_err());
147    /// # Ok::<(), Error>(())
148    /// # });
149    /// ```
150    pub async fn new_with_name<N: AsRef<str>>(name: N) -> Result<Self, Error> {
151        Self::new_with_name_in(name, Self::default_dir()).await
152    }
153
154    /// Creates a new temporary file in the default location.
155    /// When the instance goes out of scope, the file will be deleted.
156    ///
157    /// ## Arguments
158    ///
159    /// * `uuid` - A UUID to use as a suffix to the file name.
160    ///
161    /// ## Example
162    ///
163    /// ```
164    /// # use async_tempfile::{TempFile, Error};
165    /// # use tokio::fs;
166    /// # let _ = tokio_test::block_on(async {
167    /// let id = uuid::Uuid::new_v4();
168    /// let file = TempFile::new_with_uuid(id).await?;
169    ///
170    /// // The file exists.
171    /// let file_path = file.file_path().clone();
172    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
173    ///
174    /// // Deletes the file.
175    /// drop(file);
176    ///
177    /// // The file was removed.
178    /// assert!(fs::metadata(file_path).await.is_err());
179    /// # Ok::<(), Error>(())
180    /// # });
181    /// ```
182    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
183    #[cfg(feature = "uuid")]
184    pub async fn new_with_uuid(uuid: Uuid) -> Result<Self, Error> {
185        Self::new_with_uuid_in(uuid, Self::default_dir()).await
186    }
187
188    /// Creates a new temporary file in the specified location.
189    /// When the instance goes out of scope, the file will be deleted.
190    ///
191    /// ## Arguments
192    ///
193    /// * `dir` - The directory to create the file in.
194    ///
195    /// ## Example
196    ///
197    /// ```
198    /// # use async_tempfile::{TempFile, Error};
199    /// # use tokio::fs;
200    /// # let _ = tokio_test::block_on(async {
201    /// let path = std::env::temp_dir();
202    /// let file = TempFile::new_in(path).await?;
203    ///
204    /// // The file exists.
205    /// let file_path = file.file_path().clone();
206    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
207    ///
208    /// // Deletes the file.
209    /// drop(file);
210    ///
211    /// // The file was removed.
212    /// assert!(fs::metadata(file_path).await.is_err());
213    /// # Ok::<(), Error>(())
214    /// # });
215    /// ```
216    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
217    #[cfg(feature = "uuid")]
218    pub async fn new_in<P: Borrow<PathBuf>>(dir: P) -> Result<Self, Error> {
219        let id = Uuid::new_v4();
220        return Self::new_with_uuid_in(id, dir).await;
221    }
222
223    /// Creates a new temporary file in the specified location.
224    /// When the instance goes out of scope, the file will be deleted.
225    ///
226    /// ## Arguments
227    ///
228    /// * `dir` - The directory to create the file in.
229    /// * `name` - The file name to use.
230    ///
231    /// ## Example
232    ///
233    /// ```
234    /// # use async_tempfile::{TempFile, Error};
235    /// # use tokio::fs;
236    /// # let _ = tokio_test::block_on(async {
237    /// let path = std::env::temp_dir();
238    /// let file = TempFile::new_with_name_in("temporary.file", path).await?;
239    ///
240    /// // The file exists.
241    /// let file_path = file.file_path().clone();
242    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
243    ///
244    /// // Deletes the file.
245    /// drop(file);
246    ///
247    /// // The file was removed.
248    /// assert!(fs::metadata(file_path).await.is_err());
249    /// # Ok::<(), Error>(())
250    /// # });
251    /// ```
252    pub async fn new_with_name_in<N: AsRef<str>, P: Borrow<PathBuf>>(
253        name: N,
254        dir: P,
255    ) -> Result<Self, Error> {
256        let dir = dir.borrow();
257        if !dir.is_dir() {
258            return Err(Error::InvalidDirectory);
259        }
260        let file_name = name.as_ref();
261        let mut path = dir.clone();
262        path.push(file_name);
263        Ok(Self::new_internal(path, Ownership::Owned).await?)
264    }
265
266    /// Creates a new temporary file in the specified location.
267    /// When the instance goes out of scope, the file will be deleted.
268    ///
269    /// ## Arguments
270    ///
271    /// * `dir` - The directory to create the file in.
272    /// * `uuid` - A UUID to use as a suffix to the file name.
273    ///
274    /// ## Example
275    ///
276    /// ```
277    /// # use async_tempfile::{TempFile, Error};
278    /// # use tokio::fs;
279    /// # let _ = tokio_test::block_on(async {
280    /// let path = std::env::temp_dir();
281    /// let id = uuid::Uuid::new_v4();
282    /// let file = TempFile::new_with_uuid_in(id, path).await?;
283    ///
284    /// // The file exists.
285    /// let file_path = file.file_path().clone();
286    /// assert!(fs::metadata(file_path.clone()).await.is_ok());
287    ///
288    /// // Deletes the file.
289    /// drop(file);
290    ///
291    /// // The file was removed.
292    /// assert!(fs::metadata(file_path).await.is_err());
293    /// # Ok::<(), Error>(())
294    /// # });
295    /// ```
296    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
297    #[cfg(feature = "uuid")]
298    pub async fn new_with_uuid_in<P: Borrow<PathBuf>>(uuid: Uuid, dir: P) -> Result<Self, Error> {
299        let file_name = format!("{}{}", FILE_PREFIX, uuid);
300        Self::new_with_name_in(file_name, dir).await
301    }
302
303    /// Wraps a new instance of this type around an existing file.
304    /// If `ownership` is set to [`Ownership::Borrowed`], this method does not take ownership of
305    /// the file, i.e. the file will not be deleted when the instance is dropped.
306    ///
307    /// ## Arguments
308    ///
309    /// * `path` - The path of the file to wrap.
310    /// * `ownership` - The ownership of the file.
311    pub async fn from_existing(path: PathBuf, ownership: Ownership) -> Result<Self, Error> {
312        if !path.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    async fn new_internal(path: PathBuf, ownership: Ownership) -> Result<Self, Error> {
376        let core = TempFileCore {
377            file: ManuallyDrop::new(
378                OpenOptions::new()
379                    .create(ownership == Ownership::Owned)
380                    .read(false)
381                    .write(true)
382                    .open(path.clone())
383                    .await?,
384            ),
385            ownership,
386            path: path.clone(),
387        };
388
389        let file = OpenOptions::new()
390            .read(true)
391            .write(true)
392            .open(path.clone())
393            .await?;
394        Ok(Self {
395            file: ManuallyDrop::new(file),
396            core: ManuallyDrop::new(Arc::new(Box::new(core))),
397        })
398    }
399
400    /// Gets the default temporary file directory.
401    #[inline(always)]
402    fn default_dir() -> PathBuf {
403        std::env::temp_dir()
404    }
405}
406
407/// Ensures the file handles are closed before the core reference is freed.
408/// If the core reference would be freed while handles are still open, it is
409/// possible that the underlying file cannot be deleted.
410impl Drop for TempFile {
411    fn drop(&mut self) {
412        // Ensure all file handles are closed before we attempt to delete the file itself via core.
413        drop(unsafe { ManuallyDrop::take(&mut self.file) });
414        drop(unsafe { ManuallyDrop::take(&mut self.core) });
415    }
416}
417
418/// Ensures that the underlying file is deleted if this is a owned instance.
419/// If the underlying file is not owned, this operation does nothing.
420impl Drop for TempFileCore {
421    fn drop(&mut self) {
422        // Ensure we don't drop borrowed files.
423        if self.ownership != Ownership::Owned {
424            return;
425        }
426
427        // Closing the file handle first, as otherwise the file might not be deleted.
428        drop(unsafe { ManuallyDrop::take(&mut self.file) });
429
430        // TODO: Use asynchronous variant if running in an async context.
431        // Note that if TempFile is used from the executor's handle,
432        //      this may block the executor itself.
433        let _ = std::fs::remove_file(&self.path);
434    }
435}
436
437impl Debug for TempFileCore {
438    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
439        write!(f, "{:?}", self.path)
440    }
441}
442
443impl Debug for TempFile {
444    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
445        write!(f, "{:?}", self.core)
446    }
447}
448
449/// Allows implicit treatment of TempFile as a File.
450impl Deref for TempFile {
451    type Target = File;
452
453    fn deref(&self) -> &Self::Target {
454        &self.file
455    }
456}
457
458/// Allows implicit treatment of TempFile as a mutable File.
459impl DerefMut for TempFile {
460    fn deref_mut(&mut self) -> &mut File {
461        &mut self.file
462    }
463}
464
465impl Borrow<File> for TempFile {
466    fn borrow(&self) -> &File {
467        &self.file
468    }
469}
470
471impl BorrowMut<File> for TempFile {
472    fn borrow_mut(&mut self) -> &mut File {
473        &mut self.file
474    }
475}
476
477impl AsRef<File> for TempFile {
478    fn as_ref(&self) -> &File {
479        &self.file
480    }
481}
482
483/// Forwarding AsyncWrite to the embedded File
484impl AsyncWrite for TempFile {
485    fn poll_write(
486        mut self: Pin<&mut Self>,
487        cx: &mut Context<'_>,
488        buf: &[u8],
489    ) -> Poll<Result<usize, std::io::Error>> {
490        Pin::new(self.file.deref_mut()).poll_write(cx, buf)
491    }
492
493    fn poll_flush(
494        mut self: Pin<&mut Self>,
495        cx: &mut Context<'_>,
496    ) -> Poll<Result<(), std::io::Error>> {
497        Pin::new(self.file.deref_mut()).poll_flush(cx)
498    }
499
500    fn poll_shutdown(
501        mut self: Pin<&mut Self>,
502        cx: &mut Context<'_>,
503    ) -> Poll<Result<(), std::io::Error>> {
504        Pin::new(self.file.deref_mut()).poll_shutdown(cx)
505    }
506
507    fn poll_write_vectored(
508        mut self: Pin<&mut Self>,
509        cx: &mut Context<'_>,
510        bufs: &[IoSlice<'_>],
511    ) -> Poll<Result<usize, std::io::Error>> {
512        Pin::new(self.file.deref_mut()).poll_write_vectored(cx, bufs)
513    }
514}
515
516/// Forwarding AsyncWrite to the embedded TempFile
517impl AsyncRead for TempFile {
518    fn poll_read(
519        mut self: Pin<&mut Self>,
520        cx: &mut Context<'_>,
521        buf: &mut ReadBuf<'_>,
522    ) -> Poll<std::io::Result<()>> {
523        Pin::new(self.file.deref_mut()).poll_read(cx, buf)
524    }
525}
526
527/// Forwarding AsyncSeek to the embedded File
528impl AsyncSeek for TempFile {
529    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> {
530        Pin::new(self.file.deref_mut()).start_seek(position)
531    }
532
533    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<u64>> {
534        Pin::new(self.file.deref_mut()).poll_complete(cx)
535    }
536}