1use crate::{Throttle, WalkOptions, crossdev, inodefilter::InodeFilter};
2
3use crossbeam::channel::Receiver;
4use filesize::PathExt;
5use petgraph::{Directed, Direction, graph::NodeIndex, stable_graph::StableGraph};
6use std::time::Instant;
7use std::{
8 collections::HashMap,
9 fmt,
10 fs::Metadata,
11 io,
12 path::{Path, PathBuf},
13 sync::Arc,
14 time::{Duration, SystemTime, UNIX_EPOCH},
15};
16
17pub type TreeIndex = NodeIndex;
19pub type Tree = StableGraph<EntryData, (), Directed>;
21
22#[derive(Eq, PartialEq, Clone)]
24pub struct EntryData {
25 pub name: PathBuf,
27 pub size: u128,
30 pub mtime: SystemTime,
32 pub entry_count: Option<u64>,
34 pub metadata_io_error: bool,
36 pub is_dir: bool,
38}
39
40impl Default for EntryData {
41 fn default() -> EntryData {
42 EntryData {
43 name: PathBuf::default(),
44 size: u128::default(),
45 mtime: UNIX_EPOCH,
46 entry_count: None,
47 metadata_io_error: bool::default(),
48 is_dir: false,
49 }
50 }
51}
52
53impl fmt::Debug for EntryData {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.debug_struct("EntryData")
56 .field("name", &self.name)
57 .field("size", &self.size)
58 .field("entry_count", &self.entry_count)
59 .field("metadata_io_error", &self.metadata_io_error)
61 .finish()
62 }
63}
64
65#[derive(Debug)]
67pub struct Traversal {
68 pub tree: Tree,
70 pub root_index: TreeIndex,
72 pub start_time: Instant,
74 pub cost: Option<Duration>,
76}
77
78impl Default for Traversal {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl Traversal {
85 #[must_use]
87 pub fn new() -> Self {
88 let mut tree = Tree::new();
89 let root_index = tree.add_node(EntryData::default());
90 Self {
91 tree,
92 root_index,
93 start_time: Instant::now(),
94 cost: None,
95 }
96 }
97
98 #[must_use]
100 pub fn is_costly(&self) -> bool {
101 self.cost.is_none_or(|d| d.as_secs_f32() > 10.0)
102 }
103}
104
105#[derive(Clone, Copy)]
107pub struct TraversalStats {
108 pub entries_traversed: u64,
110 pub start: std::time::Instant,
112 pub elapsed: Option<std::time::Duration>,
114 pub io_errors: u64,
116 pub total_bytes: Option<u128>,
118}
119
120impl Default for TraversalStats {
121 fn default() -> Self {
122 Self {
123 entries_traversed: 0,
124 start: std::time::Instant::now(),
125 elapsed: None,
126 io_errors: 0,
127 total_bytes: None,
128 }
129 }
130}
131
132pub struct TraversalEntry(crate::walk::Entry);
134
135pub enum TraversalEvent {
137 Entry(io::Result<TraversalEntry>, Arc<PathBuf>, u64),
139 Finished(u64),
141}
142
143pub struct BackgroundTraversal {
145 walk_options: WalkOptions,
146 pub root_idx: TreeIndex,
148 pub stats: TraversalStats,
150 nodes_by_path: HashMap<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 skip_root: bool,
167 use_root_path: bool,
168 ) -> anyhow::Result<BackgroundTraversal> {
169 let (entry_tx, entry_rx) = crossbeam::channel::bounded(100);
170 std::thread::Builder::new()
171 .name("dua-fs-walk-dispatcher".to_string())
172 .spawn({
173 let walk_options = walk_options.clone();
174 let mut io_errors: u64 = 0;
175 move || {
176 for root_path in input {
177 log::info!("Walking {}", root_path.display());
178 let Ok(device_id) = crossdev::init(root_path.as_ref()) else {
179 io_errors += 1;
180 continue;
181 };
182
183 let root_path = Arc::new(root_path);
184 for entry in walk_options.iter_from_path(
185 root_path.as_ref(),
186 device_id,
187 skip_root,
188 crate::walk::Order::ParentFirst,
189 ) {
190 if entry_tx
191 .send(TraversalEvent::Entry(
192 entry.map(TraversalEntry),
193 Arc::clone(&root_path),
194 device_id,
195 ))
196 .is_err()
197 {
198 return;
201 }
202 }
203 }
204 if entry_tx.send(TraversalEvent::Finished(io_errors)).is_err() {
205 log::error!("Failed to send TraversalEvents::Finished event");
206 }
207 }
208 })?;
209
210 Ok(Self {
211 walk_options: walk_options.clone(),
212 root_idx,
213 stats: TraversalStats::default(),
214 nodes_by_path: HashMap::new(),
215 inodes: InodeFilter::default(),
216 throttle: Some(Throttle::new(Duration::from_millis(250), None)),
217 skip_root,
218 use_root_path,
219 event_rx: entry_rx,
220 })
221 }
222
223 #[expect(
236 clippy::too_many_lines,
237 reason = "event integration keeps tree updates atomic"
238 )]
239 pub fn integrate_traversal_event(
240 &mut self,
241 traversal: &mut Traversal,
242 event: TraversalEvent,
243 ) -> Option<bool> {
244 match event {
245 TraversalEvent::Entry(entry, root_path, device_id) => {
246 self.stats.entries_traversed += 1;
247 let mut data = EntryData::default();
248 match entry {
249 Ok(TraversalEntry(entry)) => {
250 let walk_depth = entry.depth;
251 if self.skip_root {
252 data.name = entry.file_name.clone().into();
253 } else {
254 data.name = if walk_depth < 1 && self.use_root_path {
255 (*root_path).clone()
256 } else {
257 entry.file_name.clone().into()
258 }
259 }
260
261 let mut file_size = 0u128;
262 let mut mtime: SystemTime = UNIX_EPOCH;
263 data.is_dir = entry.file_type.is_dir();
264 if let Ok(m) = &entry.metadata {
265 if self.walk_options.count_hard_links
266 || self.inodes.add(m)
267 && (self.walk_options.cross_filesystems
268 || crossdev::is_same_device(device_id, m))
269 {
270 if self.walk_options.apparent_size {
271 file_size = u128::from(m.len());
272 } else {
273 file_size = u128::from(
274 size_on_disk(&entry.parent_path, &data.name, m)
275 .unwrap_or_else(|_| {
276 self.stats.io_errors += 1;
277 data.metadata_io_error = true;
278 0
279 }),
280 );
281 }
282 } else {
283 data.entry_count = Some(0);
284 }
285
286 if let Ok(modified) = m.modified() {
287 mtime = modified;
288 } else {
289 self.stats.io_errors += 1;
290 data.metadata_io_error = true;
291 }
292 } else {
293 self.stats.io_errors += 1;
294 data.metadata_io_error = true;
295 }
296
297 data.mtime = mtime;
298 data.size = file_size;
299 if data.is_dir {
300 data.entry_count = Some(1);
301 }
302 let entry_count = u64::from(data.is_dir || data.entry_count != Some(0));
303
304 let parent_index = if walk_depth == 0 {
305 self.root_idx
306 } else {
307 if self.skip_root {
308 self.nodes_by_path
309 .entry((*root_path).clone())
310 .or_insert(self.root_idx);
311 }
312 *self
313 .nodes_by_path
314 .get(entry.parent_path.as_ref())
315 .expect("parent entries are emitted before their children")
316 };
317 let entry_index = traversal.tree.add_node(data);
318 traversal.tree.add_edge(parent_index, entry_index, ());
319 if traversal.tree[entry_index].is_dir {
320 self.nodes_by_path.insert(entry.path(), entry_index);
321 }
322
323 let mut ancestor = Some(parent_index);
324 while let Some(index) = ancestor {
325 ancestor = traversal
326 .tree
327 .neighbors_directed(index, Direction::Incoming)
328 .next();
329 let entry = &mut traversal.tree[index];
330 entry.size += file_size;
331 *entry.entry_count.get_or_insert(0) += entry_count;
332 }
333 }
334 Err(_) => self.stats.io_errors += 1,
335 }
336
337 if self.throttle.as_ref().is_some_and(|t| t.can_update()) {
338 return Some(false);
339 }
340 }
341 TraversalEvent::Finished(io_errors) => {
342 self.stats.io_errors += io_errors;
343
344 self.throttle = None;
345 let root_size = traversal.tree[self.root_idx].size;
346 self.nodes_by_path = HashMap::new();
347 self.stats.total_bytes = Some(root_size);
348 self.stats.elapsed = Some(self.stats.start.elapsed());
349
350 return Some(true);
351 }
352 }
353 None
354 }
355}
356
357#[cfg(not(windows))]
358fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result<u64> {
360 name.size_on_disk_fast(meta)
361}
362
363#[cfg(windows)]
364fn size_on_disk(parent: &Path, name: &Path, meta: &Metadata) -> io::Result<u64> {
366 parent.join(name).size_on_disk_fast(meta)
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn ancestor_sizes_update_before_traversal_finishes() {
375 let dir = tempfile::tempdir().unwrap();
376 std::fs::create_dir(dir.path().join("nested")).unwrap();
377 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
378
379 let mut traversal = Traversal::new();
380 let mut background = BackgroundTraversal::start(
381 traversal.root_index,
382 &WalkOptions {
383 threads: 2,
384 count_hard_links: true,
385 apparent_size: true,
386 cross_filesystems: true,
387 ignore_dirs: std::collections::BTreeSet::default(),
388 },
389 vec![dir.path().to_owned()],
390 false,
391 false,
392 )
393 .unwrap();
394
395 loop {
396 let event = background.event_rx.recv().unwrap();
397 let is_file = matches!(
398 &event,
399 TraversalEvent::Entry(Ok(TraversalEntry(entry)), _, _)
400 if entry.file_name == "file"
401 );
402 background.integrate_traversal_event(&mut traversal, event);
403 if is_file {
404 let root_size = traversal.tree[traversal.root_index].size;
405 assert!(
406 root_size >= 7,
407 "root size should include the 7-byte nested file, got {root_size}"
408 );
409 let nested_size = traversal
410 .tree
411 .node_weights()
412 .find(|entry| entry.name == Path::new("nested"))
413 .unwrap()
414 .size;
415 assert!(
416 nested_size >= 7,
417 "nested directory size should include its 7-byte file, got {nested_size}"
418 );
419 break;
420 }
421 }
422 }
423
424 #[test]
425 fn size_of_entry_data() {
426 assert!(
427 std::mem::size_of::<EntryData>() <= 80,
428 "the size of this ({}) should not exceed 80 as it affects overall memory consumption",
429 std::mem::size_of::<EntryData>()
430 );
431 }
432}