use crate::error::PathError;
use crate::internal::validation::{is_hidden_name, reject_nul_path};
use crate::metadata::{EntryKind, FileEntry, SortMode, TraversalErrorPolicy, sort_entries};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::{DirEntry, WalkDir};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListOptions {
pub recursive: bool,
pub follow_symlinks: bool,
pub include_files: bool,
pub include_directories: bool,
pub include_symlinks: bool,
pub include_hidden: bool,
pub include_other: bool,
pub max_depth: Option<usize>,
pub max_entries: Option<usize>,
pub sort: SortMode,
pub error_policy: TraversalErrorPolicy,
pub include_root: bool,
}
impl Default for ListOptions {
fn default() -> Self {
Self {
recursive: false,
follow_symlinks: false,
include_files: true,
include_directories: true,
include_symlinks: true,
include_hidden: false,
include_other: false,
max_depth: None,
max_entries: None,
sort: SortMode::Path,
error_policy: TraversalErrorPolicy::FailFast,
include_root: false,
}
}
}
impl ListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
pub fn follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
pub fn include_files(mut self, include: bool) -> Self {
self.include_files = include;
self
}
pub fn include_directories(mut self, include: bool) -> Self {
self.include_directories = include;
self
}
pub fn include_symlinks(mut self, include: bool) -> Self {
self.include_symlinks = include;
self
}
pub fn include_other(mut self, include: bool) -> Self {
self.include_other = include;
self
}
pub fn include_hidden(mut self, include: bool) -> Self {
self.include_hidden = include;
self
}
pub fn max_depth(mut self, depth: Option<usize>) -> Self {
self.max_depth = depth;
self
}
pub fn max_entries(mut self, max: Option<usize>) -> Self {
self.max_entries = max;
self
}
pub fn sort(mut self, sort: SortMode) -> Self {
self.sort = sort;
self
}
pub fn error_policy(mut self, policy: TraversalErrorPolicy) -> Self {
self.error_policy = policy;
self
}
pub fn include_root(mut self, include: bool) -> Self {
self.include_root = include;
self
}
}
pub(crate) fn walk_depth_bounds(options: &ListOptions) -> (usize, usize) {
if options.recursive {
let max = options.max_depth.unwrap_or(usize::MAX);
let min = if options.include_root { 0 } else { 1 };
let min = min.min(max);
(min, max)
} else {
if options.include_root { (0, 1) } else { (1, 1) }
}
}
pub fn list(root: impl AsRef<Path>, options: &ListOptions) -> Result<Vec<FileEntry>, PathError> {
let mut entries = Vec::new();
for item in walk(root, options)? {
entries.push(item?);
if let Some(max) = options.max_entries {
if entries.len() >= max {
break;
}
}
}
sort_entries(&mut entries, options.sort);
if let Some(max) = options.max_entries {
entries.truncate(max);
}
Ok(entries)
}
pub fn walk(root: impl AsRef<Path>, options: &ListOptions) -> Result<WalkIter, PathError> {
let root = root.as_ref();
reject_nul_path(root)?;
ensure_listable_root(root)?;
let (min_depth, max_depth) = walk_depth_bounds(options);
let walker = WalkDir::new(root)
.min_depth(min_depth)
.max_depth(max_depth)
.follow_links(options.follow_symlinks)
.same_file_system(false);
Ok(WalkIter {
root: root.to_path_buf(),
options: options.clone(),
inner: walker.into_iter(),
yielded: 0,
})
}
pub struct WalkIter {
root: PathBuf,
options: ListOptions,
inner: walkdir::IntoIter,
yielded: usize,
}
impl Iterator for WalkIter {
type Item = Result<FileEntry, PathError>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(max) = self.options.max_entries {
if self.yielded >= max {
return None;
}
}
loop {
if let Some(max) = self.options.max_entries {
if self.yielded >= max {
return None;
}
}
let item = self.inner.next()?;
let dir_entry = match item {
Ok(e) => e,
Err(err) => match self.options.error_policy {
TraversalErrorPolicy::FailFast => {
return Some(Err(PathError::traversal(err.to_string())));
}
TraversalErrorPolicy::SkipErrors => continue,
},
};
match filter_and_build(&self.root, &dir_entry, &self.options) {
Ok(Some(entry)) => {
self.yielded += 1;
return Some(Ok(entry));
}
Ok(None) => continue,
Err(e) => match self.options.error_policy {
TraversalErrorPolicy::FailFast => return Some(Err(e)),
TraversalErrorPolicy::SkipErrors => continue,
},
}
}
}
}
fn ensure_listable_root(root: &Path) -> Result<(), PathError> {
let meta = fs::symlink_metadata(root).map_err(|e| PathError::filesystem(root, e))?;
if meta.is_dir() {
return Ok(());
}
if meta.file_type().is_symlink() {
match fs::metadata(root) {
Ok(m) if m.is_dir() => return Ok(()),
Ok(_) => {}
Err(e) => return Err(PathError::filesystem(root, e)),
}
}
Err(PathError::invalid(format!(
"list root is not a directory: {}",
root.to_string_lossy()
)))
}
fn filter_and_build(
root: &Path,
dir_entry: &DirEntry,
options: &ListOptions,
) -> Result<Option<FileEntry>, PathError> {
let path = dir_entry.path();
let name = dir_entry.file_name();
if name == "." || name == ".." {
return Ok(None);
}
let is_root = path == root;
if !is_root && !options.include_hidden && (is_hidden_name(name) || is_windows_hidden(path)) {
return Ok(None);
}
let relative = path.strip_prefix(root).ok().map(Path::to_path_buf);
entry_from_dir_entry(path, relative, dir_entry, options)
}
fn entry_from_dir_entry(
path: &Path,
relative: Option<PathBuf>,
dir_entry: &DirEntry,
options: &ListOptions,
) -> Result<Option<FileEntry>, PathError> {
let file_type = dir_entry.file_type();
let kind = if file_type.is_symlink() {
EntryKind::Symlink
} else if file_type.is_dir() {
EntryKind::Directory
} else if file_type.is_file() {
EntryKind::File
} else {
EntryKind::Other
};
if !kind_allowed(kind, options) {
return Ok(None);
}
let (size, modified, readonly) = match fs::symlink_metadata(path) {
Ok(meta) => {
let size = if meta.is_file() {
Some(meta.len())
} else {
None
};
let modified = meta.modified().ok();
let readonly = Some(meta.permissions().readonly());
(size, modified, readonly)
}
Err(e) => match options.error_policy {
TraversalErrorPolicy::FailFast => {
return Err(PathError::filesystem(path, e));
}
TraversalErrorPolicy::SkipErrors => (None, None, None),
},
};
Ok(Some(FileEntry {
path: path.to_path_buf(),
relative_path: relative,
kind,
size,
modified,
readonly,
}))
}
fn kind_allowed(kind: EntryKind, options: &ListOptions) -> bool {
match kind {
EntryKind::File => options.include_files,
EntryKind::Directory => options.include_directories,
EntryKind::Symlink => options.include_symlinks,
EntryKind::Other => options.include_other,
}
}
#[cfg(windows)]
fn is_windows_hidden(path: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
fs::symlink_metadata(path)
.map(|m| m.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0)
.unwrap_or(false)
}
#[cfg(not(windows))]
fn is_windows_hidden(_path: &Path) -> bool {
false
}
#[cfg(feature = "async")]
pub async fn list_async(
root: impl AsRef<Path> + Send + 'static,
options: ListOptions,
) -> Result<Vec<FileEntry>, PathError> {
let root = root.as_ref().to_path_buf();
tokio::task::spawn_blocking(move || list(root, &options))
.await
.map_err(|e| PathError::traversal(format!("async listing join failed: {e}")))?
}