use crate::{crossdev, walk};
use byte_unit::{Byte, Unit, UnitType};
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,
}
impl fmt::Display for ByteFormatDisplay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
let bytes = Byte::from_u128(self.bytes).expect("supported byte count");
let adjusted = match self.format {
Bytes => return write!(f, "{} b", self.bytes),
Binary => bytes.get_appropriate_unit(UnitType::Binary),
Metric => bytes.get_appropriate_unit(UnitType::Decimal),
GB => bytes.get_adjusted_unit(Unit::GB),
GiB => bytes.get_adjusted_unit(Unit::GiB),
MB => bytes.get_adjusted_unit(Unit::MB),
MiB => bytes.get_adjusted_unit(Unit::MiB),
};
let b = format!("{adjusted:.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>,
}
pub(crate) struct WalkRoot {
pub index: usize,
pub path: PathBuf,
pub device_id: u64,
}
impl WalkOptions {
pub(crate) fn iter_from_paths(
&self,
roots: Vec<WalkRoot>,
skip_root: bool,
order: walk::Order,
) -> impl Iterator<Item = (usize, walk::RootEvent)> + use<> {
let num_roots = roots
.iter()
.map(|root| root.index)
.max()
.map_or(0, |idx| idx + 1);
let path_count = roots.len();
let (device_ids, paths_with_idx) = roots.into_iter().fold(
(vec![0; num_roots], Vec::with_capacity(path_count)),
|(mut device_ids, mut paths), root| {
device_ids[root.index] = root.device_id;
paths.push((root.index, root.path));
(device_ids, paths)
},
);
let ignore_dirs = self.ignore_dirs.clone();
let cwd = std::env::current_dir().unwrap_or_default();
let cross_filesystems = self.cross_filesystems;
walk::walk_roots(
paths_with_idx,
self.threads,
order,
move |root_idx, entry| {
(cross_filesystems
|| entry.metadata.as_ref().map_or(true, |metadata| {
crossdev::is_same_device(device_ids[root_idx], metadata)
}))
&& (entry.depth == 0 || !ignore_directory(&entry.path(), &ignore_dirs, &cwd))
},
)
.filter(move |(_, event)| {
!skip_root
|| match event {
walk::RootEvent::Entry(entry) => {
entry.as_ref().map_or(true, |entry| entry.depth > 0)
}
walk::RootEvent::Finished => true,
}
})
}
}
#[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_paths(
vec![WalkRoot {
index: 0,
path: root.path().to_owned(),
device_id: crossdev::init(root.path()).unwrap(),
}],
false,
walk::Order::Completion,
)
.filter_map(|(_, event)| match event {
walk::RootEvent::Entry(entry) => Some(entry.unwrap().path()),
walk::RootEvent::Finished => None,
})
.collect::<Vec<_>>();
assert!(paths.contains(&child));
}
}