rust_filesearch/fs/
traverse.rs

1use crate::errors::Result;
2use crate::fs::filters::Predicate;
3use crate::fs::metadata::extract_entry;
4use crate::models::Entry;
5use ignore::WalkBuilder;
6use std::path::Path;
7
8/// Configuration for filesystem traversal
9#[derive(Debug, Clone)]
10pub struct TraverseConfig {
11    pub max_depth: Option<usize>,
12    pub follow_symlinks: bool,
13    pub include_hidden: bool,
14    pub respect_gitignore: bool,
15    pub threads: usize,
16    pub quiet: bool,
17}
18
19impl Default for TraverseConfig {
20    fn default() -> Self {
21        Self {
22            max_depth: None,
23            follow_symlinks: false,
24            include_hidden: false,
25            respect_gitignore: true,
26            threads: 1,
27            quiet: false,
28        }
29    }
30}
31
32/// Walk a directory tree and yield entries matching the predicate
33pub fn walk<P>(root: &Path, config: &TraverseConfig, predicate: Option<&P>) -> Result<Vec<Entry>>
34where
35    P: Predicate + ?Sized,
36{
37    let mut builder = WalkBuilder::new(root);
38
39    builder
40        .follow_links(config.follow_symlinks)
41        .hidden(!config.include_hidden)
42        .git_ignore(config.respect_gitignore)
43        .git_exclude(config.respect_gitignore);
44
45    if let Some(depth) = config.max_depth {
46        builder.max_depth(Some(depth));
47    }
48
49    let mut entries = Vec::new();
50
51    for result in builder.build() {
52        match result {
53            Ok(dir_entry) => {
54                let path = dir_entry.path();
55                let depth = dir_entry.depth();
56
57                match extract_entry(path, depth) {
58                    Ok(entry) => {
59                        // Apply predicate filter if provided
60                        if let Some(pred) = predicate {
61                            if pred.test(&entry) {
62                                entries.push(entry);
63                            }
64                        } else {
65                            entries.push(entry);
66                        }
67                    }
68                    Err(e) => {
69                        // Log error but continue traversal
70                        if !config.quiet {
71                            eprintln!("Warning: Failed to extract entry for {:?}: {}", path, e);
72                        }
73                    }
74                }
75            }
76            Err(e) => {
77                if !config.quiet {
78                    eprintln!("Warning: Error during traversal: {}", e);
79                }
80            }
81        }
82    }
83
84    Ok(entries)
85}
86
87/// Walk a directory tree without filtering (convenience function)
88pub fn walk_no_filter(root: &Path, config: &TraverseConfig) -> Result<Vec<Entry>> {
89    let mut builder = WalkBuilder::new(root);
90
91    builder
92        .follow_links(config.follow_symlinks)
93        .hidden(!config.include_hidden)
94        .git_ignore(config.respect_gitignore)
95        .git_exclude(config.respect_gitignore);
96
97    if let Some(depth) = config.max_depth {
98        builder.max_depth(Some(depth));
99    }
100
101    let mut entries = Vec::new();
102
103    for result in builder.build() {
104        match result {
105            Ok(dir_entry) => {
106                let path = dir_entry.path();
107                let depth = dir_entry.depth();
108
109                match extract_entry(path, depth) {
110                    Ok(entry) => {
111                        entries.push(entry);
112                    }
113                    Err(e) => {
114                        // Log error but continue traversal
115                        if !config.quiet {
116                            eprintln!("Warning: Failed to extract entry for {:?}: {}", path, e);
117                        }
118                    }
119                }
120            }
121            Err(e) => {
122                if !config.quiet {
123                    eprintln!("Warning: Error during traversal: {}", e);
124                }
125            }
126        }
127    }
128
129    Ok(entries)
130}
131
132/// Parallel walk implementation (requires "parallel" feature)
133#[cfg(feature = "parallel")]
134pub fn walk_parallel<P>(
135    root: &Path,
136    config: &TraverseConfig,
137    predicate: Option<&P>,
138) -> Result<Vec<Entry>>
139where
140    P: Predicate + Sync,
141{
142    use jwalk::WalkDir;
143    use rayon::prelude::*;
144
145    let mut builder = WalkDir::new(root);
146
147    builder = builder
148        .follow_links(config.follow_symlinks)
149        .skip_hidden(!config.include_hidden);
150
151    if let Some(depth) = config.max_depth {
152        builder = builder.max_depth(depth);
153    }
154
155    let entries: Vec<Entry> = builder
156        .into_iter()
157        .par_bridge()
158        .filter_map(|result| result.ok())
159        .filter_map(|dir_entry| {
160            let path = dir_entry.path();
161            let depth = dir_entry.depth;
162
163            match extract_entry(&path, depth) {
164                Ok(entry) => {
165                    if let Some(pred) = predicate {
166                        if pred.test(&entry) {
167                            Some(entry)
168                        } else {
169                            None
170                        }
171                    } else {
172                        Some(entry)
173                    }
174                }
175                Err(_) => None,
176            }
177        })
178        .collect();
179
180    Ok(entries)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::fs;
187    use tempfile::tempdir;
188
189    #[test]
190    fn test_walk_basic() {
191        let dir = tempdir().unwrap();
192        let file1 = dir.path().join("file1.txt");
193        let file2 = dir.path().join("file2.txt");
194        fs::write(&file1, "test").unwrap();
195        fs::write(&file2, "test").unwrap();
196
197        let config = TraverseConfig::default();
198        let entries = walk_no_filter(dir.path(), &config).unwrap();
199
200        // Should have at least the directory itself and two files
201        assert!(entries.len() >= 3);
202    }
203
204    #[test]
205    fn test_walk_max_depth() {
206        let dir = tempdir().unwrap();
207        let subdir = dir.path().join("subdir");
208        fs::create_dir(&subdir).unwrap();
209        fs::write(subdir.join("file.txt"), "test").unwrap();
210
211        let config = TraverseConfig {
212            max_depth: Some(1),
213            ..Default::default()
214        };
215
216        let entries = walk_no_filter(dir.path(), &config).unwrap();
217
218        // Should not include files in subdir
219        assert!(entries.iter().all(|e| e.depth <= 1));
220    }
221
222    #[test]
223    fn test_walk_hidden() {
224        let dir = tempdir().unwrap();
225        let hidden = dir.path().join(".hidden");
226        fs::write(&hidden, "test").unwrap();
227
228        // Without include_hidden
229        let config = TraverseConfig::default();
230        let entries = walk_no_filter(dir.path(), &config).unwrap();
231        assert!(!entries.iter().any(|e| e.name == ".hidden"));
232
233        // With include_hidden
234        let config = TraverseConfig {
235            include_hidden: true,
236            ..Default::default()
237        };
238        let entries = walk_no_filter(dir.path(), &config).unwrap();
239        assert!(entries.iter().any(|e| e.name == ".hidden"));
240    }
241}