utils_box_pathfinder/
paths.rs1use anyhow::{Result, anyhow, bail};
6use directories::BaseDirs;
7use glob::{MatchOptions, glob_with};
8use names::{Generator, Name};
9use std::{env, path::PathBuf, str::FromStr};
10use walkdir::{DirEntry, WalkDir};
11
12use utils_box_logger::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 pub fn new() -> Self {
33 Self {
34 paths: IncludePaths {
35 known_paths: vec![],
36 unknown_paths: vec![],
37 },
38 }
39 }
40
41 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 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 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 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 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 pub fn seek_in_unknown(&self, file: &str) -> Result<PathBuf> {
117 let _home_dir: PathBuf = if let Some(base_dirs) = BaseDirs::new() {
119 base_dirs.home_dir().to_path_buf()
120 } else {
121 bail!(
122 "[include_paths][seek_in_unknown] Failed to retrieve system's home directory path"
123 )
124 };
125
126 let current_dir: PathBuf = if let Ok(curr) = env::current_dir() {
128 curr.to_path_buf().canonicalize()?
129 } else {
130 bail!("[include_paths][seek_in_unknown] Failed to retrieve current directory path")
131 };
132
133 for repo in self.unknown_paths.iter().rev() {
134 for parent_dir in current_dir.ancestors() {
135 for entry in WalkDir::new(parent_dir)
136 .follow_links(true)
137 .into_iter()
138 .filter_entry(|e| {
139 (e.file_type().is_dir() || e.path_is_symlink()) && is_not_hidden(e)
140 })
141 {
142 let e_path = match entry {
143 Err(_) => continue,
144 Ok(ref e) => e.path(),
145 };
146
147 match e_path.file_name() {
148 Some(f_name) => {
149 if f_name == repo && e_path.join(file).exists() {
150 return Ok(e_path.join(file));
151 } else {
152 continue;
153 }
154 }
155 None => continue,
156 }
157 }
158 }
159 }
160
161 bail!(
162 "[include_paths][seek_in_unknown] Failed to find file in directories in included unknown paths"
163 );
164 }
165
166 pub fn search_glob(&self, pattern: &str) -> Vec<PathBuf> {
169 let seek_dir = self.search_glob_known(pattern);
170
171 if !seek_dir.is_empty() {
172 return seek_dir;
173 }
174
175 self.search_glob_unknown(pattern)
176 }
177
178 pub fn search_glob_known(&self, pattern: &str) -> Vec<PathBuf> {
180 let options = MatchOptions {
181 case_sensitive: true,
182 require_literal_separator: false,
184 require_literal_leading_dot: false,
185 };
186
187 let detected_files: Vec<PathBuf> = self
188 .known_paths
189 .iter()
190 .flat_map(|dir| {
191 let new_pattern = dir.join("**/").join(pattern);
192 let new_pattern = new_pattern.to_str().unwrap();
193 glob_with(new_pattern, options)
194 .unwrap()
195 .filter_map(|path| path.ok())
196 .collect::<Vec<PathBuf>>()
197 })
198 .collect();
199
200 log_debug!(
201 "[include_paths][search_glob_known] Detected files for [{}]: \n{:?}",
202 pattern,
203 detected_files
204 );
205
206 detected_files
207 }
208
209 pub fn search_glob_unknown(&self, pattern: &str) -> Vec<PathBuf> {
211 let options = MatchOptions {
212 case_sensitive: true,
213 require_literal_separator: false,
215 require_literal_leading_dot: false,
216 };
217
218 let home_dir: PathBuf = if let Some(base_dirs) = BaseDirs::new() {
220 base_dirs.home_dir().to_path_buf()
221 } else {
222 log_debug!(
223 "[include_paths][search_glob_unknown] Failed to retrieve system's home directory path"
224 );
225 PathBuf::new()
226 };
227
228 let detected_files: Vec<PathBuf> = {
229 let mut tmp: Vec<PathBuf> = vec![];
230 for repo in self.unknown_paths.iter() {
231 let mut dir: Vec<PathBuf> = WalkDir::new(&home_dir)
232 .follow_links(true)
233 .into_iter()
234 .filter_entry(|e| e.file_type().is_dir() && is_not_hidden(e))
235 .filter_map(|entry| {
236 let dir = match entry {
237 Err(_) => return None,
238 Ok(ref e) => e.path(),
239 };
240
241 match dir.file_name() {
242 Some(f_name) => {
243 if f_name != repo {
244 return None;
245 }
246 }
247 None => return None,
248 }
249
250 let new_pattern = dir.join("**/").join(pattern);
251 let new_pattern = new_pattern.to_str().unwrap();
252
253 Some(
254 glob_with(new_pattern, options)
255 .unwrap()
256 .filter_map(|path| path.ok())
257 .collect::<Vec<PathBuf>>(),
258 )
259 })
260 .flatten()
261 .collect::<Vec<PathBuf>>();
262
263 tmp.append(&mut dir);
264 }
265
266 tmp
267 };
268
269 log_debug!(
270 "[include_paths][search_glob_unknown] Detected files for [{}]: \n{:?}",
271 pattern,
272 detected_files
273 );
274
275 detected_files
276 }
277}
278
279fn is_not_hidden(entry: &DirEntry) -> bool {
280 entry
281 .file_name()
282 .to_str()
283 .map(|s| !s.starts_with('.'))
284 .unwrap_or(false)
285}
286
287pub fn random_filename() -> String {
289 let mut generator = Generator::with_naming(Name::Numbered);
290 generator.next().unwrap()
291}
292
293pub fn timestamp_filename() -> String {
295 format!("{}", chrono::offset::Utc::now().format("%Y%m%d_%H%M%S"))
296}
297
298#[cfg(test)]
299mod tests {
300 use crate::paths::*;
301
302 #[test]
303 fn include_path_test() {
304 let paths = IncludePathsBuilder::new()
305 .include_known("/test/")
306 .include_known("/2/")
307 .include_exe_dir()
308 .include_known("test_data/")
309 .build();
310
311 println!("Paths: {:?}", paths);
312
313 let file = "test_archives.tar";
314 assert_eq!(
315 std::fs::canonicalize(PathBuf::from_str("test_data/test_archives.tar").unwrap())
316 .unwrap(),
317 std::fs::canonicalize(paths.seek(file).unwrap()).unwrap()
318 );
319 }
320
321 #[test]
322 fn include_repos_test() {
323 let paths = IncludePathsBuilder::new()
324 .include_unknown("utils-box")
325 .build();
326
327 println!("Paths: {:?}", paths);
328
329 let file = "src/paths.rs";
330 assert_eq!(
331 std::fs::canonicalize(
332 std::env::current_exe()
333 .unwrap()
334 .parent()
335 .unwrap()
336 .join("../../../src/paths.rs")
337 )
338 .unwrap(),
339 std::fs::canonicalize(paths.seek_in_unknown(file).unwrap()).unwrap()
340 );
341 }
342
343 #[test]
344 fn include_seek_all_test() {
345 let paths = IncludePathsBuilder::new()
346 .include_known("/test/")
347 .include_known("/2/")
348 .include_exe_dir()
349 .include_known("tests_data/")
350 .include_unknown("utils-box")
351 .build();
352
353 println!("Paths: {:?}", paths);
354
355 let file = "src/paths.rs";
356 assert_eq!(
357 std::fs::canonicalize(
358 std::env::current_exe()
359 .unwrap()
360 .parent()
361 .unwrap()
362 .join("../../../src/paths.rs")
363 )
364 .unwrap(),
365 std::fs::canonicalize(paths.seek(file).unwrap()).unwrap()
366 );
367 }
368
369 #[test]
370 fn search_glob_dir_test() {
371 let paths = IncludePathsBuilder::new()
372 .include_known("test_data/")
373 .build();
374
375 println!("Paths: {:?}", paths);
376
377 let pattern = "test_*.tar";
378
379 let results = paths.search_glob_known(pattern);
380
381 assert!(!results.is_empty());
382 }
383
384 #[test]
385 fn search_glob_repos_test() {
386 let paths = IncludePathsBuilder::new()
387 .include_unknown("utils-box")
388 .build();
389
390 println!("Paths: {:?}", paths);
391
392 let pattern = "test_*.tar";
393
394 let results = paths.search_glob_unknown(pattern);
395
396 assert!(!results.is_empty());
397 }
398
399 #[test]
400 fn search_glob_all_test() {
401 let paths = IncludePathsBuilder::new()
402 .include_exe_dir()
403 .include_unknown("utils-box")
404 .build();
405
406 println!("Paths: {:?}", paths);
407
408 let pattern = "test_*.tar";
409
410 let results = paths.search_glob(pattern);
411
412 assert!(!results.is_empty());
413 }
414}