rust_filesearch/fs/
traverse.rs1use 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#[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
32pub 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 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 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
87pub 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 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#[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 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 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 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 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}