Skip to main content

dua/
tree.rs

1use crate::aggregate::{TraversalProgress, output_colored_path};
2use crate::traverse::{BackgroundTraversal, EntryData, Traversal, Tree, TreeIndex};
3use crate::{ByteFormat, WalkOptions, WalkResult};
4use anyhow::{Context, Result};
5use owo_colors::AnsiColors as Color;
6use petgraph::Direction;
7use std::io;
8use std::path::PathBuf;
9
10/// Traverse `paths` and write an indented tree of their disk usage to `out`, descending up to
11/// `max_depth` levels below each given root.
12///
13/// The given roots are at depth `0`, so `max_depth` of `0` lists only them (the same set of entries the
14/// flat aggregation prints), `1` also lists their children, and higher values reveal deeper entries.
15/// When `compute_total` is set and more than one root is given, a trailing `total` line is written.
16/// `sort_by_size_in_bytes` sorts the children at each level ascending by size, otherwise they are
17/// left in the order they were discovered.
18#[allow(clippy::too_many_arguments)]
19pub fn aggregate_tree(
20    out: (impl io::Write, bool),
21    err: Option<impl io::Write>,
22    walk_options: WalkOptions,
23    byte_format: ByteFormat,
24    paths: Vec<PathBuf>,
25    max_depth: usize,
26    compute_total: bool,
27    sort_by_size_in_bytes: bool,
28) -> Result<WalkResult> {
29    let (mut out, out_supports_colors) = out;
30    let output_options = (byte_format, out_supports_colors);
31    let mut traversal = Traversal::new();
32    if paths.is_empty() {
33        return Ok(WalkResult::default());
34    }
35
36    let pattern_roots = walk_options
37        .ignore_patterns
38        .as_ref()
39        .map(|_| paths.as_slice());
40    let mut background = BackgroundTraversal::start(
41        traversal.root_index,
42        &walk_options,
43        paths.clone(),
44        pattern_roots,
45        false,
46        true,
47    )?
48    .retain_depth(Some(max_depth));
49    let mut progress = TraversalProgress::new(err);
50
51    while let Ok(event) = background.event_rx.recv() {
52        let finished = background
53            .integrate_traversal_event(&mut traversal, event)
54            .unwrap_or(false);
55        progress.update(background.stats.entries_traversed);
56        if finished {
57            break;
58        }
59    }
60    progress.clear();
61
62    let num_errors = background.stats.io_errors;
63    let mut roots = background
64        .root_nodes
65        .into_iter()
66        .collect::<Option<Vec<_>>>()
67        .context("traversal did not produce a node for every root")?;
68    if sort_by_size_in_bytes {
69        roots.sort_by_key(|root| traversal.tree[*root].size);
70    }
71    let mut total = 0u128;
72    for root in &roots {
73        total += traversal.tree[*root].size;
74        write_subtree(
75            &mut out,
76            &traversal.tree,
77            *root,
78            0,
79            max_depth,
80            sort_by_size_in_bytes,
81            output_options,
82        )?;
83    }
84
85    if roots.len() > 1 && compute_total {
86        write_entry(
87            &mut out,
88            "total",
89            total,
90            false,
91            num_errors,
92            0,
93            output_options,
94        )?;
95    }
96
97    Ok(WalkResult { num_errors })
98}
99
100/// Write `index` and, while there is depth budget left, its descendants, indented by their level.
101fn write_subtree(
102    out: &mut impl io::Write,
103    tree: &Tree,
104    index: TreeIndex,
105    depth: usize,
106    max_depth: usize,
107    sort_by_size_in_bytes: bool,
108    output_options: (ByteFormat, bool),
109) -> io::Result<()> {
110    let entry: &EntryData = &tree[index];
111    let name = entry.name.to_string_lossy();
112    write_entry(
113        out,
114        &name,
115        entry.size,
116        entry.is_dir,
117        u64::from(entry.metadata_io_error),
118        depth,
119        output_options,
120    )?;
121
122    if depth >= max_depth {
123        return Ok(());
124    }
125    for child in sorted_children(tree, index, sort_by_size_in_bytes) {
126        write_subtree(
127            out,
128            tree,
129            child,
130            depth + 1,
131            max_depth,
132            sort_by_size_in_bytes,
133            output_options,
134        )?;
135    }
136    Ok(())
137}
138
139/// Return the children of `index`, ordered by size ascending when `sort_by_size_in_bytes` is set,
140/// otherwise in the order they were discovered during the traversal.
141fn sorted_children(tree: &Tree, index: TreeIndex, sort_by_size_in_bytes: bool) -> Vec<TreeIndex> {
142    let mut children: Vec<TreeIndex> = tree
143        .neighbors_directed(index, Direction::Outgoing)
144        .collect();
145    // `petgraph` yields neighbors in the reverse of their insertion order, so undo that to recover
146    // the discovery order the walk produced.
147    children.reverse();
148    if sort_by_size_in_bytes {
149        children.sort_by_key(|child| tree[*child].size);
150    }
151    children
152}
153
154fn write_entry(
155    out: &mut impl io::Write,
156    name: &str,
157    num_bytes: u128,
158    is_dir: bool,
159    num_errors: u64,
160    indent_level: usize,
161    (byte_format, out_supports_colors): (ByteFormat, bool),
162) -> io::Result<()> {
163    output_colored_path(
164        out,
165        out_supports_colors,
166        format!("{}{name}", "  ".repeat(indent_level)),
167        num_bytes,
168        num_errors,
169        is_dir.then_some(Color::Cyan),
170        byte_format,
171    )
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    fn walk_options() -> WalkOptions {
179        WalkOptions {
180            threads: 1,
181            count_hard_links: true,
182            apparent_size: true,
183            cross_filesystems: true,
184            ignore_dirs: std::collections::BTreeSet::default(),
185            ignore_patterns: None,
186            metadata_options: crate::TraversalOptions::default(),
187        }
188    }
189
190    fn lines(out: &[u8]) -> Vec<String> {
191        std::str::from_utf8(out)
192            .unwrap()
193            .lines()
194            .map(str::to_owned)
195            .collect()
196    }
197
198    #[test]
199    fn depth_limits_how_far_the_tree_descends() {
200        let dir = tempfile::tempdir().unwrap();
201        std::fs::create_dir(dir.path().join("nested")).unwrap();
202        std::fs::write(dir.path().join("nested/deep"), b"1234567890").unwrap();
203
204        let mut shallow = Vec::new();
205        aggregate_tree(
206            (&mut shallow, false),
207            None::<Vec<u8>>,
208            walk_options(),
209            ByteFormat::Bytes,
210            vec![dir.path().to_owned()],
211            0,
212            true,
213            true,
214        )
215        .unwrap();
216        let shallow = lines(&shallow);
217        assert_eq!(
218            shallow.len(),
219            1,
220            "a depth of zero prints only the given root: {shallow:?}"
221        );
222        assert!(shallow[0].contains(&dir.path().to_string_lossy().into_owned()));
223
224        let mut deep = Vec::new();
225        aggregate_tree(
226            (&mut deep, false),
227            None::<Vec<u8>>,
228            walk_options(),
229            ByteFormat::Bytes,
230            vec![dir.path().to_owned()],
231            2,
232            true,
233            true,
234        )
235        .unwrap();
236        let deep = lines(&deep);
237        assert!(
238            deep.iter().any(|line| line.contains("nested")),
239            "the nested directory shows up once we go deeper: {deep:?}"
240        );
241        assert!(
242            deep.iter().any(|line| line.contains("deep")),
243            "so does the file inside it: {deep:?}"
244        );
245        assert!(
246            deep.iter().any(|line| line.contains("  nested")),
247            "children are indented below their parent: {deep:?}"
248        );
249    }
250
251    #[test]
252    fn children_are_sorted_by_size_ascending_by_default() {
253        let dir = tempfile::tempdir().unwrap();
254        std::fs::write(dir.path().join("small"), b"1").unwrap();
255        std::fs::write(dir.path().join("large"), vec![0u8; 4096]).unwrap();
256
257        let mut out = Vec::new();
258        aggregate_tree(
259            (&mut out, false),
260            None::<Vec<u8>>,
261            walk_options(),
262            ByteFormat::Bytes,
263            vec![dir.path().to_owned()],
264            1,
265            true,
266            true,
267        )
268        .unwrap();
269        let out = String::from_utf8(out).unwrap();
270        let small = out.find("small").expect("small file is listed");
271        let large = out.find("large").expect("large file is listed");
272        assert!(small < large, "the smaller child is printed first: {out:?}");
273    }
274
275    #[test]
276    fn multiple_roots_get_a_total() {
277        let dir = tempfile::tempdir().unwrap();
278        std::fs::write(dir.path().join("a"), b"aa").unwrap();
279        std::fs::write(dir.path().join("b"), b"bbbb").unwrap();
280
281        let mut with_total = Vec::new();
282        aggregate_tree(
283            (&mut with_total, false),
284            None::<Vec<u8>>,
285            walk_options(),
286            ByteFormat::Bytes,
287            vec![dir.path().join("a"), dir.path().join("b")],
288            0,
289            true,
290            false,
291        )
292        .unwrap();
293        assert!(
294            String::from_utf8(with_total).unwrap().contains("total"),
295            "several roots are summed up"
296        );
297
298        let mut without_total = Vec::new();
299        aggregate_tree(
300            (&mut without_total, false),
301            None::<Vec<u8>>,
302            walk_options(),
303            ByteFormat::Bytes,
304            vec![dir.path().join("a"), dir.path().join("b")],
305            0,
306            false,
307            false,
308        )
309        .unwrap();
310        assert!(
311            !String::from_utf8(without_total).unwrap().contains("total"),
312            "no total line when it is turned off"
313        );
314    }
315
316    #[test]
317    fn failed_roots_are_printed_in_input_order() {
318        let dir = tempfile::tempdir().unwrap();
319        let missing = dir.path().join("missing");
320        let valid = dir.path().join("valid");
321        std::fs::write(&valid, b"content").unwrap();
322
323        let mut out = Vec::new();
324        let result = aggregate_tree(
325            (&mut out, false),
326            None::<Vec<u8>>,
327            walk_options(),
328            ByteFormat::Bytes,
329            vec![missing.clone(), valid.clone()],
330            0,
331            true,
332            false,
333        )
334        .unwrap();
335        let out = lines(&out);
336
337        assert_eq!(result.num_errors, 1);
338        assert!(out[0].contains(&missing.to_string_lossy().into_owned()));
339        assert!(out[0].contains("<1 IO Error>"));
340        assert!(out[1].contains(&valid.to_string_lossy().into_owned()));
341        assert!(out[2].contains("total  <1 IO Error>"));
342    }
343}