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 = fs::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
90
91        // Przygotowanie reguł
92        let mut exclude_rules = Vec::new();
93        for p in &task.path_exclude {
94            if !p.trim().is_empty() { exclude_rules.push(glob_to_regex(p)); }
95        }
96
97        let mut include_only_rules = Vec::new();
98        for p in &task.path_include_only {
99            if !p.trim().is_empty() { include_only_rules.push(glob_to_regex(p)); }
100        }
101
102        let mut filter_files_rules = Vec::new();
103        for p in &task.filter_files {
104            if !p.trim().is_empty() { filter_files_rules.push(glob_to_regex(p)); }
105        }
106
107        // =========================================================
108        // KROK 1: PEŁNY SKAN Z ODRZUCENIEM CAŁYCH GAŁĘZI EXCLUDE
109        // =========================================================
110        let mut scanned_paths = Vec::new();
111        scan_step1(&canonical_root, &canonical_root, &exclude_rules, &mut scanned_paths);
112
113        // =========================================================
114        // KROK 2: ZACHOWANIE FOLDERÓW I FILTROWANIE PLIKÓW INCLUDE
115        // =========================================================
116        for path in scanned_paths {
117            let rel_path = path.strip_prefix(&canonical_root)
118                .unwrap()
119                .to_string_lossy()
120                .replace('\\', "/");
121            let path_slash = format!("{}/", rel_path);
122
123            if !include_only_rules.is_empty() {
124                let mut matches = false;
125                for rule in &include_only_rules {
126                    if rule.only_dir {
127                        // Jeśli reguła dotyczy TYLKO folderów
128                        if path.is_dir() && rule.regex.is_match(&path_slash) {
129                            matches = true; 
130                            break;
131                        }
132                    } else {
133                        // Jeśli reguła jest uniwersalna (pliki i foldery)
134                        if rule.regex.is_match(&rel_path) || rule.regex.is_match(&path_slash) {
135                            matches = true; 
136                            break;
137                        }
138                    }
139                }
140                if !matches {
141                    continue; 
142                }
143            }
144
145            if path.is_dir() {
146                // Jeśli tryb to NIE "files" (czyli "dirs" lub "dirs_and_files")
147                // to dodajemy folder normalnie.
148                if task.output_type != "files" {
149                    all_results.insert(path);
150                }
151            } else {
152                // Jeśli tryb to "dirs", całkowicie ignorujemy pliki
153                if task.output_type == "dirs" {
154                    continue;
155                }
156
157                // Pliki sprawdzamy pod kątem filter_files
158                let mut is_file_matched = false;
159                if filter_files_rules.is_empty() {
160                    is_file_matched = true;
161                } else {
162                    for rule in &filter_files_rules {
163                        if rule.only_dir { continue; } 
164                        if rule.regex.is_match(&rel_path) {
165                            is_file_matched = true;
166                            break;
167                        }
168                    }
169                }
170
171                if is_file_matched {
172                    all_results.insert(path.clone());
173                    
174                    // MAGIA DLA "files":
175                    // Aby drzewo nie spłaszczyło się do zwykłej listy, musimy dodać foldery nadrzędne
176                    // tylko dla TEGO KONKRETNEGO dopasowanego pliku. (Ukrywa to puste foldery!)
177                    if task.output_type == "files" {
178                        let mut current_parent = path.parent();
179                        while let Some(p) = current_parent {
180                            all_results.insert(p.to_path_buf());
181                            if p == canonical_root { break; }
182                            current_parent = p.parent();
183                        }
184                    }
185                }
186            }
187        }
188    }
189
190    let result: Vec<PathBuf> = all_results.into_iter().collect();
191    result
192}
193
194// Prywatna funkcja pomocnicza do wykonania Kroku 1
195fn scan_step1(
196    root_path: &Path,
197    current_path: &Path,
198    exclude_rules: &[Rule],
199    scanned_paths: &mut Vec<PathBuf>
200) {
201    let read_dir = match fs::read_dir(current_path) {
202        Ok(rd) => rd,
203        Err(_) => return,
204    };
205
206    for entry in read_dir.filter_map(|e| e.ok()) {
207        let path = entry.path();
208        let is_dir = path.is_dir();
209
210        let rel_path = match path.strip_prefix(root_path) {
211            Ok(p) => p.to_string_lossy().replace('\\', "/"),
212            Err(_) => continue,
213        };
214
215        if rel_path.is_empty() {
216            continue;
217        }
218        
219        let path_slash = format!("{}/", rel_path);
220
221        // KROK 1.1: Czy wykluczone przez EXCLUDE?
222        let mut is_excluded = false;
223        for rule in exclude_rules {
224            if rule.only_dir && !is_dir {
225                continue;
226            }
227            if rule.regex.is_match(&rel_path) || (is_dir && rule.regex.is_match(&path_slash)) {
228                is_excluded = true;
229                break;
230            }
231        }
232
233        // Jeśli folder/plik jest wykluczony - URYWAMY GAŁĄŹ I NIE WCHODZIMY GŁĘBIEJ
234        if is_excluded {
235            continue; 
236        }
237
238        // KROK 1.2: Dodajemy do tymczasowych wyników KROKU 1
239        scanned_paths.push(path.clone());
240
241        // KROK 1.3: Jeśli to bezpieczny folder, skanujemy jego zawartość
242        if is_dir {
243            scan_step1(root_path, &path, exclude_rules, scanned_paths);
244        }
245    }
246}