use super::fnc_read_dir::read_dir;
use crate::filter::fnc_apply_filters::apply_filters;
use crate::{Error, FileEntry, GitignoreMatcher, Result, TraversalOptions};
use ignore::WalkBuilder;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub async fn read_dir_recursive(
path: impl AsRef<Path>,
options: TraversalOptions,
) -> Result<Vec<FileEntry>> {
let path = path.as_ref().to_path_buf();
if options.gitignore {
return read_dir_with_walkbuilder(&path, &options).await;
}
let mut all_entries = Vec::new();
let mut visited = HashSet::new();
if let Ok(canon) = path.canonicalize() {
visited.insert(canon);
}
let max_depth = options.max_depth.unwrap_or(usize::MAX);
read_recursive_inner(
&path,
&options,
None, max_depth,
0,
&mut all_entries,
&mut visited,
)
.await?;
Ok(all_entries)
}
async fn read_dir_with_walkbuilder(
path: &Path,
options: &TraversalOptions,
) -> Result<Vec<FileEntry>> {
let path = path.to_path_buf();
let max_depth = options.max_depth;
let include_hidden = options.include_hidden;
let extensions: Vec<String> = options.extensions.clone();
let entries = tokio::task::spawn_blocking(move || {
let mut builder = WalkBuilder::new(&path);
builder
.hidden(!include_hidden) .git_ignore(true)
.git_global(true)
.git_exclude(true)
.ignore(true);
if let Some(depth) = max_depth {
builder.max_depth(Some(depth + 1)); }
let mut results = Vec::new();
for entry in builder.build() {
let entry = match entry {
Ok(e) => e,
Err(_) => continue, };
let entry_path = entry.path().to_path_buf();
if entry_path == path {
continue;
}
let metadata = match entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
let is_dir = metadata.is_dir();
if !is_dir && !extensions.is_empty() {
let has_ext = entry_path
.extension()
.and_then(|e| e.to_str())
.map(|ext| {
let ext_lower = ext.to_lowercase();
extensions
.iter()
.any(|allowed| allowed.to_lowercase() == ext_lower)
})
.unwrap_or(false);
if !has_ext {
continue;
}
}
let is_symlink = entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false);
let is_readonly = metadata.permissions().readonly();
let file_entry = FileEntry::new(
entry_path,
is_dir,
if is_dir { 0 } else { metadata.len() },
metadata.modified().ok(),
is_symlink,
is_readonly,
);
results.push(file_entry);
}
results
})
.await
.map_err(|e| Error::Io(std::io::Error::other(e)))?;
Ok(entries)
}
fn read_recursive_inner<'a>(
path: &'a Path,
options: &'a TraversalOptions,
gitignore: Option<&'a GitignoreMatcher>,
max_depth: usize,
current_depth: usize,
entries: &'a mut Vec<FileEntry>,
visited: &'a mut HashSet<PathBuf>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if current_depth > max_depth {
return Ok(());
}
let dir_entries = match read_dir(path).await {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in dir_entries {
let filter_result = apply_filters(&entry, options, gitignore);
if !filter_result.is_pass() {
continue;
}
let is_dir = entry.is_dir;
let entry_path = entry.path.clone();
entries.push(entry);
if is_dir && current_depth < max_depth {
if let Ok(canon) = entry_path.canonicalize() {
if visited.contains(&canon) {
continue;
}
visited.insert(canon);
}
read_recursive_inner(
&entry_path,
options,
gitignore,
max_depth,
current_depth + 1,
entries,
visited,
)
.await?;
}
}
Ok(())
})
}