Skip to main content

parallel_disk_usage/
app.rs

1pub mod sub;
2
3pub use sub::Sub;
4
5use crate::{
6    args::{Args, Quantity, Threads},
7    bytes_format::BytesFormat,
8    device::DeviceBoundary,
9    get_size::{GetApparentSize, GetSize},
10    hardlink,
11    json_data::{JsonData, JsonDataBody, JsonShared, JsonTree},
12    reporter::{ErrorOnlyReporter, ErrorReport, ProgressAndErrorReporter, ProgressReport},
13    runtime_error::RuntimeError,
14    size,
15    visualizer::{BarAlignment, ColumnWidthDistribution, Direction, Visualizer},
16};
17use clap::Parser;
18use hdd::any_path_is_in_hdd;
19use pipe_trait::Pipe;
20use std::{io::stdin, time::Duration};
21use sub::JsonOutputParam;
22use sysinfo::{Disk, Disks};
23
24#[cfg(unix)]
25use crate::get_size::{GetBlockCount, GetBlockSize};
26
27/// The main application.
28pub struct App {
29    /// The CLI arguments.
30    args: Args,
31}
32
33/// Geometry options that control how the chart is laid out.
34#[derive(Clone, Copy)]
35struct ChartLayout {
36    /// How the available width is distributed across the columns.
37    column_width_distribution: ColumnWidthDistribution,
38    /// Whether the tree grows from the top down or from the bottom up.
39    direction: Direction,
40    /// Whether the bars are aligned to the left or to the right.
41    bar_alignment: BarAlignment,
42}
43
44/// Tree-shaping options applied to a deserialized `--json-input` tree before visualization.
45#[derive(Clone, Copy)]
46struct JsonInputShaping {
47    /// Maximum number of levels to display.
48    max_depth: u64,
49    /// Minimal size proportion required to appear.
50    min_ratio: f32,
51    /// Whether to preserve the input order of the entries.
52    no_sort: bool,
53}
54
55impl App {
56    /// Initialize the application from the environment.
57    pub fn from_env() -> Self {
58        App {
59            args: Args::parse(),
60        }
61    }
62
63    /// Run the application.
64    pub fn run(mut self) -> Result<(), RuntimeError> {
65        // DYNAMIC DISPATCH POLICY:
66        //
67        // Errors rarely occur, therefore, using dynamic dispatch to report errors have an acceptable
68        // impact on performance.
69        //
70        // The other operations which are invoked frequently should not utilize dynamic dispatch.
71
72        let column_width_distribution = self.args.column_width_distribution();
73
74        if self.args.json_input {
75            if !self.args.files.is_empty() {
76                return Err(RuntimeError::JsonInputArgConflict);
77            }
78
79            let Args {
80                bytes_format,
81                top_down,
82                align_right,
83                max_depth,
84                min_ratio,
85                no_sort,
86                ..
87            } = self.args;
88            let layout = ChartLayout {
89                column_width_distribution,
90                direction: Direction::from_top_down(top_down),
91                bar_alignment: BarAlignment::from_align_right(align_right),
92            };
93            let shaping = JsonInputShaping {
94                max_depth: max_depth.get(),
95                min_ratio: min_ratio.into(),
96                no_sort,
97            };
98
99            let body = stdin()
100                .pipe(serde_json::from_reader::<_, JsonData>)
101                .map_err(RuntimeError::DeserializationFailure)?
102                .body;
103
104            trait VisualizeJsonTree: size::Size + Into<u64> + Send {
105                fn visualize_json_tree(
106                    tree: JsonTree<Self>,
107                    bytes_format: Self::DisplayFormat,
108                    layout: ChartLayout,
109                    shaping: JsonInputShaping,
110                ) -> Result<String, RuntimeError> {
111                    let JsonTree { tree, shared } = tree;
112                    let ChartLayout {
113                        column_width_distribution,
114                        direction,
115                        bar_alignment,
116                    } = layout;
117                    let JsonInputShaping {
118                        max_depth,
119                        min_ratio,
120                        no_sort,
121                    } = shaping;
122
123                    let mut data_tree = tree
124                        .par_try_into_tree()
125                        .map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))?
126                        .into_par_retained(|_, depth| depth + 1 < max_depth);
127                    data_tree.par_cull_insignificant_data(min_ratio);
128                    if !no_sort {
129                        data_tree
130                            .par_sort_by(|left, right| left.size().cmp(&right.size()).reverse());
131                    }
132
133                    let visualizer = Visualizer {
134                        data_tree: &data_tree,
135                        bytes_format,
136                        column_width_distribution,
137                        direction,
138                        bar_alignment,
139                    };
140
141                    let JsonShared { details, summary } = shared;
142                    let summary = summary.or_else(|| details.map(|details| details.summarize()));
143
144                    let visualization = if let Some(summary) = summary {
145                        let summary = summary.display(bytes_format);
146                        // visualizer already ends with "\n"
147                        format!("{visualizer}{summary}\n")
148                    } else {
149                        visualizer.to_string()
150                    };
151
152                    Ok(visualization)
153                }
154            }
155
156            impl<Size: size::Size + Into<u64> + Send> VisualizeJsonTree for Size {}
157
158            macro_rules! visualize {
159                ($tree:expr, $bytes_format:expr) => {
160                    VisualizeJsonTree::visualize_json_tree($tree, $bytes_format, layout, shaping)
161                };
162            }
163
164            let visualization = match body {
165                JsonDataBody::Bytes(tree) => visualize!(tree, bytes_format),
166                JsonDataBody::Blocks(tree) => visualize!(tree, ()),
167            }?;
168
169            print!("{visualization}"); // it already ends with "\n", println! isn't needed here.
170            return Ok(());
171        }
172
173        #[cfg(not(unix))]
174        if self.args.deduplicate_hardlinks {
175            return crate::runtime_error::UnsupportedFeature::DeduplicateHardlink
176                .pipe(RuntimeError::UnsupportedFeature)
177                .pipe(Err);
178        }
179
180        #[cfg(not(unix))]
181        if self.args.one_file_system {
182            return crate::runtime_error::UnsupportedFeature::OneFileSystem
183                .pipe(RuntimeError::UnsupportedFeature)
184                .pipe(Err);
185        }
186
187        let threads = match self.args.threads {
188            Threads::Auto => {
189                let disks = Disks::new_with_refreshed_list();
190                any_path_is_in_hdd::<Disk, hdd::RealFs>(&self.args.files, &disks).then(|| {
191                    eprintln!("warning: HDD detected, the thread limit will be set to 1");
192                    eprintln!("hint: You can pass --threads=max disable this behavior");
193                    1
194                })
195            }
196            Threads::Max => None,
197            Threads::Fixed(threads) => Some(threads.get()),
198        };
199
200        if let Some(threads) = threads {
201            rayon::ThreadPoolBuilder::new()
202                .num_threads(threads)
203                .build_global()
204                .unwrap_or_else(|_| eprintln!("warning: Failed to set thread limit to {threads}"));
205        }
206
207        if cfg!(unix) && self.args.deduplicate_hardlinks && self.args.files.len() > 1 {
208            // Hardlinks deduplication doesn't work properly if there are more than 1 paths pointing to
209            // the same tree or if a path points to a subtree of another path. Therefore, we must find
210            // and remove such overlapping paths before they cause problems.
211            use overlapping_arguments::{RealApi, remove_overlapping_paths};
212            remove_overlapping_paths::<RealApi>(&mut self.args.files);
213        }
214
215        let report_error = if self.args.silent_errors {
216            ErrorReport::SILENT
217        } else {
218            ErrorReport::TEXT
219        };
220
221        trait GetSizeUtils: GetSize<Size: size::Size> {
222            const INSTANCE: Self;
223            const QUANTITY: Quantity;
224            fn formatter(bytes_format: BytesFormat) -> <Self::Size as size::Size>::DisplayFormat;
225        }
226
227        impl GetSizeUtils for GetApparentSize {
228            const INSTANCE: Self = GetApparentSize;
229            const QUANTITY: Quantity = Quantity::ApparentSize;
230            #[inline]
231            fn formatter(bytes_format: BytesFormat) -> BytesFormat {
232                bytes_format
233            }
234        }
235
236        #[cfg(unix)]
237        impl GetSizeUtils for GetBlockSize {
238            const INSTANCE: Self = GetBlockSize;
239            const QUANTITY: Quantity = Quantity::BlockSize;
240            #[inline]
241            fn formatter(bytes_format: BytesFormat) -> BytesFormat {
242                bytes_format
243            }
244        }
245
246        #[cfg(unix)]
247        impl GetSizeUtils for GetBlockCount {
248            const INSTANCE: Self = GetBlockCount;
249            const QUANTITY: Quantity = Quantity::BlockCount;
250            #[inline]
251            fn formatter(_: BytesFormat) {}
252        }
253
254        trait CreateReporter<const REPORT_PROGRESS: bool>: GetSizeUtils {
255            type Reporter;
256            fn create_reporter(report_error: fn(ErrorReport)) -> Self::Reporter;
257        }
258
259        impl<SizeGetter> CreateReporter<false> for SizeGetter
260        where
261            Self: GetSizeUtils,
262        {
263            type Reporter = ErrorOnlyReporter<fn(ErrorReport)>;
264            #[inline]
265            fn create_reporter(report_error: fn(ErrorReport)) -> Self::Reporter {
266                ErrorOnlyReporter::new(report_error)
267            }
268        }
269
270        impl<SizeGetter> CreateReporter<true> for SizeGetter
271        where
272            Self: GetSizeUtils,
273            Self::Size: Into<u64> + Send + Sync,
274            ProgressReport<Self::Size>: Default + 'static,
275            u64: Into<Self::Size>,
276        {
277            type Reporter = ProgressAndErrorReporter<Self::Size, fn(ErrorReport)>;
278            #[inline]
279            fn create_reporter(report_error: fn(ErrorReport)) -> Self::Reporter {
280                ProgressAndErrorReporter::new(
281                    ProgressReport::TEXT,
282                    Duration::from_millis(100),
283                    report_error,
284                )
285            }
286        }
287
288        trait CreateHardlinksHandler<const DEDUPLICATE_HARDLINKS: bool, const REPORT_PROGRESS: bool>:
289            CreateReporter<REPORT_PROGRESS>
290        {
291            type HardlinksHandler: hardlink::RecordHardlinks<Self::Size, Self::Reporter>
292                + sub::HardlinkSubroutines<Self::Size>;
293            fn create_hardlinks_handler() -> Self::HardlinksHandler;
294        }
295
296        impl<const REPORT_PROGRESS: bool, SizeGetter> CreateHardlinksHandler<false, REPORT_PROGRESS>
297            for SizeGetter
298        where
299            Self: CreateReporter<REPORT_PROGRESS>,
300            Self::Size: Send + Sync,
301        {
302            type HardlinksHandler = hardlink::HardlinkIgnorant;
303            #[inline]
304            fn create_hardlinks_handler() -> Self::HardlinksHandler {
305                hardlink::HardlinkIgnorant
306            }
307        }
308
309        #[cfg(unix)]
310        impl<const REPORT_PROGRESS: bool, SizeGetter> CreateHardlinksHandler<true, REPORT_PROGRESS>
311            for SizeGetter
312        where
313            Self: CreateReporter<REPORT_PROGRESS>,
314            Self::Size: Send + Sync + 'static,
315            Self::Reporter: crate::reporter::Reporter<Self::Size>,
316        {
317            type HardlinksHandler = hardlink::HardlinkAware<Self::Size>;
318            #[inline]
319            fn create_hardlinks_handler() -> Self::HardlinksHandler {
320                hardlink::HardlinkAware::new()
321            }
322        }
323
324        macro_rules! run {
325            ($(
326                $(#[$variant_attrs:meta])*
327                $size_getter:ident, $progress:literal, $hardlinks:ident;
328            )*) => { match self.args {$(
329                $(#[$variant_attrs])*
330                Args {
331                    quantity: <$size_getter as GetSizeUtils>::QUANTITY,
332                    progress: $progress,
333                    #[cfg(unix)] deduplicate_hardlinks: $hardlinks,
334                    #[cfg(not(unix))] deduplicate_hardlinks: _,
335                    one_file_system,
336                    files,
337                    json_output,
338                    bytes_format,
339                    top_down,
340                    align_right,
341                    max_depth,
342                    min_ratio,
343                    no_sort,
344                    omit_json_shared_details,
345                    omit_json_shared_summary,
346                    ..
347                } => Sub {
348                    direction: Direction::from_top_down(top_down),
349                    bar_alignment: BarAlignment::from_align_right(align_right),
350                    size_getter: <$size_getter as GetSizeUtils>::INSTANCE,
351                    hardlinks_handler: <$size_getter as CreateHardlinksHandler<{ cfg!(unix) && $hardlinks }, $progress>>::create_hardlinks_handler(),
352                    device_boundary: DeviceBoundary::from_one_file_system(one_file_system),
353                    reporter: <$size_getter as CreateReporter<$progress>>::create_reporter(report_error),
354                    bytes_format: <$size_getter as GetSizeUtils>::formatter(bytes_format),
355                    files,
356                    json_output: JsonOutputParam::from_cli_flags(json_output, omit_json_shared_details, omit_json_shared_summary),
357                    column_width_distribution,
358                    max_depth,
359                    min_ratio,
360                    no_sort,
361                }
362                .run(),
363            )*} };
364        }
365
366        run! {
367            GetApparentSize, false, false;
368            GetApparentSize, true, false;
369            #[cfg(unix)] GetBlockSize, false, false;
370            #[cfg(unix)] GetBlockSize, true, false;
371            #[cfg(unix)] GetBlockCount, false, false;
372            #[cfg(unix)] GetBlockCount, true, false;
373            #[cfg(unix)] GetApparentSize, false, true;
374            #[cfg(unix)] GetApparentSize, true, true;
375            #[cfg(unix)] GetBlockSize, false, true;
376            #[cfg(unix)] GetBlockSize, true, true;
377            #[cfg(unix)] GetBlockCount, false, true;
378            #[cfg(unix)] GetBlockCount, true, true;
379        }
380    }
381}
382
383mod hdd;
384mod mount_point;
385mod overlapping_arguments;