utils_box/
paths.rs

1//! # Path utilities
2//! A toolbox of small utilities to handle paths for files.
3//! Useful for searching for files in a pre-determined list of directories or git repositories.
4
5use anyhow::{Result, anyhow, bail};
6use directories::BaseDirs;
7use glob::{MatchOptions, glob_with};
8use names::{Generator, Name};
9use std::{path::PathBuf, str::FromStr};
10use walkdir::{DirEntry, WalkDir};
11
12use crate::log_debug;
13
14#[derive(Debug, Clone)]
15pub struct IncludePaths {
16    known_paths: Vec<PathBuf>,
17    unknown_paths: Vec<PathBuf>,
18}
19
20pub struct IncludePathsBuilder {
21    paths: IncludePaths,
22}
23
24impl Default for IncludePathsBuilder {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl IncludePathsBuilder {
31    /// Create new builder
32    pub fn new() -> Self {
33        Self {
34            paths: IncludePaths {
35                known_paths: vec![],
36                unknown_paths: vec![],
37            },
38        }
39    }
40
41    /// Add another include directory that we know the path to seek into
42    /// The directories are considered to be **absolute** if starting with `/` or relative to current folder otherwise
43    /// You can chain multiple calls
44    pub fn include_known(&mut self, path: &str) -> &mut Self {
45        let new = self;
46        new.paths.known_paths.push(PathBuf::from_str(path).unwrap());
47        new
48    }
49
50    /// Add current executable directory to seek into
51    /// You can chain multiple calls
52    pub fn include_exe_dir(&mut self) -> &mut Self {
53        let new = self;
54        new.paths
55            .known_paths
56            .push(std::env::current_exe().unwrap().parent().unwrap().into());
57        new
58    }
59
60    /// Add another include directory that we do not know the path to, to seek into
61    /// We will start seeking the directory first by backtracing from the  the current folder until we reach the $HOME directory.
62    /// Once we find the directory, then we will check inside for the files.
63    /// Useful for testing inside project data.
64    /// For example `cargo test` will execute from the `./target` folder and you have your test data inside a `config` folder somewhere in your project.
65    /// The same fodler will be deployed in a subfolder next to the binary in deployment.
66    /// You can use this function to include the `config` folder as unknown and the code will discover the directory by itself.
67    /// This feature is useful to avoid having multiple includes with `../..` etc to cover all scenarios.
68    /// You can chain multiple calls
69    pub fn include_unknown(&mut self, path: &str) -> &mut Self {
70        let new = self;
71        new.paths
72            .unknown_paths
73            .push(PathBuf::from_str(path).unwrap());
74        new
75    }
76
77    pub fn build(&mut self) -> IncludePaths {
78        self.paths.clone()
79    }
80}
81
82impl IncludePaths {
83    /// Seek File in include directories first
84    /// If not found, fall-back in unknown path search (if applicable)
85    pub fn seek(&self, file: &str) -> Result<PathBuf> {
86        let seek_dir = self.seek_in_known(file);
87
88        if seek_dir.is_ok() {
89            return seek_dir;
90        }
91
92        self.seek_in_unknown(file).map_err(|_| {
93            anyhow!("[include_paths][seek] Failed to find file in directories in included known & unknown paths")
94        })
95    }
96
97    /// Seek only in included known paths (ingoring included unknown paths)
98    pub fn seek_in_known(&self, file: &str) -> Result<PathBuf> {
99        let file = PathBuf::from_str(file)?;
100        self.known_paths
101            .iter()
102            .find_map(|f| {
103                if f.join(&file).exists() {
104                    Some(f.join(&file))
105                } else {
106                    None
107                }
108            })
109            .ok_or(anyhow!(
110                "[include_paths][seek_in_known] Failed to find file in directories in included known paths"
111            ))
112    }
113
114    /// Seek only in included repositories (ingoring included directories)
115    /// Tries to locate the repositories in the current working directory and then going backwards until reaching `$HOME` or `%HOME%`
116    pub fn seek_in_unknown(&self, file: &str) -> Result<PathBuf> {
117        // Get HOME directory
118        let home_dir: PathBuf = if let Some(base_dirs) = BaseDirs::new() {
119            base_dirs.home_dir().to_path_buf()
120        } else {
121            bail!("[include_paths][seek_in_repos] Failed to retrieve system's home directory path")
122        };
123
124        for repo in self.unknown_paths.iter().rev() {
125            for entry in WalkDir::new(&home_dir)
126                .follow_links(true)
127                .into_iter()
128                .filter_entry(|e| {
129                    (e.file_type().is_dir() || e.path_is_symlink()) && is_not_hidden(e)
130                })
131            {
132                let e_path = match entry {
133                    Err(_) => continue,
134                    Ok(ref e) => e.path(),
135                };
136
137                match e_path.file_name() {
138                    Some(f_name) => {
139                        if f_name == repo && e_path.join(file).exists() {
140                            return Ok(e_path.join(file));
141                        } else {
142                            continue;
143                        }
144                    }
145                    None => continue,
146                }
147            }
148        }
149
150        bail!(
151            "[include_paths][seek_in_unknown] Failed to find file in directories in included unknown paths"
152        );
153    }
154
155    /// Search in directories in known paths first for files matching the glob pattern requested
156    /// If not found, fall-back in unknown paths search (if applicable)
157    pub fn search_glob(&self, pattern: &str) -> Vec<PathBuf> {
158        let seek_dir = self.search_glob_known(pattern);
159
160        if !seek_dir.is_empty() {
161            return seek_dir;
162        }
163
164        self.search_glob_unknown(pattern)
165    }
166
167    /// Search in included directories for files matching the glob pattern requested
168    pub fn search_glob_known(&self, pattern: &str) -> Vec<PathBuf> {
169        let options = MatchOptions {
170            case_sensitive: true,
171            // Allow relative paths
172            require_literal_separator: false,
173            require_literal_leading_dot: false,
174        };
175
176        let detected_files: Vec<PathBuf> = self
177            .known_paths
178            .iter()
179            .flat_map(|dir| {
180                let new_pattern = dir.join("**/").join(pattern);
181                let new_pattern = new_pattern.to_str().unwrap();
182                glob_with(new_pattern, options)
183                    .unwrap()
184                    .filter_map(|path| path.ok())
185                    .collect::<Vec<PathBuf>>()
186            })
187            .collect();
188
189        log_debug!(
190            "[include_paths][search_glob_known] Detected files for [{}]: \n{:?}",
191            pattern,
192            detected_files
193        );
194
195        detected_files
196    }
197
198    /// Search in included repsotories for files matching the glob pattern requested
199    pub fn search_glob_unknown(&self, pattern: &str) -> Vec<PathBuf> {
200        let options = MatchOptions {
201            case_sensitive: true,
202            // Allow relative paths
203            require_literal_separator: false,
204            require_literal_leading_dot: false,
205        };
206
207        // Get HOME directory
208        let home_dir: PathBuf = if let Some(base_dirs) = BaseDirs::new() {
209            base_dirs.home_dir().to_path_buf()
210        } else {
211            log_debug!(
212                "[include_paths][search_glob_unknown] Failed to retrieve system's home directory path"
213            );
214            PathBuf::new()
215        };
216
217        let detected_files: Vec<PathBuf> = {
218            let mut tmp: Vec<PathBuf> = vec![];
219            for repo in self.unknown_paths.iter() {
220                let mut dir: Vec<PathBuf> = WalkDir::new(&home_dir)
221                    .follow_links(true)
222                    .into_iter()
223                    .filter_entry(|e| e.file_type().is_dir() && is_not_hidden(e))
224                    .filter_map(|entry| {
225                        let dir = match entry {
226                            Err(_) => return None,
227                            Ok(ref e) => e.path(),
228                        };
229
230                        match dir.file_name() {
231                            Some(f_name) => {
232                                if f_name != repo {
233                                    return None;
234                                }
235                            }
236                            None => return None,
237                        }
238
239                        let new_pattern = dir.join("**/").join(pattern);
240                        let new_pattern = new_pattern.to_str().unwrap();
241
242                        Some(
243                            glob_with(new_pattern, options)
244                                .unwrap()
245                                .filter_map(|path| path.ok())
246                                .collect::<Vec<PathBuf>>(),
247                        )
248                    })
249                    .flatten()
250                    .collect::<Vec<PathBuf>>();
251
252                tmp.append(&mut dir);
253            }
254
255            tmp
256        };
257
258        log_debug!(
259            "[include_paths][search_glob_unknown] Detected files for [{}]: \n{:?}",
260            pattern,
261            detected_files
262        );
263
264        detected_files
265    }
266}
267
268fn is_not_hidden(entry: &DirEntry) -> bool {
269    entry
270        .file_name()
271        .to_str()
272        .map(|s| !s.starts_with('.'))
273        .unwrap_or(false)
274}
275
276/// Generate random filename
277pub fn random_filename() -> String {
278    let mut generator = Generator::with_naming(Name::Numbered);
279    generator.next().unwrap()
280}
281
282/// Generate timestamp for filenames
283pub fn timestamp_filename() -> String {
284    format!("{}", chrono::offset::Utc::now().format("%Y%m%d_%H%M%S"))
285}
286
287#[cfg(test)]
288mod tests {
289    use crate::paths::*;
290
291    #[test]
292    fn include_path_test() {
293        let paths = IncludePathsBuilder::new()
294            .include_known("/test/")
295            .include_known("/2/")
296            .include_exe_dir()
297            .include_known("test_data/")
298            .build();
299
300        println!("Paths: {:?}", paths);
301
302        let file = "test_archives.tar";
303        assert_eq!(
304            std::fs::canonicalize(PathBuf::from_str("test_data/test_archives.tar").unwrap())
305                .unwrap(),
306            std::fs::canonicalize(paths.seek(file).unwrap()).unwrap()
307        );
308    }
309
310    #[test]
311    fn include_repos_test() {
312        let paths = IncludePathsBuilder::new()
313            .include_unknown("utils-box")
314            .build();
315
316        println!("Paths: {:?}", paths);
317
318        let file = "src/paths.rs";
319        assert_eq!(
320            std::fs::canonicalize(
321                std::env::current_exe()
322                    .unwrap()
323                    .parent()
324                    .unwrap()
325                    .join("../../../src/paths.rs")
326            )
327            .unwrap(),
328            std::fs::canonicalize(paths.seek_in_unknown(file).unwrap()).unwrap()
329        );
330    }
331
332    #[test]
333    fn include_seek_all_test() {
334        let paths = IncludePathsBuilder::new()
335            .include_known("/test/")
336            .include_known("/2/")
337            .include_exe_dir()
338            .include_known("tests_data/")
339            .include_unknown("utils-box")
340            .build();
341
342        println!("Paths: {:?}", paths);
343
344        let file = "src/paths.rs";
345        assert_eq!(
346            std::fs::canonicalize(
347                std::env::current_exe()
348                    .unwrap()
349                    .parent()
350                    .unwrap()
351                    .join("../../../src/paths.rs")
352            )
353            .unwrap(),
354            std::fs::canonicalize(paths.seek(file).unwrap()).unwrap()
355        );
356    }
357
358    #[test]
359    fn search_glob_dir_test() {
360        let paths = IncludePathsBuilder::new()
361            .include_known("test_data/")
362            .build();
363
364        println!("Paths: {:?}", paths);
365
366        let pattern = "test_*.tar";
367
368        let results = paths.search_glob_known(pattern);
369
370        assert!(!results.is_empty());
371    }
372
373    #[test]
374    fn search_glob_repos_test() {
375        let paths = IncludePathsBuilder::new()
376            .include_unknown("utils-box")
377            .build();
378
379        println!("Paths: {:?}", paths);
380
381        let pattern = "test_*.tar";
382
383        let results = paths.search_glob_unknown(pattern);
384
385        assert!(!results.is_empty());
386    }
387
388    #[test]
389    fn search_glob_all_test() {
390        let paths = IncludePathsBuilder::new()
391            .include_exe_dir()
392            .include_unknown("utils-box")
393            .build();
394
395        println!("Paths: {:?}", paths);
396
397        let pattern = "test_*.tar";
398
399        let results = paths.search_glob(pattern);
400
401        assert!(!results.is_empty());
402    }
403}