1use crate::crossdev;
2use crate::traverse::{EntryData, Tree, TreeIndex};
3use byte_unit::{ByteUnit, n_gb_bytes, n_gib_bytes, n_mb_bytes, n_mib_bytes};
4use serde::Deserialize;
5use std::collections::BTreeSet;
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Duration;
10use std::{fmt, path::Path};
11
12pub(crate) fn get_entry_or_panic(tree: &Tree, node_idx: TreeIndex) -> &EntryData {
14 tree.node_weight(node_idx)
15 .expect("node should always be retrievable with valid index")
16}
17
18pub(crate) fn get_size_or_panic(tree: &Tree, node_idx: TreeIndex) -> u128 {
19 get_entry_or_panic(tree, node_idx).size
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
24pub enum ByteFormat {
25 #[serde(rename = "metric")]
27 Metric,
28 #[serde(rename = "binary")]
30 Binary,
31 #[serde(rename = "bytes")]
33 Bytes,
34 #[serde(rename = "gb")]
36 GB,
37 #[serde(rename = "gib")]
39 GiB,
40 #[serde(rename = "mb")]
42 MB,
43 #[serde(rename = "mib")]
45 MiB,
46}
47
48impl ByteFormat {
49 pub fn width(self) -> usize {
51 use ByteFormat::*;
52 match self {
53 Metric => 10,
54 Binary => 11,
55 Bytes => 12,
56 MiB | MB => 12,
57 _ => 10,
58 }
59 }
60 pub fn total_width(self) -> usize {
62 use ByteFormat::*;
63 const THE_SPACE_BETWEEN_UNIT_AND_NUMBER: usize = 1;
64
65 self.width()
66 + match self {
67 Binary | MiB | GiB => 3,
68 Metric | MB | GB => 2,
69 Bytes => 1,
70 }
71 + THE_SPACE_BETWEEN_UNIT_AND_NUMBER
72 }
73 pub fn display(self, bytes: u128) -> impl fmt::Display {
75 ByteFormatDisplay {
76 format: self,
77 bytes,
78 }
79 }
80}
81
82struct ByteFormatDisplay {
84 format: ByteFormat,
85 bytes: u128,
86}
87
88impl fmt::Display for ByteFormatDisplay {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
90 use ByteFormat::*;
91 use byte_unit::Byte;
92
93 let format = match self.format {
94 Bytes => return write!(f, "{} b", self.bytes),
95 Binary => (true, None),
96 Metric => (false, None),
97 GB => (false, Some((n_gb_bytes!(1), ByteUnit::GB))),
98 GiB => (false, Some((n_gib_bytes!(1), ByteUnit::GiB))),
99 MB => (false, Some((n_mb_bytes!(1), ByteUnit::MB))),
100 MiB => (false, Some((n_mib_bytes!(1), ByteUnit::MiB))),
101 };
102
103 let b = match format {
104 (_, Some((divisor, unit))) => Byte::from_unit(self.bytes as f64 / divisor as f64, unit)
105 .expect("byte count > 0")
106 .get_adjusted_unit(unit),
107 (binary, None) => Byte::from_bytes(self.bytes).get_appropriate_unit(binary),
108 }
109 .format(2);
110 let mut splits = b.split(' ');
111 match (splits.next(), splits.next()) {
112 (Some(bytes), Some(unit)) => write!(
113 f,
114 "{} {:>unit_width$}",
115 bytes,
116 unit,
117 unit_width = match self.format {
118 Binary => 3,
119 Metric => 2,
120 _ => 2,
121 }
122 ),
123 _ => f.write_str(&b),
124 }
125 }
126}
127
128#[derive(Clone)]
130pub enum TraversalSorting {
131 None,
133 AlphabeticalByFileName,
135}
136
137#[derive(Debug)]
139pub(crate) struct Throttle {
140 trigger: Arc<AtomicBool>,
141}
142
143impl Throttle {
144 pub(crate) fn new(duration: Duration, initial_sleep: Option<Duration>) -> Self {
148 let instance = Self {
149 trigger: Default::default(),
150 };
151
152 let trigger = Arc::downgrade(&instance.trigger);
153 std::thread::spawn(move || {
154 if let Some(duration) = initial_sleep {
155 std::thread::sleep(duration)
156 }
157 while let Some(t) = trigger.upgrade() {
158 t.store(true, Ordering::Relaxed);
159 std::thread::sleep(duration);
160 }
161 });
162
163 instance
164 }
165
166 pub(crate) fn throttled<F>(&self, f: F)
168 where
169 F: FnOnce(),
170 {
171 if self.can_update() {
172 f()
173 }
174 }
175
176 pub(crate) fn can_update(&self) -> bool {
178 self.trigger.swap(false, Ordering::Relaxed)
179 }
180}
181
182#[derive(Clone)]
184pub struct WalkOptions {
185 pub threads: usize,
188 pub count_hard_links: bool,
190 pub apparent_size: bool,
192 pub sorting: TraversalSorting,
194 pub cross_filesystems: bool,
196 pub ignore_dirs: BTreeSet<PathBuf>,
198}
199
200type WalkDir = jwalk::WalkDirGeneric<((), Option<Result<std::fs::Metadata, jwalk::Error>>)>;
201
202impl WalkOptions {
203 pub(crate) fn iter_from_path(
208 &self,
209 root: &Path,
210 root_device_id: u64,
211 skip_root: bool,
212 ) -> WalkDir {
213 let ignore_dirs = self.ignore_dirs.clone();
214 let cwd = std::env::current_dir().unwrap_or_else(|_| root.to_owned());
215 WalkDir::new(root)
216 .follow_links(false)
217 .min_depth(if skip_root { 1 } else { 0 })
218 .sort(match self.sorting {
219 TraversalSorting::None => false,
220 TraversalSorting::AlphabeticalByFileName => true,
221 })
222 .skip_hidden(false)
223 .process_read_dir({
224 let cross_filesystems = self.cross_filesystems;
225 move |_, _, _, dir_entry_results| {
226 dir_entry_results.iter_mut().for_each(|dir_entry_result| {
227 if let Ok(dir_entry) = dir_entry_result {
228 let metadata = dir_entry.metadata();
229
230 if dir_entry.file_type.is_dir() {
231 let ok_for_fs = cross_filesystems
232 || metadata
233 .as_ref()
234 .map(|m| crossdev::is_same_device(root_device_id, m))
235 .unwrap_or(true);
236 if !ok_for_fs
237 || ignore_directory(&dir_entry.path(), &ignore_dirs, &cwd)
238 {
239 dir_entry.read_children_path = None;
240 }
241 }
242
243 dir_entry.client_state = Some(metadata);
244 }
245 })
246 }
247 })
248 .parallelism(match self.threads {
249 0 => jwalk::Parallelism::RayonDefaultPool {
250 busy_timeout: std::time::Duration::from_secs(1),
251 },
252 1 => jwalk::Parallelism::Serial,
253 _ => jwalk::Parallelism::RayonExistingPool {
254 pool: jwalk::rayon::ThreadPoolBuilder::new()
255 .stack_size(128 * 1024)
256 .num_threads(self.threads)
257 .thread_name(|idx| format!("dua-fs-walk-{idx}"))
258 .build()
259 .expect("fields we set cannot fail")
260 .into(),
261 busy_timeout: None,
262 },
263 })
264 }
265}
266
267#[derive(Default)]
269pub struct WalkResult {
270 pub num_errors: u64,
272}
273
274impl WalkResult {
275 pub fn to_exit_code(&self) -> i32 {
279 i32::from(self.num_errors > 0)
280 }
281}
282
283pub fn canonicalize_ignore_dirs(ignore_dirs: &[PathBuf]) -> BTreeSet<PathBuf> {
287 let dirs = ignore_dirs
288 .iter()
289 .map(gix::path::realpath)
290 .filter_map(Result::ok)
291 .collect();
292 log::info!("Ignoring canonicalized {dirs:?}");
293 dirs
294}
295
296fn ignore_directory(path: &Path, ignore_dirs: &BTreeSet<PathBuf>, cwd: &Path) -> bool {
297 if ignore_dirs.is_empty() {
298 return false;
299 }
300 let path = gix::path::realpath_opts(path, cwd, 32);
301 path.map(|path| {
302 let ignored = ignore_dirs.contains(&path);
303 if ignored {
304 log::debug!("Ignored {path:?}");
305 }
306 ignored
307 })
308 .unwrap_or(false)
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_ignore_directories() {
317 let cwd = std::env::current_dir().unwrap();
318 #[cfg(unix)]
319 let mut parameters = vec![
320 ("/usr", vec!["/usr"], true),
321 ("/usr/local", vec!["/usr"], false),
322 ("/smth", vec!["/usr"], false),
323 ("/usr/local/..", vec!["/usr/local/.."], true),
324 ("/usr", vec!["/usr/local/.."], true),
325 ("/usr/local/share/../..", vec!["/usr"], true),
326 ];
327
328 #[cfg(windows)]
329 let mut parameters = vec![
330 ("C:\\Windows", vec!["C:\\Windows"], true),
331 ("C:\\Windows\\System", vec!["C:\\Windows"], false),
332 ("C:\\Smth", vec!["C:\\Windows"], false),
333 (
334 "C:\\Windows\\System\\..",
335 vec!["C:\\Windows\\System\\.."],
336 true,
337 ),
338 ("C:\\Windows", vec!["C:\\Windows\\System\\.."], true),
339 (
340 "C:\\Windows\\System\\Speech\\..\\..",
341 vec!["C:\\Windows"],
342 true,
343 ),
344 ];
345
346 parameters.extend([
347 ("src", vec!["src"], true),
348 ("src/interactive", vec!["src"], false),
349 ("src/interactive/..", vec!["src"], true),
350 ]);
351
352 for (path, ignore_dirs, expected_result) in parameters {
353 let ignore_dirs = canonicalize_ignore_dirs(
354 &ignore_dirs.into_iter().map(Into::into).collect::<Vec<_>>(),
355 );
356 assert_eq!(
357 ignore_directory(path.as_ref(), &ignore_dirs, &cwd),
358 expected_result,
359 "result='{expected_result}' for path='{path}' and ignore_dir='{ignore_dirs:?}' "
360 );
361 }
362 }
363}