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