Skip to main content

dua/
tree.rs

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/// Traverse `paths` and write an indented tree of their disk usage to `out`, descending up to
13/// `max_depth` levels below each given root.
14///
15/// The given roots are at depth `0`, so `max_depth` of `0` lists only them (the same set of entries the
16/// flat aggregation prints), `1` also lists their children, and higher values reveal deeper entries.
17/// When `compute_total` is set and more than one root is given, a trailing `total` line is written.
18/// `sort_by_size_in_bytes` sorts the children at each level ascending by size, otherwise they are
19/// left in the order they were discovered.
20#[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/// Write an already completed traversal as an indented aggregate tree.
85///
86/// `roots` must contain the traversal's top-level nodes in their original input order. The
87/// returned error count is derived from the stored metadata-error flags.
88#[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/// Replay a verified snapshot as an indented aggregate tree, retaining only displayed levels.
115#[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
218/// Write `index` and, while there is depth budget left, its descendants, indented by their level.
219fn 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
254/// Return the children of `index`, ordered by size ascending when `sort_by_size_in_bytes` is set,
255/// otherwise in the order they were discovered during the traversal.
256fn 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 are linked newest-first, so undo that to recover discovery order.
259    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}