temp_dir_builder/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    env,
5    fs::File,
6    io::Write,
7    path::{Path, PathBuf},
8};
9
10use path_clean::PathClean;
11use rand::{distributions::Alphanumeric, thread_rng, Rng};
12
13/// Represents a temporary directory.  
14/// By default this temporary directory is deleted when this struct is dropped.
15#[derive(Debug)]
16pub struct TempDirectory {
17    path: PathBuf,
18    delete_on_drop: bool,
19}
20
21impl TempDirectory {
22    #[must_use]
23    #[allow(clippy::missing_const_for_fn)]
24    pub fn path(&self) -> &Path {
25        &self.path
26    }
27}
28
29/// Error happening when creating the directory tree.
30#[derive(Debug, thiserror::Error)]
31pub enum BuildError {
32    #[error("Failed to create the root directory '{0}': {1}")]
33    FailedToCreateRootDirectory(PathBuf, std::io::Error),
34    #[error("Failed to create directory '{0}': {1}")]
35    FailedToCreateDirectory(PathBuf, std::io::Error),
36    #[error("Failed to delete directory '{0}': {1}")]
37    FailedToDeleteDirectory(PathBuf, std::io::Error),
38    #[error("Failed to create file '{0}': {1}")]
39    FailedToCreateFile(PathBuf, std::io::Error),
40    #[error("Failed to read source file '{0}': {1}")]
41    FailedToCopyFile(PathBuf, std::io::Error),
42    #[error("Failed to write file '{0}': {1}")]
43    FailedToWriteFile(PathBuf, std::io::Error),
44    #[error("The entry '{0}' is outside the temporary directory")]
45    EntryOutsideDirectory(PathBuf),
46    #[error("The entry {0} has an empty name")]
47    EmptyEntryName(usize),
48    #[error("The entry '{0}' is already existing")]
49    DuplicateEntry(PathBuf),
50}
51
52/// A temporary directory builder that contains a list of entries to be created.
53///
54/// # Examples
55///
56/// ```rust
57// <snip id="example-builder">
58/// use temp_dir_builder::TempDirectoryBuilder;
59/// let temp_dir = TempDirectoryBuilder::default()
60///     .add_text_file("test/foo.txt", "bar")
61///     .add_binary_file("test/foo2.txt", &[98u8, 97u8, 114u8])
62///     .add_empty_file("test/folder-a/folder-b/bar.txt")
63///     .add_file("test/file.rs", file!())
64///     .add_directory("test/dir")
65///     .build()
66///     .expect("create temp dir");
67/// println!("created successfully in {}", temp_dir.path().display());
68// </snip>
69/// ```
70#[derive(Debug)]
71pub struct TempDirectoryBuilder {
72    /// Root folder where the tree will be created.
73    root: PathBuf,
74    /// List of file metadata entries in the tree.
75    entries: Vec<Entry>,
76    /// Flag indicating whether the temporary directory created must be deleted when the instance is dropped.
77    delete_on_drop: bool,
78}
79
80impl Default for TempDirectoryBuilder {
81    /// Creates a default `TempDirectoryBuilder` instance with an empty file list,
82    fn default() -> Self {
83        Self {
84            entries: vec![],
85            root: random_temp_directory(),
86            delete_on_drop: true,
87        }
88    }
89}
90
91impl Drop for TempDirectory {
92    fn drop(&mut self) {
93        if self.delete_on_drop {
94            let _ = std::fs::remove_dir_all(&self.path);
95        }
96    }
97}
98
99impl TempDirectoryBuilder {
100    /// Sets the root folder where the tree will be created.  
101    /// By default this is the temporary directory path returned by `std::env::temp_dir()`.
102    #[must_use]
103    pub fn root_folder(mut self, dir: impl AsRef<Path>) -> Self {
104        self.root = dir.as_ref().to_path_buf();
105        self
106    }
107
108    /// Specifies whether to automatically delete the temporary folder when the `TempDirectory` instance is dropped.  
109    /// By default this is value is set to `true`.
110    #[must_use]
111    pub const fn delete_on_drop(mut self, delete_on_drop: bool) -> Self {
112        self.delete_on_drop = delete_on_drop;
113        self
114    }
115
116    #[must_use]
117    fn add(mut self, path: impl AsRef<Path>, kind: Kind) -> Self {
118        self.entries.push(Entry {
119            path: path.as_ref().to_path_buf(),
120            kind,
121        });
122        self
123    }
124
125    /// Adds an empty file.
126    /// * `path` - Path of the file to create. This path must be relative to the created directory. If the path is outside
127    ///   the created directory (e.g: "../foo") the error `BuildError::EntryOutsideDirectory` will be returned.
128    #[must_use]
129    pub fn add_empty_file<P: AsRef<Path>>(self, path: P) -> Self {
130        self.add(path, Kind::EmptyFile)
131    }
132
133    /// Adds a directory.
134    /// * `path` - Path of the directory to create. This path must be relative to the created directory.
135    ///   If the path is outside the created directory (e.g: "../foo") the error `BuildError::EntryOutsideDirectory` will be returned.
136    #[must_use]
137    pub fn add_directory(self, path: impl AsRef<Path>) -> Self {
138        self.add(path, Kind::Directory)
139    }
140
141    /// Adds a text file specifying the content.
142    /// * `path` - Path of the text file to create. This path must be relative to the created directory.
143    ///   If the path is outside the created directory (e.g: "../foo") the error `BuildError::EntryOutsideDirectory` will be returned.
144    /// * `text` - Text to be written in the new file created.
145    #[must_use]
146    #[allow(clippy::needless_pass_by_value)]
147    pub fn add_text_file(self, path: impl AsRef<Path>, text: impl ToString) -> Self {
148        self.add(path, Kind::TextFile(text.to_string()))
149    }
150
151    /// Adds a binary file specifying the content.
152    /// * `path` - Path of the binary file to create. This path must be relative to the created directory.
153    ///   If the path is outside the created directory (e.g: "../foo") the error `BuildError::EntryOutsideDirectory` will be returned.
154    /// * `content` - The bytes to be written in the new file created.
155    #[must_use]
156    pub fn add_binary_file(self, path: impl AsRef<Path>, content: &[u8]) -> Self {
157        self.add(path, Kind::BinaryFile(content.to_vec()))
158    }
159
160    /// Adds a file specifying a source file to be copied.
161    /// * `path` - Path of the file to create. This path must be relative to the created directory.
162    ///   If the path is outside the created directory (e.g: "../foo") the error `BuildError::EntryOutsideDirectory` will be returned.
163    /// * `file` - Path of the file to be copied. This path must be absolute.
164    #[must_use]
165    pub fn add_file(self, path: impl AsRef<Path>, file: impl AsRef<Path>) -> Self {
166        self.add(path, Kind::FileToCopy(file.as_ref().to_path_buf()))
167    }
168
169    /// Builds the file tree by generating files and directories based on the
170    /// list of `Entry`s.
171    ///
172    /// # Errors
173    /// A `BuildError` is returned in case of error.
174    pub fn build(&self) -> Result<TempDirectory, BuildError> {
175        if !self.root.exists() {
176            std::fs::create_dir_all(&self.root)
177                .map_err(|err| BuildError::FailedToCreateRootDirectory(self.root.clone(), err))?;
178        }
179
180        for (entry_index, entry) in self.entries.iter().enumerate() {
181            if entry.path.as_os_str().is_empty() {
182                return Err(BuildError::EmptyEntryName(entry_index));
183            }
184
185            let entry_path = self.root.join(&entry.path).clean();
186
187            if !entry_path.starts_with(&self.root) {
188                return Err(BuildError::EntryOutsideDirectory(entry.path.clone()));
189            }
190
191            if entry_path.exists() {
192                return Err(BuildError::DuplicateEntry(entry_path));
193            }
194
195            if let Some(parent_dir) = Path::new(&entry_path).parent() {
196                std::fs::create_dir_all(parent_dir).map_err(|err| {
197                    BuildError::FailedToCreateDirectory(parent_dir.to_path_buf(), err)
198                })?;
199            }
200
201            match &entry.kind {
202                Kind::Directory => {
203                    std::fs::create_dir(&entry_path)
204                        .map_err(|err| BuildError::FailedToCreateDirectory(entry_path, err))?;
205                }
206                Kind::EmptyFile => {
207                    File::create(&entry_path)
208                        .map_err(|err| BuildError::FailedToCreateFile(entry_path, err))?;
209                }
210                Kind::TextFile(text) => {
211                    let mut new_file = File::create(&entry_path)
212                        .map_err(|err| BuildError::FailedToCreateFile(entry_path.clone(), err))?;
213
214                    new_file
215                        .write_all(text.as_bytes())
216                        .map_err(|err| BuildError::FailedToWriteFile(entry_path, err))?;
217                }
218                Kind::BinaryFile(bytes) => {
219                    let mut new_file = File::create(&entry_path)
220                        .map_err(|err| BuildError::FailedToCreateFile(entry_path.clone(), err))?;
221
222                    new_file
223                        .write_all(bytes)
224                        .map_err(|err| BuildError::FailedToWriteFile(entry_path, err))?;
225                }
226                Kind::FileToCopy(source_path) => {
227                    std::fs::copy(source_path, &entry_path)
228                        .map_err(|err| BuildError::FailedToCopyFile(source_path.clone(), err))?;
229                }
230            }
231        }
232
233        Ok(TempDirectory {
234            path: self.root.clone(),
235            delete_on_drop: self.delete_on_drop,
236        })
237    }
238}
239
240fn random_temp_directory() -> PathBuf {
241    loop {
242        let random_string: String = thread_rng()
243            .sample_iter(&Alphanumeric)
244            .take(5)
245            .map(char::from)
246            .collect();
247
248        let path = env::temp_dir().join(random_string);
249
250        if !path.exists() {
251            return path;
252        }
253    }
254}
255
256#[derive(Debug)]
257enum Kind {
258    Directory,
259    EmptyFile,
260    TextFile(String),
261    BinaryFile(Vec<u8>),
262    FileToCopy(PathBuf),
263}
264
265/// Represents an entry, file or directory, to be created.
266#[derive(Debug)]
267struct Entry {
268    /// Path of the entry relative to the root folder.
269    path: PathBuf,
270    /// The kind of the entry
271    kind: Kind,
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_temp_dir() {
280        let temp_dir = TempDirectoryBuilder::default().build().unwrap();
281
282        assert!(temp_dir.path().exists());
283        assert!(temp_dir.path().is_dir());
284    }
285
286    #[test]
287    fn test_add_text_file() {
288        let expected_content = "bar";
289        let entry_name = "foo.txt";
290        let temp_dir = TempDirectoryBuilder::default()
291            .add_text_file(entry_name, expected_content)
292            .build()
293            .unwrap();
294        let entry_path = temp_dir.path().join(entry_name);
295
296        assert!(entry_path.exists());
297
298        let content = std::fs::read_to_string(entry_path).expect("read text in foo.txt");
299
300        assert_eq!(content, expected_content);
301    }
302
303    #[test]
304    fn test_add_binary_file() {
305        let expected_content = [98u8, 97u8, 114u8];
306        let entry_name = "foo.txt";
307        let temp_dir = TempDirectoryBuilder::default()
308            .add_binary_file(entry_name, &expected_content)
309            .build()
310            .unwrap();
311        let entry_path = temp_dir.path().join(entry_name);
312
313        assert!(entry_path.exists());
314
315        let content = std::fs::read(entry_path).expect("read foo.txt");
316
317        assert_eq!(content, expected_content);
318    }
319
320    #[test]
321    fn test_add_empty_file() {
322        let entry_name = "empty_file.txt";
323        let temp_dir = TempDirectoryBuilder::default()
324            .add_empty_file(entry_name)
325            .build()
326            .unwrap();
327        let entry_path = temp_dir.path().join(entry_name);
328
329        assert!(entry_path.exists());
330
331        let created_entry_metadata = std::fs::metadata(entry_path).expect("get entry metadata");
332
333        assert_eq!(created_entry_metadata.len(), 0);
334    }
335
336    #[test]
337    fn test_add_directory() {
338        let entry_name = "empty_directory";
339        let temp_dir = TempDirectoryBuilder::default()
340            .add_directory(entry_name)
341            .build()
342            .unwrap();
343        let entry_path = temp_dir.path().join(entry_name);
344
345        assert!(entry_path.exists());
346        assert!(entry_path.is_dir());
347    }
348
349    #[test]
350    fn test_add_file() {
351        let entry_name = "test.rs";
352        let source_file_path = file!();
353        let temp_dir = TempDirectoryBuilder::default()
354            .add_file(entry_name, source_file_path)
355            .build()
356            .unwrap();
357        let entry_path = temp_dir.path().join(entry_name);
358
359        assert!(entry_path.exists());
360        assert!(entry_path.is_file());
361
362        let entry_content = std::fs::read_to_string(entry_path).unwrap();
363        let source_content = std::fs::read_to_string(source_file_path).unwrap();
364
365        assert_eq!(entry_content, source_content);
366    }
367
368    #[test]
369    fn test_temp_dir_is_dropped() {
370        let temp_dir = TempDirectoryBuilder::default().build().unwrap();
371
372        let temp_dir_path = temp_dir.path().to_path_buf();
373
374        assert!(temp_dir_path.exists());
375        assert!(temp_dir_path.is_dir());
376
377        drop(temp_dir);
378
379        assert!(!temp_dir_path.exists())
380    }
381
382    #[test]
383    fn test_entry_outside_temp_dir() {
384        let path_outside_temp_dir = std::env::temp_dir().join("outside");
385        let builder = TempDirectoryBuilder::default().add_empty_file(path_outside_temp_dir);
386        let error = builder.build().unwrap_err();
387
388        assert!(matches!(error, BuildError::EntryOutsideDirectory(_)));
389    }
390
391    #[test]
392    fn test_source_file_does_not_exists() {
393        let source_file_path = std::env::temp_dir().join("not existing file");
394        let builder = TempDirectoryBuilder::default().add_file("foo", source_file_path);
395        let error = builder.build().unwrap_err();
396
397        assert!(matches!(error, BuildError::FailedToCopyFile(..)));
398    }
399
400    #[test]
401    fn test_duplicated_entries() {
402        let builder = TempDirectoryBuilder::default()
403            .add_empty_file("foo")
404            .add_empty_file("foo");
405        let error = builder.build().unwrap_err();
406
407        assert!(matches!(error, BuildError::DuplicateEntry(..)));
408    }
409
410    #[test]
411    fn test_entry_outside_directory() {
412        let builder = TempDirectoryBuilder::default().add_empty_file("../foo");
413        let error = builder.build().unwrap_err();
414
415        assert!(matches!(error, BuildError::EntryOutsideDirectory(..)));
416    }
417
418    #[test]
419    fn test_empty_entry_name() {
420        let builder = TempDirectoryBuilder::default().add_empty_file("");
421        let error = builder.build().unwrap_err();
422
423        assert!(matches!(error, BuildError::EmptyEntryName(0)));
424    }
425}