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(&current, Permissions::from_mode(0o700))?;
144                // Append the next path component.
145                if let Some(component) = path.strip_prefix(&current).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}