Skip to main content

lib/
fn_filespath.rs

1use regex::Regex;
2use std::collections::HashSet;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
7struct Rule {
8    regex: Regex,
9    only_dir: bool,
10    // is_generic: bool,
11    // raw_clean: String,
12}
13
14fn glob_to_regex(pattern: &str) -> Rule {
15    let raw = pattern.trim();
16    let mut p = raw.replace('\\', "/");
17
18    // NAPRAWA: Jeśli użytkownik podał "./folder", ucinamy "./",
19    // ponieważ relatywna ścieżka (rel_path) nigdy tego nie zawiera.
20    if p.starts_with("./") {
21        p = p[2..].to_string();
22    }
23
24    // let is_generic = p == "*" || p == "**/*";
25    let only_dir = p.ends_with('/');
26    if only_dir {
27        p.pop();
28    }
29    // let raw_clean = p.clone();
30
31    let mut regex_str = regex::escape(&p);
32    regex_str = regex_str.replace(r"\*\*", ".*");
33    regex_str = regex_str.replace(r"\*", "[^/]*");
34    regex_str = regex_str.replace(r"\?", "[^/]");
35    regex_str = regex_str.replace(r"\[!", "[^");
36
37    if regex_str.starts_with('/') {
38        regex_str = format!("^{}", &regex_str[1..]);
39    } else if regex_str.starts_with(".*") {
40        regex_str = format!("^{}", regex_str);
41    } else {
42        regex_str = format!("(?:^|/){}", regex_str);
43    }
44
45    if only_dir {
46        regex_str.push_str("(?:/.*)?$");
47    } else {
48        regex_str.push('$');
49    }
50
51    let final_regex = format!("(?i){}", regex_str);
52
53    Rule {
54        regex: Regex::new(&final_regex).unwrap_or_else(|_| Regex::new("(?i)$.^").unwrap()),
55        only_dir,
56        // is_generic,
57        // raw_clean,
58    }
59}
60
61/// Element tablicy wejściowej
62/// Element tablicy wejściowej
63pub struct Task<'a> {
64    pub path_location: &'a str,
65    pub path_exclude: Vec<&'a str>,
66    pub path_include_only: Vec<&'a str>,
67    pub filter_files: Vec<&'a str>,
68    pub output_type: &'a str, // "dirs", "files", "dirs_and_files"
69}
70
71// Implementujemy wartości domyślne, co pozwoli nam pomijać nieużywane pola
72impl<'a> Default for Task<'a> {
73    fn default() -> Self {
74        Self {
75            path_location: ".",
76            path_exclude: vec![],
77            path_include_only: vec![],
78            filter_files: vec![],
79            output_type: "dirs_and_files",
80        }
81    }
82}
83
84pub fn filespath(tasks: &[Task]) -> Vec<PathBuf> {
85    let mut all_results = HashSet::new();
86
87    for task in tasks {
88        let root_path = Path::new(task.path_location);
89        let canonical_root =
90            fs::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
91
92        // Przygotowanie reguł
93        let mut exclude_rules = Vec::new();
94        for p in &task.path_exclude {
95            if !p.trim().is_empty() {
96                exclude_rules.push(glob_to_regex(p));
97            }
98        }
99
100        let mut include_only_rules = Vec::new();
101        for p in &task.path_include_only {
102            if !p.trim().is_empty() {
103                include_only_rules.push(glob_to_regex(p));
104            }
105        }
106
107        let mut filter_files_rules = Vec::new();
108        for p in &task.filter_files {
109            if !p.trim().is_empty() {
110                filter_files_rules.push(glob_to_regex(p));
111            }
112        }
113
114        // =========================================================
115        // KROK 1: PEŁNY SKAN Z ODRZUCENIEM CAŁYCH GAŁĘZI EXCLUDE
116        // =========================================================
117        let mut scanned_paths = Vec::new();
118        scan_step1(
119            &canonical_root,
120            &canonical_root,
121            &exclude_rules,
122            &mut scanned_paths,
123        );
124
125        // =========================================================
126        // KROK 2: ZACHOWANIE FOLDERÓW I FILTROWANIE PLIKÓW INCLUDE
127        // =========================================================
128        for path in scanned_paths {
129            let rel_path = path
130                .strip_prefix(&canonical_root)
131                .unwrap()
132                .to_string_lossy()
133                .replace('\\', "/");
134            let path_slash = format!("{}/", rel_path);
135
136            if !include_only_rules.is_empty() {
137                let mut matches = false;
138                for rule in &include_only_rules {
139                    if rule.only_dir {
140                        // Jeśli reguła dotyczy TYLKO folderów
141                        if path.is_dir() && rule.regex.is_match(&path_slash) {
142                            matches = true;
143                            break;
144                        }
145                    } else {
146                        // Jeśli reguła jest uniwersalna (pliki i foldery)
147                        if rule.regex.is_match(&rel_path) || rule.regex.is_match(&path_slash) {
148                            matches = true;
149                            break;
150                        }
151                    }
152                }
153                if !matches {
154                    continue;
155                }
156            }
157
158            if path.is_dir() {
159                // Jeśli tryb to NIE "files" (czyli "dirs" lub "dirs_and_files")
160                // to dodajemy folder normalnie.
161                if task.output_type != "files" {
162                    all_results.insert(path);
163                }
164            } else {
165                // Jeśli tryb to "dirs", całkowicie ignorujemy pliki
166                if task.output_type == "dirs" {
167                    continue;
168                }
169
170                // Pliki sprawdzamy pod kątem filter_files
171                let mut is_file_matched = false;
172                if filter_files_rules.is_empty() {
173                    is_file_matched = true;
174                } else {
175                    for rule in &filter_files_rules {
176                        if rule.only_dir {
177                            continue;
178                        }
179                        if rule.regex.is_match(&rel_path) {
180                            is_file_matched = true;
181                            break;
182                        }
183                    }
184                }
185
186                if is_file_matched {
187                    all_results.insert(path.clone());
188
189                    // MAGIA DLA "files":
190                    // Aby drzewo nie spłaszczyło się do zwykłej listy, musimy dodać foldery nadrzędne
191                    // tylko dla TEGO KONKRETNEGO dopasowanego pliku. (Ukrywa to puste foldery!)
192                    if task.output_type == "files" {
193                        let mut current_parent = path.parent();
194                        while let Some(p) = current_parent {
195                            all_results.insert(p.to_path_buf());
196                            if p == canonical_root {
197                                break;
198                            }
199                            current_parent = p.parent();
200                        }
201                    }
202                }
203            }
204        }
205    }
206
207    let result: Vec<PathBuf> = all_results.into_iter().collect();
208    result
209}
210
211// Prywatna funkcja pomocnicza do wykonania Kroku 1
212fn scan_step1(
213    root_path: &Path,
214    current_path: &Path,
215    exclude_rules: &[Rule],
216    scanned_paths: &mut Vec<PathBuf>,
217) {
218    let read_dir = match fs::read_dir(current_path) {
219        Ok(rd) => rd,
220        Err(_) => return,
221    };
222
223    for entry in read_dir.filter_map(|e| e.ok()) {
224        let path = entry.path();
225        let is_dir = path.is_dir();
226
227        let rel_path = match path.strip_prefix(root_path) {
228            Ok(p) => p.to_string_lossy().replace('\\', "/"),
229            Err(_) => continue,
230        };
231
232        if rel_path.is_empty() {
233            continue;
234        }
235
236        let path_slash = format!("{}/", rel_path);
237
238        // KROK 1.1: Czy wykluczone przez EXCLUDE?
239        let mut is_excluded = false;
240        for rule in exclude_rules {
241            if rule.only_dir && !is_dir {
242                continue;
243            }
244            if rule.regex.is_match(&rel_path) || (is_dir && rule.regex.is_match(&path_slash)) {
245                is_excluded = true;
246                break;
247            }
248        }
249
250        // Jeśli folder/plik jest wykluczony - URYWAMY GAŁĄŹ I NIE WCHODZIMY GŁĘBIEJ
251        if is_excluded {
252            continue;
253        }
254
255        // KROK 1.2: Dodajemy do tymczasowych wyników KROKU 1
256        scanned_paths.push(path.clone());
257
258        // KROK 1.3: Jeśli to bezpieczny folder, skanujemy jego zawartość
259        if is_dir {
260            scan_step1(root_path, &path, exclude_rules, scanned_paths);
261        }
262    }
263}