use crate::{crossdev, walk};
use byte_unit::{ByteUnit, n_gb_bytes, n_gib_bytes, n_mb_bytes, n_mib_bytes};
use serde::Deserialize;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::{fmt, path::Path};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub enum ByteFormat {
#[serde(rename = "metric")]
Metric,
#[serde(rename = "binary")]
Binary,
#[serde(rename = "bytes")]
Bytes,
#[serde(rename = "gb")]
GB,
#[serde(rename = "gib")]
GiB,
#[serde(rename = "mb")]
MB,
#[serde(rename = "mib")]
MiB,
}
impl ByteFormat {
#[must_use]
pub fn width(self) -> usize {
use ByteFormat::{Binary, Bytes, MB, MiB};
match self {
Binary => 11,
Bytes | MB | MiB => 12,
_ => 10,
}
}
#[must_use]
pub fn total_width(self) -> usize {
use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
const THE_SPACE_BETWEEN_UNIT_AND_NUMBER: usize = 1;
self.width()
+ match self {
Binary | MiB | GiB => 3,
Metric | MB | GB => 2,
Bytes => 1,
}
+ THE_SPACE_BETWEEN_UNIT_AND_NUMBER
}
#[must_use]
pub fn display(self, bytes: u128) -> impl fmt::Display {
ByteFormatDisplay {
format: self,
bytes,
}
}
}
struct ByteFormatDisplay {
format: ByteFormat,
bytes: u128,
}
#[expect(
clippy::cast_precision_loss,
reason = "byte_unit requires floating-point conversion"
)]
impl fmt::Display for ByteFormatDisplay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
use byte_unit::Byte;
let format = match self.format {
Bytes => return write!(f, "{} b", self.bytes),
Binary => (true, None),
Metric => (false, None),
GB => (false, Some((n_gb_bytes!(1), ByteUnit::GB))),
GiB => (false, Some((n_gib_bytes!(1), ByteUnit::GiB))),
MB => (false, Some((n_mb_bytes!(1), ByteUnit::MB))),
MiB => (false, Some((n_mib_bytes!(1), ByteUnit::MiB))),
};
let b = match format {
(_, Some((divisor, unit))) => Byte::from_unit(self.bytes as f64 / divisor as f64, unit)
.expect("byte count > 0")
.get_adjusted_unit(unit),
(binary, None) => Byte::from_bytes(self.bytes).get_appropriate_unit(binary),
}
.format(2);
let mut splits = b.split(' ');
match (splits.next(), splits.next()) {
(Some(bytes), Some(unit)) => write!(
f,
"{} {:>unit_width$}",
bytes,
unit,
unit_width = match self.format {
Binary => 3,
_ => 2,
}
),
_ => f.write_str(&b),
}
}
}
#[derive(Debug)]
pub(crate) struct Throttle {
trigger: Arc<AtomicBool>,
}
impl Throttle {
pub(crate) fn new(duration: Duration, initial_sleep: Option<Duration>) -> Self {
let instance = Self {
trigger: Arc::default(),
};
let trigger = Arc::downgrade(&instance.trigger);
std::thread::spawn(move || {
if let Some(duration) = initial_sleep {
std::thread::sleep(duration);
}
while let Some(t) = trigger.upgrade() {
t.store(true, Ordering::Relaxed);
std::thread::sleep(duration);
}
});
instance
}
pub(crate) fn throttled<F>(&self, f: F)
where
F: FnOnce(),
{
if self.can_update() {
f();
}
}
pub(crate) fn can_update(&self) -> bool {
self.trigger.swap(false, Ordering::Relaxed)
}
}
#[derive(Clone)]
pub struct WalkOptions {
pub threads: usize,
pub count_hard_links: bool,
pub apparent_size: bool,
pub cross_filesystems: bool,
pub ignore_dirs: BTreeSet<PathBuf>,
}
impl WalkOptions {
pub(crate) fn iter_from_path(
&self,
root: &Path,
root_device_id: u64,
skip_root: bool,
order: walk::Order,
) -> impl Iterator<Item = std::io::Result<walk::Entry>> {
let ignore_dirs = self.ignore_dirs.clone();
let cwd = std::env::current_dir().unwrap_or_else(|_| root.to_owned());
let cross_filesystems = self.cross_filesystems;
walk::walk(root, self.threads, order, move |entry| {
(cross_filesystems
|| entry.metadata.as_ref().map_or(true, |metadata| {
crossdev::is_same_device(root_device_id, metadata)
}))
&& (entry.depth == 0 || !ignore_directory(&entry.path(), &ignore_dirs, &cwd))
})
.filter(move |entry| !skip_root || entry.as_ref().map_or(true, |entry| entry.depth > 0))
}
}
#[derive(Default)]
pub struct WalkResult {
pub num_errors: u64,
}
impl WalkResult {
#[must_use]
pub fn to_exit_code(&self) -> i32 {
i32::from(self.num_errors > 0)
}
}
pub fn canonicalize_ignore_dirs(ignore_dirs: &[PathBuf]) -> BTreeSet<PathBuf> {
let dirs = ignore_dirs
.iter()
.map(gix::path::realpath)
.filter_map(Result::ok)
.collect();
log::info!("Ignoring canonicalized {dirs:?}");
dirs
}
fn ignore_directory(path: &Path, ignore_dirs: &BTreeSet<PathBuf>, cwd: &Path) -> bool {
if ignore_dirs.is_empty() {
return false;
}
let path = gix::path::realpath_opts(path, cwd, 32);
path.is_ok_and(|path| {
let ignored = ignore_dirs.contains(&path);
if ignored {
log::debug!("Ignored {}", path.display());
}
ignored
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ignore_directories() {
let cwd = std::env::current_dir().unwrap();
#[cfg(unix)]
let mut parameters = vec![
("/usr", vec!["/usr"], true),
("/usr/local", vec!["/usr"], false),
("/smth", vec!["/usr"], false),
("/usr/local/..", vec!["/usr/local/.."], true),
("/usr", vec!["/usr/local/.."], true),
("/usr/local/share/../..", vec!["/usr"], true),
];
#[cfg(windows)]
let mut parameters = vec![
("C:\\Windows", vec!["C:\\Windows"], true),
("C:\\Windows\\System", vec!["C:\\Windows"], false),
("C:\\Smth", vec!["C:\\Windows"], false),
(
"C:\\Windows\\System\\..",
vec!["C:\\Windows\\System\\.."],
true,
),
("C:\\Windows", vec!["C:\\Windows\\System\\.."], true),
(
"C:\\Windows\\System\\Speech\\..\\..",
vec!["C:\\Windows"],
true,
),
];
parameters.extend([
("src", vec!["src"], true),
("src/interactive", vec!["src"], false),
("src/interactive/..", vec!["src"], true),
]);
for (path, ignore_dirs, expected_result) in parameters {
let ignore_dirs = canonicalize_ignore_dirs(
&ignore_dirs.into_iter().map(Into::into).collect::<Vec<_>>(),
);
assert_eq!(
ignore_directory(path.as_ref(), &ignore_dirs, &cwd),
expected_result,
"result='{expected_result}' for path='{path}' and ignore_dir='{ignore_dirs:?}' "
);
}
}
#[test]
fn explicitly_selected_ignored_root_is_traversed() {
let root = tempfile::tempdir().unwrap();
let child = root.path().join("child");
std::fs::create_dir(&child).unwrap();
let options = WalkOptions {
threads: 2,
count_hard_links: false,
apparent_size: false,
cross_filesystems: true,
ignore_dirs: canonicalize_ignore_dirs(&[root.path().to_owned()]),
};
let paths = options
.iter_from_path(
root.path(),
crossdev::init(root.path()).unwrap(),
false,
walk::Order::Completion,
)
.map(|entry| entry.unwrap().path())
.collect::<Vec<_>>();
assert!(paths.contains(&child));
}
}