tempfs/temp_dir.rs
1#[cfg(feature = "rand_gen")]
2use crate::global_consts::{num_retry, rand_fn_len, valid_chars};
3#[cfg(feature = "rand_gen")]
4use rand::Rng;
5#[cfg(feature = "regex_support")]
6use regex::{Error as RErr, Regex};
7use std::env;
8use std::fs;
9#[cfg(unix)]
10use std::fs::Permissions;
11use std::io;
12use std::path::{Path, PathBuf};
13
14use crate::error::TempResult;
15use crate::helpers::normalize_path;
16use crate::temp_file::TempFile;
17
18// TODO: created_dir like in temp_file.rs
19
20/// A temporary directory that automatically cleans up its contents when dropped.
21///
22/// Files created through the `TempDir` are tracked and removed upon drop.
23pub struct TempDir {
24 /// The full path to the temporary directory.
25 path: Option<PathBuf>,
26 /// Temporary files contained within the directory.
27 files: Vec<TempFile>,
28 /// The first created parent directory of the parent directories.
29 created_parent: Option<PathBuf>,
30}
31
32impl TempDir {
33 /// Creates a new temporary directory at the specified path.
34 ///
35 /// The directory (and any missing parent directories) will be created.
36 ///
37 /// # Arguments
38 ///
39 /// * `path` - The path at which to create the directory. If a relative path is provided, it is resolved relative to the system temporary directory.
40 ///
41 /// # Errors
42 ///
43 /// Returns an error if the directory cannot be created.
44 pub fn new<P: AsRef<Path>>(path: P) -> TempResult<Self> {
45 let path_ref = normalize_path(path.as_ref());
46 let path_buf = if path_ref.is_absolute() {
47 path_ref
48 } else {
49 env::temp_dir().join(path_ref)
50 };
51 let created = Self::create_with_parent(&path_buf)?;
52 Ok(Self {
53 path: Some(path_buf),
54 files: Vec::new(),
55 created_parent: created,
56 })
57 }
58
59 /// Creates a new temporary directory at the specified path.
60 ///
61 /// The directory (and any missing parent directories) will be created.
62 ///
63 /// # Arguments
64 ///
65 /// * `path` - The path at which to create the directory. If a relative path is provided, it is resolved relative to the current directory.
66 ///
67 /// # Errors
68 ///
69 /// Returns an error if the directory cannot be created.
70 pub fn new_here<P: AsRef<Path>>(path: P) -> TempResult<Self> {
71 let path_ref = normalize_path(path.as_ref());
72 let path_buf = if path_ref.is_absolute() {
73 path_ref
74 } else {
75 env::current_dir()?.join(path_ref)
76 };
77 Self::new(path_buf)
78 }
79
80 #[cfg(feature = "rand_gen")]
81 /// Creates a new temporary directory with a random name in the given parent directory.
82 ///
83 /// The directory name will consist of alphanumeric characters only, ensuring compatibility
84 /// across different filesystems.
85 ///
86 /// # Arguments
87 ///
88 /// * `dir` - An optional parent directory in which to create the temporary directory. If a relative directory is provided, it is resolved relative to the system temporary directory.
89 ///
90 /// # Errors
91 ///
92 /// Returns an error if a unique directory name cannot be generated or if directory creation fails.
93 pub fn new_random<P: AsRef<Path>>(dir: Option<P>) -> TempResult<Self> {
94 let parent_dir = if let Some(d) = dir {
95 let d_ref = normalize_path(d.as_ref());
96 if d_ref.is_absolute() {
97 d_ref
98 } else {
99 env::temp_dir().join(d_ref)
100 }
101 } else {
102 env::temp_dir()
103 };
104 let mut rng = rand::rng();
105
106 for _ in 0..num_retry() {
107 let name: String = (0..rand_fn_len())
108 .map(|_| {
109 let idx = rng.random_range(0..valid_chars().len());
110 valid_chars()[idx] as char
111 })
112 .collect();
113
114 let full_path = parent_dir.join(&name);
115 if !full_path.exists() {
116 let created = Self::create_with_parent(&full_path)?;
117 return Ok(Self {
118 path: Some(full_path),
119 files: Vec::new(),
120 created_parent: created,
121 });
122 }
123 }
124 Err(io::Error::new(
125 io::ErrorKind::AlreadyExists,
126 "Could not generate a unique directory name",
127 )
128 .into())
129 }
130
131 /// Function to create the directory and its parent directories, then set their permissions to rwx------, returning the first component of the parent's path which does not exist, or None if it all exists except for the child.
132 fn create_with_parent(path: &PathBuf) -> TempResult<Option<PathBuf>> {
133 #[cfg(unix)]
134 use std::os::unix::fs::PermissionsExt;
135 let nonexistent = crate::helpers::first_missing_directory_component(path);
136 fs::create_dir_all(path)?;
137
138 #[cfg(unix)]
139 if let Some(first_missing) = nonexistent.clone() {
140 let mut current = first_missing;
141 // Loop until the final directory in the path is reached.
142 while current != *path {
143 fs::set_permissions(¤t, Permissions::from_mode(0o700))?;
144 // Append the next path component.
145 if let Some(component) = path.strip_prefix(¤t).unwrap().components().next() {
146 current = current.join(component);
147 } else {
148 break;
149 }
150 }
151 // Finally, set permissions on the final directory.
152 fs::set_permissions(path, Permissions::from_mode(0o700))?;
153 } else {
154 // If no directory was missing (only the child directory was created)
155 fs::set_permissions(path, Permissions::from_mode(0o700))?;
156 }
157
158 Ok(nonexistent)
159 }
160
161 /// Creates a new temporary directory with a random name in the given parent directory.
162 ///
163 /// The directory name will consist of alphanumeric characters only, ensuring compatibility
164 /// across different filesystems.
165 ///
166 /// # Arguments
167 ///
168 /// * `dir` - An optional parent directory in which to create the temporary directory. If a relative directory is provided, it is resolved relative to the current working directory.
169 ///
170 /// # Errors
171 ///
172 /// Returns an error if a unique directory name cannot be generated or if directory creation fails.
173 #[cfg(feature = "rand_gen")]
174 pub fn new_random_here<P: AsRef<Path>>(dir: Option<P>) -> TempResult<Self> {
175 if let Some(dir) = dir {
176 let d_ref = normalize_path(dir.as_ref());
177 if d_ref.is_absolute() {
178 Self::new_random(Some(d_ref))
179 } else {
180 Self::new_random(Some(&env::current_dir()?.join(d_ref)))
181 }
182 } else {
183 Self::new_random(Some(&env::current_dir()?))
184 }
185 }
186
187 /// Creates a new temporary file with the given filename in the directory.
188 ///
189 /// The created file is tracked and will be automatically deleted on drop.
190 ///
191 /// # Arguments
192 ///
193 /// * `filename` - The name of the file to create.
194 ///
195 /// # Errors
196 ///
197 /// This function will return an error if the inner path is `None`.
198 #[allow(clippy::missing_panics_doc)]
199 pub fn create_file<S: AsRef<str>>(&mut self, filename: S) -> TempResult<&mut TempFile> {
200 let dir = self.path.as_ref().ok_or_else(|| {
201 io::Error::new(io::ErrorKind::Other, "Temporary directory path is not set")
202 })?;
203 let file_path = dir.join(filename.as_ref());
204 self.files.push(TempFile::new(file_path)?);
205 Ok(self.files.last_mut().unwrap())
206 }
207
208 #[cfg(feature = "rand_gen")]
209 /// Creates a new temporary file with a random name in the directory.
210 ///
211 /// The file is tracked and will be automatically deleted on drop.
212 ///
213 /// # Errors
214 ///
215 /// Returns an error if a unique filename cannot be generated or if file creation fails.
216 #[allow(clippy::missing_panics_doc)]
217 pub fn create_random_file(&mut self) -> TempResult<&mut TempFile> {
218 let dir = self.path.as_ref().ok_or_else(|| {
219 io::Error::new(io::ErrorKind::Other, "Temporary directory path is not set")
220 })?;
221 self.files
222 .push(TempFile::new_random(Some(normalize_path(dir)))?);
223 Ok(self.files.last_mut().unwrap())
224 }
225
226 /// Removes a file from the directory's management.
227 ///
228 /// This does not delete the file immediately—it will be removed when the directory is dropped.
229 ///
230 /// # Arguments
231 ///
232 /// * `filename` - The name of the file to remove from management.
233 pub fn remove_file<S: AsRef<str>>(&mut self, filename: S) {
234 let filename = filename.as_ref();
235 self.files.retain(|f| {
236 f.path
237 .as_ref()
238 .and_then(|p| p.file_name())
239 .and_then(|n| n.to_str())
240 != Some(filename)
241 });
242 }
243
244 /// Retrieves a reference to a temporary file by filename.
245 ///
246 /// # Arguments
247 ///
248 /// * `filename` - The name of the file to retrieve.
249 pub fn get_file<S: AsRef<str>>(&self, filename: S) -> Option<&TempFile> {
250 let filename = filename.as_ref();
251 self.files.iter().find(|f| {
252 f.path
253 .as_ref()
254 .and_then(|p| p.file_name())
255 .and_then(|n| n.to_str())
256 == Some(filename)
257 })
258 }
259
260 /// Retrieves a mutable reference to a temporary file by filename.
261 ///
262 /// # Arguments
263 ///
264 /// * `filename` - The name of the file to retrieve.
265 pub fn get_file_mut<S: AsRef<str>>(&mut self, filename: S) -> Option<&mut TempFile> {
266 let filename = filename.as_ref();
267 self.files.iter_mut().find(|f| {
268 f.path
269 .as_ref()
270 .and_then(|p| p.file_name())
271 .and_then(|n| n.to_str())
272 == Some(filename)
273 })
274 }
275
276 /// Returns the path of the temporary directory.
277 #[must_use]
278 pub fn path(&self) -> Option<&Path> {
279 self.path.as_deref()
280 }
281
282 /// Consumes the `TempDir`, returning its path and preventing cleanup.
283 #[must_use]
284 pub fn into_path(mut self) -> Option<PathBuf> {
285 self.path.take()
286 }
287
288 /// Lists the paths of all files managed by the directory.
289 #[must_use]
290 pub fn list_files(&self) -> Vec<&Path> {
291 self.files
292 .iter()
293 .filter_map(|f| f.path.as_deref())
294 .collect()
295 }
296
297 #[cfg(feature = "rand_gen")]
298 /// Creates a new temporary directory with a random name within the given parent directory.
299 ///
300 /// # Arguments
301 ///
302 /// * `path` - The parent directory in which to create the temporary directory. If a relative path is provided, it is resolved relative to the system temporary directory.
303 ///
304 /// # Errors
305 ///
306 /// Returns an error if a unique directory name cannot be generated or if directory creation fails.
307 pub fn new_in<P: AsRef<Path>>(path: P) -> TempResult<Self> {
308 Self::new_random(Some(path))
309 }
310}
311
312#[cfg(feature = "regex_support")]
313impl TempDir {
314 /// Finds files with names matching a regex pattern.
315 ///
316 /// # Arguments
317 ///
318 /// * `pattern` - A regex pattern to match file names.
319 ///
320 /// # Errors
321 ///
322 /// Returns an error if the regex pattern is invalid.
323 pub fn find_files_by_pattern<S: AsRef<str>>(&self, pattern: S) -> Result<Vec<&TempFile>, RErr> {
324 let re = Regex::new(pattern.as_ref())?;
325 Ok(self
326 .files
327 .iter()
328 .filter(|f| {
329 f.path
330 .as_ref()
331 .and_then(|p| p.file_name())
332 .and_then(|n| n.to_str())
333 .is_some_and(|name| re.is_match(name))
334 })
335 .collect())
336 }
337
338 /// Finds mutable references to files with names matching a regex pattern.
339 ///
340 /// # Arguments
341 ///
342 /// * `pattern` - A regex pattern to match file names.
343 ///
344 /// # Errors
345 ///
346 /// Returns an error if the regex pattern is invalid.
347 pub fn find_files_by_pattern_mut<S: AsRef<str>>(
348 &mut self,
349 pattern: S,
350 ) -> Result<Vec<&mut TempFile>, RErr> {
351 let re = Regex::new(pattern.as_ref())?;
352 Ok(self
353 .files
354 .iter_mut()
355 .filter(|f| {
356 f.path
357 .as_ref()
358 .and_then(|p| p.file_name())
359 .and_then(|n| n.to_str())
360 .is_some_and(|name| re.is_match(name))
361 })
362 .collect())
363 }
364}
365
366impl Drop for TempDir {
367 fn drop(&mut self) {
368 match (self.path.take(), self.created_parent.take()) {
369 (Some(p), None) => {
370 self.files.clear();
371 let _ = fs::remove_dir_all(p);
372 }
373 (Some(_), Some(d)) => {
374 self.files.clear();
375 let _ = fs::remove_dir_all(d);
376 }
377 _ => {}
378 }
379 }
380}
381
382impl AsRef<Path> for TempDir {
383 fn as_ref(&self) -> &Path {
384 self.path.as_ref().expect("TempDir path is None")
385 }
386}