1use 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 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!("[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 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 pub fn search_glob_known(&self, pattern: &str) -> Vec<PathBuf> {
169 let options = MatchOptions {
170 case_sensitive: true,
171 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 pub fn search_glob_unknown(&self, pattern: &str) -> Vec<PathBuf> {
200 let options = MatchOptions {
201 case_sensitive: true,
202 require_literal_separator: false,
204 require_literal_leading_dot: false,
205 };
206
207 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
276pub fn random_filename() -> String {
278 let mut generator = Generator::with_naming(Name::Numbered);
279 generator.next().unwrap()
280}
281
282pub 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}