Skip to main content

dua/
common.rs

1use crate::{crossdev, walk};
2use byte_unit::{Byte, Unit, UnitType};
3use serde::Deserialize;
4use std::collections::BTreeSet;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::time::Duration;
9use std::{fmt, path::Path};
10
11/// Specifies a way to format bytes
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
13pub enum ByteFormat {
14    /// metric format, based on 1000.
15    #[serde(rename = "metric")]
16    Metric,
17    /// binary format, based on 1024
18    #[serde(rename = "binary")]
19    Binary,
20    /// raw bytes, without additional formatting
21    #[serde(rename = "bytes")]
22    Bytes,
23    /// only gigabytes without smart-unit
24    #[serde(rename = "gb")]
25    GB,
26    /// only gibibytes without smart-unit
27    #[serde(rename = "gib")]
28    GiB,
29    /// only megabytes without smart-unit
30    #[serde(rename = "mb")]
31    MB,
32    /// only mebibytes without smart-unit
33    #[serde(rename = "mib")]
34    MiB,
35}
36
37impl ByteFormat {
38    /// Return the content width (without unit suffix) needed to display values in this format.
39    #[must_use]
40    pub fn width(self) -> usize {
41        use ByteFormat::{Binary, Bytes, MB, MiB};
42        match self {
43            Binary => 11,
44            Bytes | MB | MiB => 12,
45            _ => 10,
46        }
47    }
48    /// Return the full width (value plus unit and separator) used by this format.
49    #[must_use]
50    pub fn total_width(self) -> usize {
51        use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
52        const THE_SPACE_BETWEEN_UNIT_AND_NUMBER: usize = 1;
53
54        self.width()
55            + match self {
56                Binary | MiB | GiB => 3,
57                Metric | MB | GB => 2,
58                Bytes => 1,
59            }
60            + THE_SPACE_BETWEEN_UNIT_AND_NUMBER
61    }
62    /// Create a display adapter for `bytes` using this format.
63    #[must_use]
64    pub fn display(self, bytes: u128) -> impl fmt::Display {
65        ByteFormatDisplay {
66            format: self,
67            bytes,
68        }
69    }
70}
71
72/// A lightweight display adapter created by [`ByteFormat::display`].
73struct ByteFormatDisplay {
74    format: ByteFormat,
75    bytes: u128,
76}
77
78impl fmt::Display for ByteFormatDisplay {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
80        use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
81
82        let bytes = Byte::from_u128(self.bytes).expect("supported byte count");
83        let adjusted = match self.format {
84            Bytes => return write!(f, "{} b", self.bytes),
85            Binary => bytes.get_appropriate_unit(UnitType::Binary),
86            Metric => bytes.get_appropriate_unit(UnitType::Decimal),
87            GB => bytes.get_adjusted_unit(Unit::GB),
88            GiB => bytes.get_adjusted_unit(Unit::GiB),
89            MB => bytes.get_adjusted_unit(Unit::MB),
90            MiB => bytes.get_adjusted_unit(Unit::MiB),
91        };
92        let b = format!("{adjusted:.2}");
93        let mut splits = b.split(' ');
94        match (splits.next(), splits.next()) {
95            (Some(bytes), Some(unit)) => write!(
96                f,
97                "{} {:>unit_width$}",
98                bytes,
99                unit,
100                unit_width = match self.format {
101                    Binary => 3,
102                    _ => 2,
103                }
104            ),
105            _ => f.write_str(&b),
106        }
107    }
108}
109
110/// Throttle access to an optional `io::Write` to the specified `Duration`
111#[derive(Debug)]
112pub(crate) struct Throttle {
113    trigger: Arc<AtomicBool>,
114}
115
116impl Throttle {
117    /// Create a new throttle that allows updates at most once per `duration`.
118    ///
119    /// If `initial_sleep` is set, the first update is delayed by that amount.
120    pub(crate) fn new(duration: Duration, initial_sleep: Option<Duration>) -> Self {
121        let instance = Self {
122            trigger: Arc::default(),
123        };
124
125        let trigger = Arc::downgrade(&instance.trigger);
126        std::thread::spawn(move || {
127            if let Some(duration) = initial_sleep {
128                std::thread::sleep(duration);
129            }
130            while let Some(t) = trigger.upgrade() {
131                t.store(true, Ordering::Relaxed);
132                std::thread::sleep(duration);
133            }
134        });
135
136        instance
137    }
138
139    /// Execute `f` only if the throttle currently allows an update.
140    pub(crate) fn throttled<F>(&self, f: F)
141    where
142        F: FnOnce(),
143    {
144        if self.can_update() {
145            f();
146        }
147    }
148
149    /// Return `true` if we are not currently throttled.
150    pub(crate) fn can_update(&self) -> bool {
151        self.trigger.swap(false, Ordering::Relaxed)
152    }
153}
154
155/// Configures a filesystem walk, including output and formatting options.
156#[derive(Clone)]
157pub struct WalkOptions {
158    /// The amount of filesystem worker threads to use.
159    pub threads: usize,
160    /// If `true`, count every hard-link occurrence independently.
161    pub count_hard_links: bool,
162    /// If `true`, use apparent size (`metadata.len()`), not allocated blocks on disk.
163    pub apparent_size: bool,
164    /// If `false`, traversal is constrained to the root filesystem/device.
165    pub cross_filesystems: bool,
166    /// Canonicalized directories to skip from traversal.
167    pub ignore_dirs: BTreeSet<PathBuf>,
168}
169
170/// A root prepared for a filesystem walk.
171pub(crate) struct WalkRoot {
172    /// Index used to associate emitted events with the original input path.
173    pub index: usize,
174    /// Path at which to start walking.
175    pub path: PathBuf,
176    /// Device containing the root, used to constrain cross-filesystem walks.
177    /// It's `0` if it couldn't be obtained or if `cross_filesystem` is `true`.
178    pub device_id: u64,
179}
180
181impl WalkOptions {
182    pub(crate) fn iter_from_paths(
183        &self,
184        roots: Vec<WalkRoot>,
185        skip_root: bool,
186        order: walk::Order,
187    ) -> impl Iterator<Item = (usize, walk::RootEvent)> + use<> {
188        let num_roots = roots
189            .iter()
190            .map(|root| root.index)
191            .max()
192            .map_or(0, |idx| idx + 1);
193        let path_count = roots.len();
194        let (device_ids, paths_with_idx) = roots.into_iter().fold(
195            (vec![0; num_roots], Vec::with_capacity(path_count)),
196            |(mut device_ids, mut paths), root| {
197                device_ids[root.index] = root.device_id;
198                paths.push((root.index, root.path));
199                (device_ids, paths)
200            },
201        );
202        let ignore_dirs = self.ignore_dirs.clone();
203        let cwd = std::env::current_dir().unwrap_or_default();
204        let cross_filesystems = self.cross_filesystems;
205        walk::walk_roots(
206            paths_with_idx,
207            self.threads,
208            order,
209            move |root_idx, entry| {
210                (cross_filesystems
211                    || entry.metadata.as_ref().map_or(true, |metadata| {
212                        crossdev::is_same_device(device_ids[root_idx], metadata)
213                    }))
214                    && (entry.depth == 0 || !ignore_directory(&entry.path(), &ignore_dirs, &cwd))
215            },
216        )
217        .filter(move |(_, event)| {
218            !skip_root
219                || match event {
220                    walk::RootEvent::Entry(entry) => {
221                        entry.as_ref().map_or(true, |entry| entry.depth > 0)
222                    }
223                    walk::RootEvent::Finished => true,
224                }
225        })
226    }
227}
228
229/// Information we gather during a filesystem walk
230#[derive(Default)]
231pub struct WalkResult {
232    /// The amount of `io::errors` we encountered. Can happen when fetching meta-data, or when reading the directory contents.
233    pub num_errors: u64,
234}
235
236impl WalkResult {
237    /// Convert traversal result into a process exit code.
238    ///
239    /// Returns `0` if no I/O errors occurred, otherwise `1`.
240    #[must_use]
241    pub fn to_exit_code(&self) -> i32 {
242        i32::from(self.num_errors > 0)
243    }
244}
245
246/// Canonicalize user-provided ignore directory paths.
247///
248/// Non-canonicalizable paths are ignored.
249pub fn canonicalize_ignore_dirs(ignore_dirs: &[PathBuf]) -> BTreeSet<PathBuf> {
250    let dirs = ignore_dirs
251        .iter()
252        .map(gix::path::realpath)
253        .filter_map(Result::ok)
254        .collect();
255    log::info!("Ignoring canonicalized {dirs:?}");
256    dirs
257}
258
259fn ignore_directory(path: &Path, ignore_dirs: &BTreeSet<PathBuf>, cwd: &Path) -> bool {
260    if ignore_dirs.is_empty() {
261        return false;
262    }
263    let path = gix::path::realpath_opts(path, cwd, 32);
264    path.is_ok_and(|path| {
265        let ignored = ignore_dirs.contains(&path);
266        if ignored {
267            log::debug!("Ignored {}", path.display());
268        }
269        ignored
270    })
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_ignore_directories() {
279        let cwd = std::env::current_dir().unwrap();
280        #[cfg(unix)]
281        let mut parameters = vec![
282            ("/usr", vec!["/usr"], true),
283            ("/usr/local", vec!["/usr"], false),
284            ("/smth", vec!["/usr"], false),
285            ("/usr/local/..", vec!["/usr/local/.."], true),
286            ("/usr", vec!["/usr/local/.."], true),
287            ("/usr/local/share/../..", vec!["/usr"], true),
288        ];
289
290        #[cfg(windows)]
291        let mut parameters = vec![
292            ("C:\\Windows", vec!["C:\\Windows"], true),
293            ("C:\\Windows\\System", vec!["C:\\Windows"], false),
294            ("C:\\Smth", vec!["C:\\Windows"], false),
295            (
296                "C:\\Windows\\System\\..",
297                vec!["C:\\Windows\\System\\.."],
298                true,
299            ),
300            ("C:\\Windows", vec!["C:\\Windows\\System\\.."], true),
301            (
302                "C:\\Windows\\System\\Speech\\..\\..",
303                vec!["C:\\Windows"],
304                true,
305            ),
306        ];
307
308        parameters.extend([
309            ("src", vec!["src"], true),
310            ("src/interactive", vec!["src"], false),
311            ("src/interactive/..", vec!["src"], true),
312        ]);
313
314        for (path, ignore_dirs, expected_result) in parameters {
315            let ignore_dirs = canonicalize_ignore_dirs(
316                &ignore_dirs.into_iter().map(Into::into).collect::<Vec<_>>(),
317            );
318            assert_eq!(
319                ignore_directory(path.as_ref(), &ignore_dirs, &cwd),
320                expected_result,
321                "result='{expected_result}' for path='{path}' and ignore_dir='{ignore_dirs:?}' "
322            );
323        }
324    }
325
326    #[test]
327    fn explicitly_selected_ignored_root_is_traversed() {
328        let root = tempfile::tempdir().unwrap();
329        let child = root.path().join("child");
330        std::fs::create_dir(&child).unwrap();
331        let options = WalkOptions {
332            threads: 2,
333            count_hard_links: false,
334            apparent_size: false,
335            cross_filesystems: true,
336            ignore_dirs: canonicalize_ignore_dirs(&[root.path().to_owned()]),
337        };
338
339        let paths = options
340            .iter_from_paths(
341                vec![WalkRoot {
342                    index: 0,
343                    path: root.path().to_owned(),
344                    device_id: crossdev::init(root.path()).unwrap(),
345                }],
346                false,
347                walk::Order::Completion,
348            )
349            .filter_map(|(_, event)| match event {
350                walk::RootEvent::Entry(entry) => Some(entry.unwrap().path()),
351                walk::RootEvent::Finished => None,
352            })
353            .collect::<Vec<_>>();
354
355        assert!(paths.contains(&child));
356    }
357}