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}