1use crate::{ByteFormat, InodeFilter, Throttle, WalkOptions, WalkResult, crossdev};
2use anyhow::Result;
3use filesize::PathExt;
4use owo_colors::{AnsiColors as Color, OwoColorize};
5use std::time::Duration;
6use std::{io, path::Path};
7
8pub fn aggregate(
12 mut out: impl io::Write,
13 mut err: Option<impl io::Write>,
14 walk_options: WalkOptions,
15 compute_total: bool,
16 sort_by_size_in_bytes: bool,
17 byte_format: ByteFormat,
18 paths: impl IntoIterator<Item = impl AsRef<Path>>,
19) -> Result<(WalkResult, Statistics)> {
20 let mut res = WalkResult::default();
21 let mut stats = Statistics {
22 smallest_file_in_bytes: u128::MAX,
23 ..Default::default()
24 };
25 let mut total = 0;
26 let mut num_roots = 0;
27 let mut aggregates = Vec::new();
28 let mut inodes = InodeFilter::default();
29 let progress = Throttle::new(Duration::from_millis(100), Duration::from_secs(1).into());
30
31 for path in paths {
32 num_roots += 1;
33 let mut num_bytes = 0u128;
34 let mut num_errors = 0u64;
35 let Ok(device_id) = crossdev::init(path.as_ref()) else {
36 num_errors += 1;
37 res.num_errors += 1;
38 aggregates.push((path.as_ref().to_owned(), num_bytes, num_errors));
39 continue;
40 };
41 for entry in walk_options.iter_from_path(
42 path.as_ref(),
43 device_id,
44 false,
45 crate::walk::Order::Completion,
46 ) {
47 stats.entries_traversed += 1;
48 progress.throttled(|| {
49 if let Some(err) = err.as_mut() {
50 write!(err, "Enumerating {} items\r", stats.entries_traversed).ok();
51 }
52 });
53 match entry {
54 Ok(entry) => {
55 let file_size = u128::from(match &entry.metadata {
56 Ok(m)
57 if (walk_options.count_hard_links || inodes.add(m))
58 && (walk_options.cross_filesystems
59 || crossdev::is_same_device(device_id, m)) =>
60 {
61 if walk_options.apparent_size {
62 m.len()
63 } else {
64 entry.path().size_on_disk_fast(m).unwrap_or_else(|_| {
65 num_errors += 1;
66 0
67 })
68 }
69 }
70 Ok(_) => 0,
71 Err(_) => {
72 num_errors += 1;
73 0
74 }
75 });
76 stats.largest_file_in_bytes = stats.largest_file_in_bytes.max(file_size);
77 stats.smallest_file_in_bytes = stats.smallest_file_in_bytes.min(file_size);
78 num_bytes += file_size;
79 }
80 Err(_) => num_errors += 1,
81 }
82 }
83
84 if let Some(err) = err.as_mut() {
85 write!(err, "\x1b[2K\r").ok();
86 }
87
88 if sort_by_size_in_bytes {
89 aggregates.push((path.as_ref().to_owned(), num_bytes, num_errors));
90 } else {
91 output_colored_path(
92 &mut out,
93 &path,
94 num_bytes,
95 num_errors,
96 path_color_of(&path),
97 byte_format,
98 )?;
99 }
100 total += num_bytes;
101 res.num_errors += num_errors;
102 }
103
104 if stats.entries_traversed == 0 {
105 stats.smallest_file_in_bytes = 0;
106 }
107
108 if sort_by_size_in_bytes {
109 output_sorted(&mut out, aggregates, byte_format)?;
110 }
111
112 if num_roots > 1 && compute_total {
113 output_colored_path(
114 &mut out,
115 Path::new("total"),
116 total,
117 res.num_errors,
118 None,
119 byte_format,
120 )?;
121 }
122 Ok((res, stats))
123}
124
125fn output_sorted(
126 out: &mut impl io::Write,
127 mut aggregates: Vec<(std::path::PathBuf, u128, u64)>,
128 byte_format: ByteFormat,
129) -> std::result::Result<(), io::Error> {
130 aggregates.sort_by_key(|&(_, num_bytes, _)| num_bytes);
131 for (path, num_bytes, num_errors) in aggregates {
132 output_colored_path(
133 out,
134 &path,
135 num_bytes,
136 num_errors,
137 path_color_of(&path),
138 byte_format,
139 )?;
140 }
141 Ok(())
142}
143
144fn path_color_of(path: impl AsRef<Path>) -> Option<Color> {
145 (!path.as_ref().is_file()).then_some(Color::Cyan)
146}
147
148fn output_colored_path(
149 out: &mut impl io::Write,
150 path: impl AsRef<Path>,
151 num_bytes: u128,
152 num_errors: u64,
153 path_color: Option<Color>,
154 byte_format: ByteFormat,
155) -> std::result::Result<(), io::Error> {
156 let size = byte_format.display(num_bytes).to_string();
157 let size = size.green();
158 let size_width = byte_format.width();
159 let path = path.as_ref().display();
160
161 let errors = if num_errors != 0 {
162 format!(
163 " <{num_errors} IO Error{plural_s}>",
164 plural_s = if num_errors > 1 { "s" } else { "" }
165 )
166 } else {
167 String::new()
168 };
169
170 if let Some(color) = path_color {
171 writeln!(out, "{size:>size_width$} {}{errors}", path.color(color))
172 } else {
173 writeln!(out, "{size:>size_width$} {path}{errors}")
174 }
175}
176
177#[derive(Default, Debug)]
179pub struct Statistics {
180 pub entries_traversed: u64,
182 pub smallest_file_in_bytes: u128,
184 pub largest_file_in_bytes: u128,
186}