tree_fs/
builder.rs

1use std::{
2    fs::File,
3    io::Write,
4    path::{Path, PathBuf},
5};
6
7#[cfg(feature = "yaml")]
8use serde::Deserialize;
9
10/// Represents a file tree structure
11///
12/// # Examples
13///
14/// ```rust
15/// use tree_fs::{TreeBuilder, Settings};
16/// let tree = TreeBuilder::default()
17///     .add_file("config/app.conf", "host = localhost")
18///     .add_empty_file("logs/app.log")
19///     .add_directory("data/raw")
20///     .add_file_with_settings(
21///         "secrets/api.key",
22///         "supersecretkey",
23///         Settings::new().readonly(true)
24///     )
25///     .create()
26///     .expect("create tree fs");
27/// println!("Created a complex tree in: {}", tree.root.display());
28///
29/// // You can verify the readonly status (this requires std::fs)
30/// // let key_path = tree.root.join("secrets/api.key");
31/// // let metadata = std::fs::metadata(key_path).unwrap();
32/// // assert!(metadata.permissions().readonly());
33/// ```
34///
35/// ```rust
36/// use tree_fs::TreeBuilder;
37/// let tree = TreeBuilder::default()
38///      .add_file("temp_data/file.txt", "temporary content")
39///      .add_empty_file("temp_data/another.tmp")
40///      .drop(true) // This is the default, but explicitly shown here
41///      .create()
42///      .expect("create tree fs for drop example");
43///
44/// println!("Temporary tree created at: {}", tree.root.display());
45///
46/// let path_to_check = tree.root.clone();
47/// assert!(path_to_check.exists(), "Directory should exist before drop");
48///
49/// drop(tree); // tree_fs instance goes out of scope
50/// assert!(!path_to_check.exists(), "Directory should be deleted after drop");
51/// ```
52#[derive(Debug)]
53#[cfg_attr(feature = "yaml", derive(Deserialize))]
54pub struct TreeBuilder {
55    /// Root folder where the tree will be created.
56    #[cfg_attr(feature = "yaml", serde(default = "crate::tree::temp_dir"))]
57    pub root: PathBuf,
58    /// Flag indicating whether existing files should be overridden.
59    #[cfg_attr(feature = "yaml", serde(default))]
60    override_file: bool,
61    /// List of entries in the tree.
62    entries: Vec<crate::Entry>,
63    /// Whether to automatically delete the temporary folder when Tree is dropped
64    #[cfg_attr(feature = "yaml", serde(default = "crate::yaml::default_drop"))]
65    drop: bool,
66}
67
68impl TreeBuilder {
69    /// Sets the root folder where the tree will be created.
70    #[must_use]
71    pub fn root_folder<P: AsRef<Path>>(mut self, dir: P) -> Self {
72        self.root = dir.as_ref().to_path_buf();
73        self
74    }
75
76    /// Sets the `drop` flag, indicating whether to automatically delete the temporary folder when the `tree_fs` instance is dropped
77    #[must_use]
78    pub const fn drop(mut self, yes: bool) -> Self {
79        self.drop = yes;
80        self
81    }
82
83    /// Sets the `override_file` flag, indicating whether existing files should be overridden.
84    #[must_use]
85    pub const fn override_file(mut self, yes: bool) -> Self {
86        self.override_file = yes;
87        self
88    }
89
90    /// Adds a file with content to the tree.
91    #[must_use]
92    pub fn add<P: AsRef<Path>>(mut self, path: P, content: &str) -> Self {
93        self.entries.push(crate::Entry {
94            path: path.as_ref().to_path_buf(),
95            kind: crate::Kind::TextFile {
96                content: content.to_string(),
97            },
98            settings: None,
99        });
100        self
101    }
102
103    /// Adds a file with content to the tree.
104    ///
105    /// This is an alias for `add`.
106    #[must_use]
107    pub fn add_file<P: AsRef<Path>>(self, path: P, content: &str) -> Self {
108        self.add(path, content)
109    }
110
111    /// Adds a file with a empty content.
112    #[must_use]
113    pub fn add_empty<P: AsRef<Path>>(self, path: P) -> Self {
114        self.add_empty_file(path)
115    }
116
117    /// Adds a file with a empty content.
118    #[must_use]
119    pub fn add_empty_file<P: AsRef<Path>>(mut self, path: P) -> Self {
120        self.entries.push(crate::Entry {
121            path: path.as_ref().to_path_buf(),
122            kind: crate::Kind::EmptyFile,
123            settings: None,
124        });
125        self
126    }
127
128    /// Adds a directory to the tree.
129    #[must_use]
130    pub fn add_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
131        self.entries.push(crate::Entry {
132            path: path.as_ref().to_path_buf(),
133            kind: crate::Kind::Directory,
134            settings: None,
135        });
136        self
137    }
138
139    /// Adds a file with content and custom settings to the tree.
140    #[must_use]
141    pub fn add_file_with_settings<P: AsRef<Path>>(
142        mut self,
143        path: P,
144        content: &str,
145        settings: crate::tree::Settings,
146    ) -> Self {
147        self.entries.push(crate::Entry {
148            path: path.as_ref().to_path_buf(),
149            kind: crate::Kind::TextFile {
150                content: content.to_string(),
151            },
152            settings: Some(settings),
153        });
154        self
155    }
156
157    /// Adds an empty file with custom settings to the tree.
158    #[must_use]
159    pub fn add_empty_file_with_settings<P: AsRef<Path>>(
160        mut self,
161        path: P,
162        settings: crate::tree::Settings,
163    ) -> Self {
164        self.entries.push(crate::Entry {
165            path: path.as_ref().to_path_buf(),
166            kind: crate::Kind::EmptyFile,
167            settings: Some(settings),
168        });
169        self
170    }
171
172    /// Adds a directory with custom settings to the tree.
173    #[must_use]
174    pub fn add_directory_with_settings<P: AsRef<Path>>(
175        mut self,
176        path: P,
177        settings: crate::tree::Settings,
178    ) -> Self {
179        self.entries.push(crate::Entry {
180            path: path.as_ref().to_path_buf(),
181            kind: crate::Kind::Directory,
182            settings: Some(settings),
183        });
184        self
185    }
186
187    /// Convenience method for adding a read-only file.
188    #[must_use]
189    pub fn add_readonly_file<P: AsRef<Path>>(self, path: P, content: &str) -> Self {
190        self.add_file_with_settings(path, content, crate::tree::Settings::new().readonly(true))
191    }
192
193    /// Convenience method for adding a read-only empty file.
194    #[must_use]
195    pub fn add_readonly_empty_file<P: AsRef<Path>>(self, path: P) -> Self {
196        self.add_empty_file_with_settings(path, crate::tree::Settings::new().readonly(true))
197    }
198
199    /// Creates the file tree by generating files and directories based on the specified metadata.
200    ///
201    /// # Errors
202    ///
203    /// Returns an `std::io::Result` indicating success or failure in creating the file tree.
204    pub fn create(&self) -> std::io::Result<crate::Tree> {
205        if !self.root.exists() {
206            std::fs::create_dir_all(&self.root)?;
207        }
208
209        // Process entries
210        for entry in &self.entries {
211            let dest_path = self.root.join(&entry.path);
212            if !self.override_file && dest_path.exists() {
213                continue;
214            }
215
216            match &entry.kind {
217                crate::Kind::Directory => {
218                    std::fs::create_dir_all(&dest_path)?;
219                }
220                crate::Kind::EmptyFile => {
221                    if let Some(parent_dir) = Path::new(&dest_path).parent() {
222                        std::fs::create_dir_all(parent_dir)?;
223                    }
224                    File::create(&dest_path)?;
225                }
226                crate::Kind::TextFile { content } => {
227                    if let Some(parent_dir) = Path::new(&dest_path).parent() {
228                        std::fs::create_dir_all(parent_dir)?;
229                    }
230                    let mut file = File::create(&dest_path)?;
231                    file.write_all(content.as_bytes())?;
232                }
233            }
234
235            if let Some(settings) = &entry.settings {
236                if matches!(entry.kind, crate::Kind::Directory) {
237                    continue;
238                }
239
240                let dest_path_for_perms = self.root.join(&entry.path);
241                if settings.readonly {
242                    let mut permissions = std::fs::metadata(&dest_path_for_perms)?.permissions();
243                    permissions.set_readonly(true);
244                    std::fs::set_permissions(&dest_path_for_perms, permissions)?;
245                }
246            }
247        }
248
249        Ok(crate::Tree {
250            root: self.root.clone(),
251            drop: self.drop,
252        })
253    }
254}
255
256impl Default for TreeBuilder {
257    /// Creates a default `Tree` instance with an empty file list,
258    fn default() -> Self {
259        Self {
260            entries: vec![],
261            override_file: false,
262            root: crate::tree::temp_dir(),
263            drop: true,
264        }
265    }
266}