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}