Skip to main content

dua/
aggregate.rs

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
8/// Aggregate the given `paths` and write information about them to `out` in a human-readable format.
9/// If `compute_total` is set, it will write an additional line with the total size across all given `paths`.
10/// If `sort_by_size_in_bytes` is set, we will sort all sizes (ascending) before outputting them.
11pub 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/// Statistics obtained during a filesystem walk
178#[derive(Default, Debug)]
179pub struct Statistics {
180    /// The amount of entries we have seen during filesystem traversal
181    pub entries_traversed: u64,
182    /// The size of the smallest file encountered in bytes
183    pub smallest_file_in_bytes: u128,
184    /// The size of the largest file encountered in bytes
185    pub largest_file_in_bytes: u128,
186}