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)]
11use 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#[derive(Debug)]
61pub struct TmpDir {
62 inner: PathBuf,
63}
64
65const LEN_RNG: usize = 10;
66
67impl TmpDir {
68 pub async fn new(prefix: impl AsRef<str>) -> io::Result<Self> {
70 let mut inner = env::temp_dir();
71 let s: String = {
72 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 pub fn to_path_buf(&self) -> PathBuf {
88 self.as_ref().to_owned()
89 }
90
91 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 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 pub async fn copy(&self, dest_dir: impl AsRef<Path>) -> io::Result<()> {
131 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 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 pub async fn close(&self) -> io::Result<()> {
160 fs::remove_dir_all(&self.inner).await
161 }
162}
163
164impl 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 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 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 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}