1use crate::aggregate::{TraversalProgress, output_colored_path};
2use crate::snapshot::Replay;
3#[cfg(test)]
4use crate::traverse::EntryData;
5use crate::traverse::{BackgroundTraversal, Traversal, Tree, TreeIndex};
6use crate::{ByteFormat, WalkOptions, WalkResult};
7use anyhow::{Context, Result};
8use owo_colors::AnsiColors as Color;
9use std::io;
10use std::path::PathBuf;
11
12#[allow(clippy::too_many_arguments)]
21pub fn aggregate_tree(
22 out: (impl io::Write, bool),
23 err: Option<impl io::Write>,
24 walk_options: WalkOptions,
25 byte_format: ByteFormat,
26 paths: Vec<PathBuf>,
27 max_depth: usize,
28 compute_total: bool,
29 sort_by_size_in_bytes: bool,
30) -> Result<WalkResult> {
31 let (mut out, out_supports_colors) = out;
32 let output_options = (byte_format, out_supports_colors);
33 let mut traversal = Traversal::new();
34 if paths.is_empty() {
35 return Ok(WalkResult::default());
36 }
37
38 let pattern_roots = walk_options
39 .ignore_patterns
40 .as_ref()
41 .map(|_| paths.as_slice());
42 let mut background = BackgroundTraversal::start(
43 traversal.root_index,
44 &walk_options,
45 paths.clone(),
46 pattern_roots,
47 false,
48 true,
49 )?
50 .retain_depth(Some(max_depth));
51 let mut progress = TraversalProgress::new(err);
52
53 while let Ok(event) = background.event_rx.recv() {
54 let finished = background
55 .integrate_traversal_event(&mut traversal, event)
56 .unwrap_or(false);
57 progress.update(background.stats.entries_traversed);
58 if finished {
59 break;
60 }
61 }
62 progress.clear();
63
64 let num_errors = background.stats.io_errors;
65 let mut roots = background
66 .root_nodes
67 .into_iter()
68 .collect::<Option<Vec<_>>>()
69 .context("traversal did not produce a node for every root")?;
70 write_aggregate_tree(
71 &mut out,
72 &traversal,
73 &mut roots,
74 max_depth,
75 compute_total,
76 sort_by_size_in_bytes,
77 output_options,
78 num_errors,
79 )?;
80
81 Ok(WalkResult { num_errors })
82}
83
84#[allow(clippy::too_many_arguments)]
89pub fn aggregate_tree_from_traversal(
90 out: (impl io::Write, bool),
91 traversal: &Traversal,
92 roots: &[TreeIndex],
93 byte_format: ByteFormat,
94 max_depth: usize,
95 compute_total: bool,
96 sort_by_size_in_bytes: bool,
97) -> Result<WalkResult> {
98 let (mut out, out_supports_colors) = out;
99 let num_errors = metadata_io_error_count(&traversal.tree, roots);
100 let mut roots = roots.to_vec();
101 write_aggregate_tree(
102 &mut out,
103 traversal,
104 &mut roots,
105 max_depth,
106 compute_total,
107 sort_by_size_in_bytes,
108 (byte_format, out_supports_colors),
109 num_errors,
110 )?;
111 Ok(WalkResult { num_errors })
112}
113
114#[allow(clippy::too_many_arguments)]
116pub fn aggregate_tree_from_replay<R: io::Read + io::Seek>(
117 out: (impl io::Write, bool),
118 replay: &mut Replay<R>,
119 byte_format: ByteFormat,
120 max_depth: usize,
121 compute_total: bool,
122 sort_by_size_in_bytes: bool,
123) -> Result<WalkResult> {
124 let mut num_errors = 0u64;
125 let mut traversal = Traversal::new();
126 let mut parents = Vec::new();
127 let mut roots = Vec::new();
128 replay.for_each_entry(|entry| {
129 num_errors = num_errors.saturating_add(u64::from(entry.data.metadata_io_error));
130 if entry.depth > max_depth {
131 return Ok(());
132 }
133 parents.truncate(entry.depth);
134 let parent = parents.last().copied().unwrap_or(traversal.root_index);
135 let node = traversal
136 .tree
137 .try_add_child_native(parent, entry.native_name, entry.data)
138 .map_err(|err| anyhow::anyhow!("could not add snapshot entry: {err}"))?;
139 if entry.depth == 0 {
140 roots
141 .try_reserve(1)
142 .context("could not grow snapshot root table")?;
143 roots.push(node);
144 }
145 parents
146 .try_reserve(1)
147 .context("could not grow snapshot ancestor stack")?;
148 parents.push(node);
149 Ok(())
150 })?;
151
152 let (mut out, out_supports_colors) = out;
153 write_aggregate_tree(
154 &mut out,
155 &traversal,
156 &mut roots,
157 max_depth,
158 compute_total,
159 sort_by_size_in_bytes,
160 (byte_format, out_supports_colors),
161 num_errors,
162 )?;
163 Ok(WalkResult { num_errors })
164}
165
166#[allow(clippy::too_many_arguments)]
167fn write_aggregate_tree(
168 out: &mut impl io::Write,
169 traversal: &Traversal,
170 roots: &mut [TreeIndex],
171 max_depth: usize,
172 compute_total: bool,
173 sort_by_size_in_bytes: bool,
174 output_options: (ByteFormat, bool),
175 num_errors: u64,
176) -> io::Result<()> {
177 if sort_by_size_in_bytes {
178 roots.sort_by_key(|root| traversal.tree.data(*root).map(|entry| entry.size));
179 }
180 let mut total = 0u128;
181 for root in roots.iter() {
182 total += traversal
183 .tree
184 .data(*root)
185 .expect("traversal roots exist")
186 .size;
187 write_subtree(
188 out,
189 &traversal.tree,
190 *root,
191 0,
192 max_depth,
193 sort_by_size_in_bytes,
194 output_options,
195 )?;
196 }
197
198 if roots.len() > 1 && compute_total {
199 write_entry(out, "total", total, false, num_errors, 0, output_options)?;
200 }
201 Ok(())
202}
203
204pub(crate) fn metadata_io_error_count(tree: &Tree, roots: &[TreeIndex]) -> u64 {
205 let mut errors = 0u64;
206 let mut pending = roots.to_vec();
207 while let Some(index) = pending.pop() {
208 errors = errors.saturating_add(u64::from(
209 tree.data(index)
210 .expect("traversal entry exists")
211 .metadata_io_error,
212 ));
213 pending.extend(tree.children(index));
214 }
215 errors
216}
217
218fn write_subtree(
220 out: &mut impl io::Write,
221 tree: &Tree,
222 index: TreeIndex,
223 depth: usize,
224 max_depth: usize,
225 sort_by_size_in_bytes: bool,
226 output_options: (ByteFormat, bool),
227) -> io::Result<()> {
228 let mut pending = vec![(index, depth)];
229 while let Some((index, depth)) = pending.pop() {
230 let entry = tree.entry(index).expect("traversal entry exists");
231 let name = entry.name.to_string_lossy();
232 write_entry(
233 out,
234 &name,
235 entry.size,
236 entry.is_dir,
237 u64::from(entry.metadata_io_error),
238 depth,
239 output_options,
240 )?;
241
242 if depth < max_depth {
243 pending.extend(
244 sorted_children(tree, index, sort_by_size_in_bytes)
245 .into_iter()
246 .rev()
247 .map(|child| (child, depth + 1)),
248 );
249 }
250 }
251 Ok(())
252}
253
254fn sorted_children(tree: &Tree, index: TreeIndex, sort_by_size_in_bytes: bool) -> Vec<TreeIndex> {
257 let mut children: Vec<TreeIndex> = tree.children(index).collect();
258 children.reverse();
260 if sort_by_size_in_bytes {
261 children.sort_by_key(|child| tree.data(*child).map(|entry| entry.size));
262 }
263 children
264}
265
266fn write_entry(
267 out: &mut impl io::Write,
268 name: &str,
269 num_bytes: u128,
270 is_dir: bool,
271 num_errors: u64,
272 indent_level: usize,
273 (byte_format, out_supports_colors): (ByteFormat, bool),
274) -> io::Result<()> {
275 output_colored_path(
276 out,
277 out_supports_colors,
278 format!("{}{name}", " ".repeat(indent_level)),
279 num_bytes,
280 num_errors,
281 is_dir.then_some(Color::Cyan),
282 byte_format,
283 )
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use bstr::ByteSlice;
290
291 fn walk_options() -> WalkOptions {
292 WalkOptions {
293 threads: 1,
294 count_hard_links: true,
295 apparent_size: true,
296 cross_filesystems: true,
297 ignore_dirs: std::collections::BTreeSet::default(),
298 ignore_patterns: None,
299 metadata_options: crate::TraversalOptions::default(),
300 }
301 }
302
303 fn lines(out: &[u8]) -> Vec<String> {
304 std::str::from_utf8(out)
305 .unwrap()
306 .lines()
307 .map(str::to_owned)
308 .collect()
309 }
310
311 #[test]
312 fn depth_limits_how_far_the_tree_descends() {
313 let dir = tempfile::tempdir().unwrap();
314 std::fs::create_dir(dir.path().join("nested")).unwrap();
315 std::fs::write(dir.path().join("nested/deep"), b"1234567890").unwrap();
316
317 let mut shallow = Vec::new();
318 aggregate_tree(
319 (&mut shallow, false),
320 None::<Vec<u8>>,
321 walk_options(),
322 ByteFormat::Bytes,
323 vec![dir.path().to_owned()],
324 0,
325 true,
326 true,
327 )
328 .unwrap();
329 let shallow = lines(&shallow);
330 assert_eq!(
331 shallow.len(),
332 1,
333 "a depth of zero prints only the given root: {shallow:?}"
334 );
335 assert!(shallow[0].contains(&dir.path().to_string_lossy().into_owned()));
336
337 let mut deep = Vec::new();
338 aggregate_tree(
339 (&mut deep, false),
340 None::<Vec<u8>>,
341 walk_options(),
342 ByteFormat::Bytes,
343 vec![dir.path().to_owned()],
344 2,
345 true,
346 true,
347 )
348 .unwrap();
349 let deep = lines(&deep);
350 assert!(
351 deep.iter().any(|line| line.contains("nested")),
352 "the nested directory shows up once we go deeper: {deep:?}"
353 );
354 assert!(
355 deep.iter().any(|line| line.contains("deep")),
356 "so does the file inside it: {deep:?}"
357 );
358 assert!(
359 deep.iter().any(|line| line.contains(" nested")),
360 "children are indented below their parent: {deep:?}"
361 );
362 }
363
364 #[test]
365 fn children_are_sorted_by_size_ascending_by_default() {
366 let dir = tempfile::tempdir().unwrap();
367 std::fs::write(dir.path().join("small"), b"1").unwrap();
368 std::fs::write(dir.path().join("large"), vec![0u8; 4096]).unwrap();
369
370 let mut out = Vec::new();
371 aggregate_tree(
372 (&mut out, false),
373 None::<Vec<u8>>,
374 walk_options(),
375 ByteFormat::Bytes,
376 vec![dir.path().to_owned()],
377 1,
378 true,
379 true,
380 )
381 .unwrap();
382 let out = String::from_utf8(out).unwrap();
383 let small = out.find("small").expect("small file is listed");
384 let large = out.find("large").expect("large file is listed");
385 assert!(small < large, "the smaller child is printed first: {out:?}");
386 }
387
388 #[test]
389 fn multiple_roots_get_a_total() {
390 let dir = tempfile::tempdir().unwrap();
391 std::fs::write(dir.path().join("a"), b"aa").unwrap();
392 std::fs::write(dir.path().join("b"), b"bbbb").unwrap();
393
394 let mut with_total = Vec::new();
395 aggregate_tree(
396 (&mut with_total, false),
397 None::<Vec<u8>>,
398 walk_options(),
399 ByteFormat::Bytes,
400 vec![dir.path().join("a"), dir.path().join("b")],
401 0,
402 true,
403 false,
404 )
405 .unwrap();
406 assert!(
407 String::from_utf8(with_total).unwrap().contains("total"),
408 "several roots are summed up"
409 );
410
411 let mut without_total = Vec::new();
412 aggregate_tree(
413 (&mut without_total, false),
414 None::<Vec<u8>>,
415 walk_options(),
416 ByteFormat::Bytes,
417 vec![dir.path().join("a"), dir.path().join("b")],
418 0,
419 false,
420 false,
421 )
422 .unwrap();
423 assert!(
424 !String::from_utf8(without_total).unwrap().contains("total"),
425 "no total line when it is turned off"
426 );
427 }
428
429 #[test]
430 fn failed_roots_are_printed_in_input_order() {
431 let dir = tempfile::tempdir().unwrap();
432 let missing = dir.path().join("missing");
433 let valid = dir.path().join("valid");
434 std::fs::write(&valid, b"content").unwrap();
435
436 let mut out = Vec::new();
437 let result = aggregate_tree(
438 (&mut out, false),
439 None::<Vec<u8>>,
440 walk_options(),
441 ByteFormat::Bytes,
442 vec![missing.clone(), valid.clone()],
443 0,
444 true,
445 false,
446 )
447 .unwrap();
448 let out = lines(&out);
449
450 assert_eq!(result.num_errors, 1);
451 assert!(out[0].contains(&missing.to_string_lossy().into_owned()));
452 assert!(out[0].contains("<1 IO Error>"));
453 assert!(out[1].contains(&valid.to_string_lossy().into_owned()));
454 assert!(out[2].contains("total <1 IO Error>"));
455 }
456
457 #[test]
458 fn completed_traversal_supports_depth_sorting_totals_and_errors() {
459 let mut traversal = Traversal::new();
460 let large = traversal.tree.add_child(
461 traversal.root_index,
462 "large",
463 EntryData {
464 size: 9,
465 is_dir: true,
466 ..EntryData::default()
467 },
468 );
469 traversal.tree.add_child(
470 large,
471 "hidden",
472 EntryData {
473 size: 9,
474 metadata_io_error: true,
475 ..EntryData::default()
476 },
477 );
478 let small = traversal.tree.add_child(
479 traversal.root_index,
480 "small",
481 EntryData {
482 size: 2,
483 ..EntryData::default()
484 },
485 );
486
487 let mut out = Vec::new();
488 let result = aggregate_tree_from_traversal(
489 (&mut out, false),
490 &traversal,
491 &[large, small],
492 ByteFormat::Bytes,
493 0,
494 true,
495 true,
496 )
497 .unwrap();
498 let mut bytes = Vec::new();
499 crate::snapshot::write(&mut bytes, &traversal, &[large, small], None).unwrap();
500 let mut replay = Replay::new(std::io::Cursor::new(bytes)).unwrap();
501 let mut replayed = Vec::new();
502 let replayed_result = aggregate_tree_from_replay(
503 (&mut replayed, false),
504 &mut replay,
505 ByteFormat::Bytes,
506 0,
507 true,
508 true,
509 )
510 .unwrap();
511 assert_eq!(replayed, out);
512 assert_eq!(replayed_result.num_errors, result.num_errors);
513 insta::assert_snapshot!(out.as_bstr(), "depth 0, size-sorted, with total and IO error", @r"
514 2 b small
515 9 b large
516 11 b total <1 IO Error>
517 ");
518 assert_eq!(result.num_errors, 1);
519 }
520}