#[cfg(feature = "async")]
use crate::{
batch_io::BatchIO, error::GlobError, patterns::Patterns, predicates::Predicates, GlobOptions,
};
#[cfg(feature = "async")]
use async_stream::stream;
#[cfg(feature = "async")]
use camino::Utf8PathBuf;
#[cfg(feature = "async")]
use futures::Stream;
#[cfg(feature = "async")]
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
#[cfg(feature = "async")]
use tokio::{fs, sync::Semaphore, task};
#[cfg(feature = "async")]
fn check_for_cycles(path: &Path, visited: &mut HashSet<PathBuf>) -> bool {
if visited.contains(path) {
return true;
}
visited.insert(path.to_path_buf());
false
}
#[cfg(feature = "async")]
fn is_path_allowed(path: &Path, root_dir: &Option<PathBuf>) -> bool {
if let Some(root) = root_dir {
path.starts_with(root)
} else {
true
}
}
#[cfg(feature = "async")]
pub fn glob_stream(
patterns: Patterns,
opts: GlobOptions,
predicates: Option<Predicates>,
) -> impl Stream<Item = Result<PathBuf, GlobError>> {
let semaphore = Arc::new(Semaphore::new(opts.max_inflight));
let patterns = Arc::new(patterns);
let predicates = Arc::new(predicates);
let batch_io = Arc::new(BatchIO::new(1000, opts.follow_symlinks));
let root = opts.root_dir.clone().unwrap_or_else(|| PathBuf::from("."));
stream! {
let mut visited_links = HashSet::new();
let mut stack = vec![(root, 0)];
while let Some((dir, depth)) = stack.pop() {
let mut rd = match fs::read_dir(&dir).await {
Ok(rd) => rd,
Err(e) => {
yield Err(GlobError::Io(e));
continue;
}
};
loop {
let entry = match rd.next_entry().await {
Ok(Some(entry)) => entry,
Ok(None) => break,
Err(e) => {
yield Err(GlobError::Io(e));
break;
}
};
let path = entry.path();
if !is_path_allowed(&path, &opts.root_dir) {
continue;
}
if opts.follow_symlinks && check_for_cycles(&path, &mut visited_links) {
continue;
}
let file_type = match entry.file_type().await {
Ok(ft) => ft,
Err(e) => {
yield Err(GlobError::Io(e));
continue;
}
};
let is_dir = file_type.is_dir();
let is_symlink = file_type.is_symlink();
if is_symlink && !opts.follow_symlinks {
continue;
}
if is_dir {
if let Some(max_depth) = opts.max_depth {
if depth >= max_depth {
continue;
}
}
stack.push((path.clone(), depth + 1));
continue;
}
let patterns_clone = patterns.clone();
let predicates_clone = predicates.clone();
let batch_io_clone = batch_io.clone();
let path_clone = path.clone();
let semaphore_clone = semaphore.clone();
let permit = match tokio::time::timeout(
opts.timeout.unwrap_or(Duration::from_secs(30)),
semaphore_clone.acquire_owned()
).await {
Ok(Ok(permit)) => permit,
Ok(Err(_)) => continue, Err(_) => continue, };
let join_handle = task::spawn_blocking(move || {
let _permit = permit;
let utf8_path = match Utf8PathBuf::from_path_buf(path_clone.clone()) {
Ok(p) => p,
Err(_) => return Ok(None), };
if !patterns_clone.is_match(&utf8_path) {
return Ok(None);
}
if let Some(preds) = &*predicates_clone {
let meta = match batch_io_clone.stat(&path_clone) {
Ok(meta) => meta,
Err(e) => return Err(e),
};
if !preds.matches(&meta) {
return Ok(None);
}
}
Ok(Some(path_clone))
});
match join_handle.await {
Ok(Ok(Some(file))) => yield Ok(file),
Ok(Ok(None)) => {}, Ok(Err(e)) => yield Err(e),
Err(e) => yield Err(GlobError::Other(format!("Task failed: {}", e))),
}
}
}
}
}