use std::fmt::Debug;
use std::fs::{File, Metadata};
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{io, thread};
use anyhow::{Context, bail};
use crossbeam::channel::{RecvTimeoutError, SendError, Sender};
use crossterm::tty::IsTty;
use globwalk::FileType;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
pub trait Draw {
fn draw(&self, width: usize) -> String;
}
pub struct Walker<'a> {
path: &'a Path,
file_list: bool,
filters: Vec<String>,
max_depth: Option<usize>,
metadata_filter: Option<Box<dyn Fn(Metadata) -> bool + Send + 'a>>,
}
impl<'a> Walker<'a> {
pub fn path(path: &'a Path) -> Self {
Self {
path,
filters: Vec::new(),
file_list: false,
max_depth: None,
metadata_filter: None,
}
}
pub fn file_list(path: &'a Path) -> Self {
Self {
path,
filters: Vec::new(),
file_list: true,
max_depth: None,
metadata_filter: None,
}
}
pub fn filter(&mut self, filter: &str) -> &mut Self {
self.filters.push(filter.to_string());
self
}
pub fn metadata_filter(
&mut self,
filter: impl Fn(Metadata) -> bool + Send + 'a,
) -> &mut Self {
self.metadata_filter = Some(Box::new(filter));
self
}
pub fn max_depth(&mut self, n: usize) -> &mut Self {
self.max_depth = Some(n);
self
}
pub fn walk<F, E>(self, mut f: F, mut e: E) -> anyhow::Result<()>
where
F: FnMut(&Path) -> anyhow::Result<()>,
E: FnMut(anyhow::Error) -> anyhow::Result<()>,
{
if self.file_list && self.path == Path::new("-") {
return self.walk_file_list(f, e);
}
let metadata =
match self.path.metadata().with_context(|| {
format!("can't open `{}`", self.path.display())
}) {
Ok(metadata) => metadata,
Err(err) => {
return e(err);
}
};
if self.file_list {
if !metadata.is_file() {
bail!("`{}` is not a file", self.path.display())
}
self.walk_file_list(f, e)
} else {
if metadata.is_file() {
if self.pass_metadata_filter(metadata)
&& let Err(err) = f(self.path)
{
return e(err);
};
return Ok(());
}
self.walk_dir(f, e)
}
}
fn walk_file_list<F, E>(self, f: F, e: E) -> anyhow::Result<()>
where
F: FnMut(&Path) -> anyhow::Result<()>,
E: FnMut(anyhow::Error) -> anyhow::Result<()>,
{
if self.path == Path::new("-") {
let stdin = io::stdin();
self.walk_file_list_lines(stdin.lock().lines(), f, e)
} else {
let file = File::open(self.path)?;
self.walk_file_list_lines(io::BufReader::new(file).lines(), f, e)
}
}
fn walk_file_list_lines<F, E, I>(
self,
lines: I,
mut f: F,
mut e: E,
) -> anyhow::Result<()>
where
F: FnMut(&Path) -> anyhow::Result<()>,
E: FnMut(anyhow::Error) -> anyhow::Result<()>,
I: Iterator<Item = io::Result<String>>,
{
for line in lines {
let path = PathBuf::from(line?);
let metadata = match path
.metadata()
.with_context(|| format!("can't open `{}`", path.display()))
{
Ok(metadata) => metadata,
Err(err) => match e(err) {
Ok(_) => continue,
Err(err) => return Err(err),
},
};
if self.pass_metadata_filter(metadata)
&& let Err(err) = f(&path)
{
e(err)?
}
}
Ok(())
}
fn walk_dir<F, E>(&self, mut f: F, mut e: E) -> anyhow::Result<()>
where
F: FnMut(&Path) -> anyhow::Result<()>,
E: FnMut(anyhow::Error) -> anyhow::Result<()>,
{
let path = if self.path.as_os_str().len() > 2 {
self.path
.strip_prefix(if cfg!(target_os = "windows") {
r#".\"#
} else {
"./"
})
.unwrap_or(self.path)
} else {
self.path
};
let mut builder = if self.filters.is_empty() {
globwalk::GlobWalkerBuilder::from_patterns(path, &["**"])
} else {
globwalk::GlobWalkerBuilder::from_patterns(
path,
self.filters.iter().as_ref(),
)
};
builder = builder.file_type(FileType::FILE);
if let Some(max_depth) = self.max_depth {
builder = builder.max_depth(max_depth + 1);
}
for entry in builder.build()? {
let entry = match entry {
Ok(e) => e,
Err(err) => {
if let Err(err) = e(err.into()) {
return Err(err);
} else {
continue;
}
}
};
match entry.metadata() {
Ok(metadata) => {
if self.pass_metadata_filter(metadata)
&& let Err(err) = f(entry.path())
{
e(err)?
}
}
Err(err) => e(err.into())?,
}
}
Ok(())
}
fn pass_metadata_filter(&self, metadata: Metadata) -> bool {
self.metadata_filter.as_ref().map(|f| f(metadata)).unwrap_or(true)
}
}
pub(crate) struct ParWalker<'a> {
num_threads: Option<u8>,
cpu_limit: Option<u8>,
walker: Walker<'a>,
}
impl<'a> ParWalker<'a> {
pub fn path(path: &'a Path) -> Self {
Self { walker: Walker::path(path), num_threads: None, cpu_limit: None }
}
pub fn file_list(path: &'a Path) -> Self {
Self {
walker: Walker::file_list(path),
num_threads: None,
cpu_limit: None,
}
}
pub fn num_threads(&mut self, n: u8) -> &mut Self {
self.num_threads = Some(n);
self
}
pub fn cpu_limit(&mut self, limit: u8) -> &mut Self {
self.cpu_limit = Some(limit);
self
}
pub fn max_depth(&mut self, n: usize) -> &mut Self {
self.walker.max_depth(n);
self
}
pub fn filter(&mut self, filter: &str) -> &mut Self {
self.walker.filter(filter);
self
}
pub fn metadata_filter(
&mut self,
filter: impl Fn(Metadata) -> bool + Send + 'a,
) -> &mut Self {
self.walker.metadata_filter(filter);
self
}
pub fn walk<S, T, I, A, F, D, E>(
self,
state: S,
init: I,
action: A,
finalize: F,
on_walk_done: D,
error: E,
) -> thread::Result<S>
where
S: Draw + Debug + Send + Sync + 'static,
I: Fn(&S, &Sender<Message>) -> T + Send + Copy + Sync,
A: Fn(&S, &Sender<Message>, PathBuf, &mut T) -> anyhow::Result<()>
+ Send
+ Sync
+ Copy,
F: Fn(&T, &Sender<Message>) + Send + Copy + Sync,
D: Fn(&Sender<Message>),
E: Fn(anyhow::Error, &Sender<Message>) -> anyhow::Result<()>
+ Send
+ Copy,
{
let num_threads = if let Some(num_threads) = self.num_threads {
num_threads as usize
} else {
thread::available_parallelism().map(usize::from).unwrap_or(32)
};
let cpu_limit = self.cpu_limit;
crossbeam::scope(|s| {
let mut threads = Vec::with_capacity(num_threads);
let (paths_send, paths_recv) =
crossbeam::channel::bounded::<PathBuf>(128);
let (msg_send, msg_recv) =
crossbeam::channel::unbounded::<Message>();
let state = Arc::new(state);
for _ in 0..num_threads {
let paths_recv = paths_recv.clone();
let msg_send = msg_send.clone();
let state = state.clone();
threads.push(s.spawn(move |_| {
let mut per_thread_obj = init(&state, &msg_send);
for path in paths_recv {
let start_time = Instant::now();
let res = action(
&state,
&msg_send,
path.to_path_buf(),
&mut per_thread_obj,
);
let t_active = start_time.elapsed();
if let Some(limit) = cpu_limit
&& limit < 100
{
let t_sleep = t_active.mul_f64(
(100.0 - limit as f64) / limit as f64,
);
if !t_sleep.is_zero() {
thread::sleep(t_sleep);
}
}
if let Err(err) = res
&& error(err, &msg_send).is_err()
{
let _ = msg_send.send(Message::Abort);
break;
}
}
finalize(&per_thread_obj, &msg_send);
}));
}
threads.push(s.spawn(move |_| {
let res = self.walker.walk(
|file_path| Ok(paths_send.send(file_path.to_path_buf())?),
|err| {
if err.is::<SendError<PathBuf>>() {
return Err(err);
}
if let Err(err) = error(err, &msg_send) {
let _ = msg_send.send(Message::Abort);
return Err(err);
}
Ok(())
},
);
if let Err(err) = res
&& error(err, &msg_send).is_err()
{
let _ = msg_send.send(Message::Abort);
}
}));
let multi_progress = if cfg!(feature = "stderr-logs") {
None
} else if io::stdout().is_tty() {
Some(MultiProgress::new())
} else {
None
};
let render_period = Duration::from_secs_f64(0.150);
output_messages(
render_period,
Instant::now(),
msg_recv,
multi_progress.as_ref(),
state.clone(),
);
threads.into_iter().for_each(|thread| thread.join().unwrap());
let (msg_send, msg_recv) =
crossbeam::channel::bounded::<Message>(32);
let handle = {
let state = state.clone();
let multi_progress = multi_progress.clone();
thread::spawn(move || {
output_messages(
render_period,
Instant::now(),
msg_recv,
multi_progress.as_ref(),
state.clone(),
);
})
};
on_walk_done(&msg_send);
drop(msg_send);
handle.join().unwrap();
Arc::<S>::try_unwrap(state).unwrap()
})
}
}
fn output_messages<S>(
render_period: Duration,
last_render: Instant,
msg_recv: crossbeam::channel::Receiver<Message>,
multi_progress: Option<&MultiProgress>,
state: Arc<S>,
) where
S: Draw,
{
let mut last_render = last_render;
let pb = multi_progress.map(|mp| {
let pb = mp.add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::default_spinner().template("{msg}").unwrap(),
);
pb
});
loop {
match msg_recv.recv_timeout(render_period) {
Ok(Message::Info(s)) => {
if let Some(mp) = multi_progress {
let _ = mp.println(s);
} else {
println!("{s}")
}
}
Ok(Message::Error(s)) => {
if let Some(mp) = multi_progress {
let _ = mp.println(s);
} else {
eprintln!("{s}")
}
}
Ok(Message::Abort) => {
break;
}
Err(RecvTimeoutError::Disconnected) => {
break;
}
Err(RecvTimeoutError::Timeout) => {}
}
if let Some(pb) = &pb
&& Instant::elapsed(&last_render) > render_period
{
let width = crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80);
pb.set_message(state.draw(width));
last_render = Instant::now();
}
}
if let Some(pb) = pb {
pb.finish_and_clear();
}
}
pub enum Message {
Info(String),
Error(String),
Abort,
}