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