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#[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
100fn 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
139fn 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 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}