tmpdir/
lib.rs

1#![warn(
2    missing_debug_implementations,
3    missing_docs,
4    rust_2018_idioms,
5    unreachable_pub,
6    non_snake_case,
7    non_upper_case_globals
8)]
9#![deny(rustdoc::broken_intra_doc_links)]
10#![allow(clippy::cognitive_complexity)]
11//! # TmpDir
12//!
13//! Useful to create temp directories and copying their contents on completion
14//! of some action. Tmp dirs will be created using [`env::temp_dir`] with
15//! some random characters prefixed to prevent a name clash
16//!
17//! `copy` will traverse recursively through a directory and copy all file
18//! contents to some destination dir. It will not follow symlinks.
19//!
20//! ## Example
21//!
22//! ```rust,no_run
23//! # #[tokio::main]
24//! # async fn main() -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
25//! use tmpdir::TmpDir;
26//! use tokio::{fs, io::AsyncWriteExt};
27//! # let tmp = TmpDir::new("foo").await.unwrap();
28//!
29//! # let tmp_dir = tmp.as_ref().to_path_buf();
30//! # fs::create_dir(tmp_dir.clone().join("dir1")).await.unwrap();
31//! # let mut file = fs::File::create(tmp_dir.clone().join("dir1").join("file1"))
32//! #    .await
33//! #    .unwrap();
34//! # file.write_all(b"foo").await.unwrap();
35//! # let mut file = fs::File::create(tmp_dir.clone().join("file2"))
36//! #    .await
37//! #    .unwrap();
38//! # file.write_all(b"foo").await.unwrap();
39//! let new_tmp = TmpDir::new("bar").await.unwrap();
40//! new_tmp.copy(tmp.as_ref()).await;
41//! new_tmp.close().await; // not necessary to explicitly call
42//! # tmp.close().await;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! [`env::temp_dir`]: std::env::temp_dir
48
49use futures::stream::{self, Stream, StreamExt};
50use rand::{distributions::Alphanumeric, Rng};
51use tokio::fs::{self, DirEntry};
52
53use std::{
54    env, fmt, io,
55    path::{Path, PathBuf},
56};
57
58/// A temporary directory with a randomly generated prefix that will
59/// automatically delete it's contents on drop
60#[derive(Debug)]
61pub struct TmpDir {
62    inner: PathBuf,
63}
64
65const LEN_RNG: usize = 10;
66
67impl TmpDir {
68    /// create a new temp dir in `env::temp_dir` with a prefix. ex. `/tmp/prefix-<random chars>`
69    pub async fn new(prefix: impl AsRef<str>) -> io::Result<Self> {
70        let mut inner = env::temp_dir();
71        let s: String = {
72            // shrink scope of rng
73            let rng = rand::thread_rng();
74            rng.sample_iter(Alphanumeric)
75                .map(char::from)
76                .take(LEN_RNG)
77                .collect()
78        };
79
80        inner.push(&format!("{}-{}", s, prefix.as_ref()));
81
82        fs::create_dir(&inner).await?;
83        Ok(Self { inner })
84    }
85
86    /// return inner path as `PathBuf`
87    pub fn to_path_buf(&self) -> PathBuf {
88        self.as_ref().to_owned()
89    }
90
91    /// list the contents of a directory, if we encounter another dir
92    /// push to our traversal list
93    async fn list_contents(
94        path: PathBuf,
95        to_visit: &mut Vec<PathBuf>,
96    ) -> io::Result<Vec<DirEntry>> {
97        let mut dir = fs::read_dir(path).await?;
98        let mut files = Vec::new();
99
100        while let Some(child) = dir.next_entry().await? {
101            if child.metadata().await?.is_dir() {
102                to_visit.push(child.path());
103                files.push(child);
104            } else {
105                files.push(child)
106            }
107        }
108
109        Ok(files)
110    }
111
112    /// returns a stream of `DirEntry` representing the flattened list of
113    /// files that are traversable from the entry path
114    fn traverse(
115        path: impl Into<PathBuf>,
116    ) -> impl Stream<Item = io::Result<DirEntry>> + Send + 'static {
117        stream::unfold(vec![path.into()], |mut to_visit| async {
118            let path = to_visit.pop()?;
119            let file_stream = match TmpDir::list_contents(path, &mut to_visit).await {
120                Ok(files) => stream::iter(files).map(Ok).left_stream(),
121                Err(e) => stream::once(async { Err(e) }).right_stream(),
122            };
123
124            Some((file_stream, to_visit))
125        })
126        .flatten()
127    }
128
129    /// recursively copies contents from tmp dir to another
130    pub async fn copy(&self, dest_dir: impl AsRef<Path>) -> io::Result<()> {
131        // create dest dir if it doesn't exist
132        fs::create_dir_all(dest_dir.as_ref()).await?;
133
134        let files = TmpDir::traverse(self.inner.clone());
135        tokio::pin!(files);
136
137        while let Some(file) = files.next().await {
138            let file = file?;
139            let base_path = self.inner.to_path_buf();
140            let file_path = file.path();
141
142            // get common base path
143            let diff = file_path
144                .strip_prefix(&base_path)
145                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid dir"))?;
146
147            let dest = dest_dir.as_ref().to_path_buf().join(diff);
148
149            if file.metadata().await?.is_dir() {
150                fs::create_dir_all(dest).await?;
151            } else {
152                fs::copy(file.path(), dest).await?;
153            }
154        }
155        Ok(())
156    }
157
158    /// close the tmp dir and nuke it's contents
159    pub async fn close(&self) -> io::Result<()> {
160        fs::remove_dir_all(&self.inner).await
161    }
162}
163
164/// deletes itself after dropped
165impl Drop for TmpDir {
166    fn drop(&mut self) {
167        let path = self.inner.clone();
168        tokio::spawn(async move {
169            let _ = fs::remove_dir_all(path).await;
170        });
171    }
172}
173
174impl AsRef<Path> for TmpDir {
175    fn as_ref(&self) -> &Path {
176        self.inner.as_ref()
177    }
178}
179
180impl fmt::Display for TmpDir {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "TmpDir {{ path: {:#?} }}", self.inner.display())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use tokio::{
190        io::AsyncWriteExt,
191        time::{self, Duration},
192    };
193
194    #[tokio::test]
195    async fn test_tmp_create() {
196        let tmp = TmpDir::new("foo").await.unwrap();
197        let metadata = fs::metadata(tmp.as_ref()).await;
198        // file exists
199        assert!(metadata.is_ok());
200
201        tmp.close().await.unwrap();
202    }
203
204    #[tokio::test]
205    async fn test_tmp_drop() {
206        let path: PathBuf;
207        {
208            let tmp = TmpDir::new("foo").await.unwrap();
209            path = tmp.as_ref().to_owned();
210            let metadata = fs::metadata(tmp.as_ref()).await;
211            // file exists
212            assert!(metadata.is_ok());
213            drop(tmp);
214        }
215        time::sleep(Duration::from_secs(1)).await;
216        let result = fs::metadata(&path).await;
217        assert!(result.is_err());
218    }
219
220    #[tokio::test]
221    async fn test_tmp_copy() {
222        let tmp = TmpDir::new("foo").await.unwrap();
223        let metadata = fs::metadata(tmp.as_ref()).await;
224        assert!(metadata.is_ok());
225
226        let tmp_dir = tmp.as_ref().to_path_buf();
227        fs::create_dir(tmp_dir.clone().join("dir1")).await.unwrap();
228        let mut file = fs::File::create(tmp_dir.clone().join("dir1").join("file1"))
229            .await
230            .unwrap();
231        file.write_all(b"foo").await.unwrap();
232        let mut file = fs::File::create(tmp_dir.clone().join("file2"))
233            .await
234            .unwrap();
235        file.write_all(b"foo").await.unwrap();
236        // file exists
237
238        let tmp2 = TmpDir::new("bar").await.unwrap();
239
240        tmp.copy(tmp2.as_ref()).await.unwrap();
241        assert!(
242            fs::metadata(tmp2.as_ref().to_path_buf().join("dir1").join("file1"))
243                .await
244                .is_ok()
245        );
246
247        tmp.close().await.unwrap();
248        tmp2.close().await.unwrap();
249    }
250}