1use crate::{Throttle, WalkOptions, WalkRoot, crossdev, inodefilter::InodeFilter};
2
3use crossbeam::channel::Receiver;
4#[cfg(not(windows))]
5use filesize::PathExt;
6use petgraph::{Directed, Direction, graph::NodeIndex, stable_graph::StableGraph};
7use std::time::Instant;
8use std::{
9 collections::HashMap,
10 fmt, io,
11 path::{Path, PathBuf},
12 sync::Arc,
13 time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16pub type TreeIndex = NodeIndex;
18pub type Tree = StableGraph<EntryData, (), Directed>;
20
21#[derive(Eq, PartialEq, Clone)]
23pub struct EntryData {
24 pub name: PathBuf,
26 pub size: u128,
29 pub mtime: SystemTime,
31 pub entry_count: Option<u64>,
33 pub metadata_io_error: bool,
35 pub is_dir: bool,
37}
38
39impl Default for EntryData {
40 fn default() -> EntryData {
41 EntryData {
42 name: PathBuf::default(),
43 size: u128::default(),
44 mtime: UNIX_EPOCH,
45 entry_count: None,
46 metadata_io_error: bool::default(),
47 is_dir: false,
48 }
49 }
50}
51
52impl fmt::Debug for EntryData {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.debug_struct("EntryData")
55 .field("name", &self.name)
56 .field("size", &self.size)
57 .field("entry_count", &self.entry_count)
58 .field("metadata_io_error", &self.metadata_io_error)
60 .finish()
61 }
62}
63
64#[derive(Debug)]
66pub struct Traversal {
67 pub tree: Tree,
69 pub root_index: TreeIndex,
71 pub start_time: Instant,
73 pub cost: Option<Duration>,
75}
76
77impl Default for Traversal {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl Traversal {
84 #[must_use]
86 pub fn new() -> Self {
87 let mut tree = Tree::new();
88 let root_index = tree.add_node(EntryData::default());
89 Self {
90 tree,
91 root_index,
92 start_time: Instant::now(),
93 cost: None,
94 }
95 }
96
97 #[must_use]
99 pub fn is_costly(&self) -> bool {
100 self.cost.is_none_or(|d| d.as_secs_f32() > 10.0)
101 }
102}
103
104#[derive(Clone, Copy)]
106pub struct TraversalStats {
107 pub entries_traversed: u64,
109 pub start: std::time::Instant,
111 pub elapsed: Option<std::time::Duration>,
113 pub io_errors: u64,
115 pub total_bytes: Option<u128>,
117}
118
119impl Default for TraversalStats {
120 fn default() -> Self {
121 Self {
122 entries_traversed: 0,
123 start: std::time::Instant::now(),
124 elapsed: None,
125 io_errors: 0,
126 total_bytes: None,
127 }
128 }
129}
130
131pub struct TraversalEntry(crate::walk::Entry);
133
134pub enum TraversalEvent {
136 Entry(io::Result<TraversalEntry>, Arc<PathBuf>, u64),
138 Finished(u64),
140}
141
142pub struct BackgroundTraversal {
144 walk_options: WalkOptions,
145 pub root_idx: TreeIndex,
147 pub stats: TraversalStats,
149 nodes_by_path: HashMap<(usize, PathBuf), TreeIndex>,
151 inodes: InodeFilter,
152 throttle: Option<Throttle>,
153 skip_root: bool,
154 use_root_path: bool,
155 pub event_rx: Receiver<TraversalEvent>,
157}
158
159impl BackgroundTraversal {
160 pub fn start(
163 root_idx: TreeIndex,
164 walk_options: &WalkOptions,
165 input: Vec<PathBuf>,
166 pattern_roots: Option<&[PathBuf]>,
167 skip_root: bool,
168 use_root_path: bool,
169 ) -> anyhow::Result<BackgroundTraversal> {
170 let (entry_tx, entry_rx) = crossbeam::channel::bounded(100);
171 let pattern_roots = pattern_roots.map(<[PathBuf]>::to_owned);
172 std::thread::Builder::new()
173 .name("dua-fs-walk-dispatcher".to_string())
174 .spawn({
175 let walk_options = walk_options.clone();
176 move || {
177 let mut io_errors = 0;
178 let (mut root_paths, mut device_ids, mut walk_roots) = (
179 Vec::with_capacity(input.len()),
180 Vec::with_capacity(input.len()),
181 Vec::with_capacity(input.len()),
182 );
183 for root_path in input {
184 log::info!("Walking {}", root_path.display());
185 let pattern_root = pattern_roots.as_deref().map(|pattern_roots| {
186 pattern_roots
187 .iter()
188 .filter(|candidate| root_path.starts_with(candidate))
189 .max_by_key(|candidate| candidate.components().count())
190 .cloned()
191 .unwrap_or_else(|| root_path.clone())
192 });
193 let device_id = if walk_options.cross_filesystems {
194 0
195 } else {
196 let Ok(device_id) = crossdev::init(&root_path) else {
197 io_errors += 1;
199 continue;
200 };
201 device_id
202 };
203 walk_roots.push(WalkRoot {
204 index: walk_roots.len(),
205 pattern_root,
206 path: root_path.clone(),
207 device_id,
208 });
209 device_ids.push(device_id);
210 root_paths.push(Arc::new(root_path));
211 }
212
213 for (root, event) in walk_options.iter_from_paths(
214 walk_roots,
215 skip_root,
216 crate::walk::Order::ParentFirst,
217 ) {
218 let crate::walk::RootEvent::Entry(entry) = event else {
219 continue;
220 };
221 if entry_tx
222 .send(TraversalEvent::Entry(
223 entry.map(TraversalEntry),
224 Arc::clone(&root_paths[root]),
225 device_ids[root],
226 ))
227 .is_err()
228 {
229 return;
232 }
233 }
234 if entry_tx.send(TraversalEvent::Finished(io_errors)).is_err() {
235 log::error!("Failed to send TraversalEvents::Finished event");
236 }
237 }
238 })?;
239
240 Ok(Self {
241 walk_options: walk_options.clone(),
242 root_idx,
243 stats: TraversalStats::default(),
244 nodes_by_path: HashMap::new(),
245 inodes: InodeFilter::default(),
246 throttle: Some(Throttle::new(Duration::from_millis(250), None)),
247 skip_root,
248 use_root_path,
249 event_rx: entry_rx,
250 })
251 }
252
253 #[expect(
266 clippy::too_many_lines,
267 reason = "event integration keeps tree updates atomic"
268 )]
269 pub fn integrate_traversal_event(
270 &mut self,
271 traversal: &mut Traversal,
272 event: TraversalEvent,
273 ) -> Option<bool> {
274 match event {
275 TraversalEvent::Entry(entry, root_path, device_id) => {
276 let root = Arc::as_ptr(&root_path) as usize;
277 self.stats.entries_traversed += 1;
278 let mut data = EntryData::default();
279 match entry {
280 Ok(TraversalEntry(entry)) => {
281 let walk_depth = entry.depth;
282 if self.skip_root {
283 data.name = entry.file_name.clone().into();
284 } else {
285 data.name = if walk_depth < 1 && self.use_root_path {
286 (*root_path).clone()
287 } else {
288 entry.file_name.clone().into()
289 }
290 }
291
292 let mut file_size = 0u128;
293 let mut mtime: SystemTime = UNIX_EPOCH;
294 data.is_dir = entry.file_type.is_dir();
295 if let Ok(m) = &entry.metadata {
296 if self.walk_options.count_hard_links
297 || self.inodes.add(m)
298 && (self.walk_options.cross_filesystems
299 || crossdev::is_same_device(device_id, m))
300 {
301 if self.walk_options.apparent_size {
302 file_size = u128::from(m.len());
303 } else {
304 file_size =
305 u128::from(
306 size_on_disk(
307 &entry.parent_path,
308 &data.name,
309 m,
310 data.is_dir,
311 )
312 .unwrap_or_else(|_| {
313 self.stats.io_errors += 1;
314 data.metadata_io_error = true;
315 0
316 }),
317 );
318 }
319 } else {
320 data.entry_count = Some(0);
321 }
322
323 if let Ok(modified) = m.modified() {
324 mtime = modified;
325 } else {
326 self.stats.io_errors += 1;
327 data.metadata_io_error = true;
328 }
329 } else {
330 self.stats.io_errors += 1;
331 data.metadata_io_error = true;
332 }
333
334 data.mtime = mtime;
335 data.size = file_size;
336 if data.is_dir {
337 data.entry_count = Some(1);
338 }
339 let entry_count = u64::from(data.is_dir || data.entry_count != Some(0));
340
341 let parent_index = if walk_depth == 0 {
342 self.root_idx
343 } else {
344 if self.skip_root {
345 self.nodes_by_path
346 .entry((root, (*root_path).clone()))
347 .or_insert(self.root_idx);
348 }
349 *self
350 .nodes_by_path
351 .get(&(root, entry.parent_path.to_path_buf()))
352 .expect("parent entries are emitted before their children")
353 };
354 let entry_index = traversal.tree.add_node(data);
355 traversal.tree.add_edge(parent_index, entry_index, ());
356 if traversal.tree[entry_index].is_dir {
357 self.nodes_by_path.insert((root, entry.path()), entry_index);
358 }
359
360 let mut ancestor = Some(parent_index);
361 while let Some(index) = ancestor {
362 ancestor = traversal
363 .tree
364 .neighbors_directed(index, Direction::Incoming)
365 .next();
366 let entry = &mut traversal.tree[index];
367 entry.size += file_size;
368 *entry.entry_count.get_or_insert(0) += entry_count;
369 }
370 }
371 Err(_) => self.stats.io_errors += 1,
372 }
373
374 if self.throttle.as_ref().is_some_and(|t| t.can_update()) {
375 return Some(false);
376 }
377 }
378 TraversalEvent::Finished(io_errors) => {
379 self.stats.io_errors += io_errors;
380 self.throttle = None;
381 let root_size = traversal.tree[self.root_idx].size;
382 self.nodes_by_path = HashMap::new();
383 self.stats.total_bytes = Some(root_size);
384 self.stats.elapsed = Some(self.stats.start.elapsed());
385
386 return Some(true);
387 }
388 }
389 None
390 }
391}
392
393#[cfg(not(windows))]
394fn size_on_disk(
396 _parent: &Path,
397 name: &Path,
398 meta: &crate::walk::Metadata,
399 _is_dir: bool,
400) -> io::Result<u64> {
401 name.size_on_disk_fast(meta)
402}
403
404#[cfg(windows)]
405#[allow(clippy::unnecessary_wraps)]
407fn size_on_disk(
408 _parent: &Path,
409 _name: &Path,
410 meta: &crate::walk::Metadata,
411 is_dir: bool,
412) -> io::Result<u64> {
413 Ok(if is_dir { 0 } else { meta.allocated_size() })
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn ancestor_sizes_update_before_traversal_finishes() {
422 let dir = tempfile::tempdir().unwrap();
423 std::fs::create_dir(dir.path().join("nested")).unwrap();
424 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
425
426 let mut traversal = Traversal::new();
427 let mut background = BackgroundTraversal::start(
428 traversal.root_index,
429 &WalkOptions {
430 threads: 2,
431 count_hard_links: true,
432 apparent_size: true,
433 cross_filesystems: true,
434 ignore_dirs: std::collections::BTreeSet::default(),
435 ignore_patterns: None,
436 },
437 vec![dir.path().to_owned()],
438 None,
439 false,
440 false,
441 )
442 .unwrap();
443
444 loop {
445 let event = background.event_rx.recv().unwrap();
446 let is_file = matches!(
447 &event,
448 TraversalEvent::Entry(Ok(TraversalEntry(entry)), _, _)
449 if entry.file_name == "file"
450 );
451 background.integrate_traversal_event(&mut traversal, event);
452 if is_file {
453 let root_size = traversal.tree[traversal.root_index].size;
454 assert!(
455 root_size >= 7,
456 "root size should include the 7-byte nested file, got {root_size}"
457 );
458 let nested_size = traversal
459 .tree
460 .node_weights()
461 .find(|entry| entry.name == Path::new("nested"))
462 .unwrap()
463 .size;
464 assert!(
465 nested_size >= 7,
466 "nested directory size should include its 7-byte file, got {nested_size}"
467 );
468 break;
469 }
470 }
471 }
472
473 #[test]
474 fn duplicate_roots_keep_their_own_children() {
475 let dir = tempfile::tempdir().unwrap();
476 std::fs::write(dir.path().join("file"), b"content").unwrap();
477 let mut traversal = Traversal::new();
478 let mut background = BackgroundTraversal::start(
479 traversal.root_index,
480 &WalkOptions {
481 threads: 1,
482 count_hard_links: true,
483 apparent_size: true,
484 cross_filesystems: true,
485 ignore_dirs: std::collections::BTreeSet::default(),
486 ignore_patterns: None,
487 },
488 vec![dir.path().to_owned(), dir.path().to_owned()],
489 None,
490 false,
491 false,
492 )
493 .unwrap();
494
495 while !background
496 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
497 .unwrap_or(false)
498 {}
499
500 let roots = traversal
501 .tree
502 .neighbors_directed(traversal.root_index, Direction::Outgoing)
503 .collect::<Vec<_>>();
504 assert_eq!(roots.len(), 2);
505 for root in roots {
506 assert_eq!(
507 traversal
508 .tree
509 .neighbors_directed(root, Direction::Outgoing)
510 .count(),
511 1
512 );
513 }
514 }
515
516 #[cfg(unix)]
517 #[test]
518 fn root_device_error_is_reported() {
519 use std::os::unix::fs::symlink;
520
521 let dir = tempfile::tempdir().unwrap();
522 let root = dir.path().join("dangling");
523 let valid = dir.path().join("valid");
524 symlink(dir.path().join("missing"), &root).unwrap();
525 std::fs::write(&valid, b"content").unwrap();
526 let mut traversal = Traversal::new();
527 let mut background = BackgroundTraversal::start(
528 traversal.root_index,
529 &WalkOptions {
530 threads: 1,
531 count_hard_links: true,
532 apparent_size: true,
533 cross_filesystems: false,
534 ignore_dirs: std::collections::BTreeSet::default(),
535 ignore_patterns: None,
536 },
537 vec![root.clone(), valid.clone()],
538 None,
539 false,
540 false,
541 )
542 .unwrap();
543
544 while !background
545 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
546 .unwrap_or(false)
547 {}
548
549 assert_eq!(background.stats.io_errors, 1);
550 }
551
552 #[test]
553 fn size_of_entry_data() {
554 assert!(
555 std::mem::size_of::<EntryData>() <= 80,
556 "the size of this ({}) should not exceed 80 as it affects overall memory consumption",
557 std::mem::size_of::<EntryData>()
558 );
559 }
560}