use std::{collections::VecDeque, fs::FileType};
use camino::{Utf8Path, Utf8PathBuf};
use crate::IMAGE_EXTENSIONS;
#[derive(Debug)]
pub struct Walker {
base_path: Utf8PathBuf,
max_depth: usize,
dirs: VecDeque<Utf8PathBuf>,
current: std::vec::IntoIter<DirEntry>,
config_dir: Option<String>,
ignore: Vec<String>,
}
impl Walker {
pub fn new(dir: impl AsRef<Utf8Path>, max_depth: usize) -> Self {
let dir = dir.as_ref();
let mut dirs = VecDeque::new();
dirs.push_back(dir.to_path_buf());
Self {
base_path: dir.to_path_buf(),
max_depth,
dirs,
current: Vec::new().into_iter(),
config_dir: None,
ignore: Vec::new(),
}
}
pub fn set_config_dir(&mut self, dir: String) {
if !dir.starts_with('.') {
self.ignore.push(dir.clone());
}
self.config_dir = Some(dir);
}
pub fn ignore(&mut self, dir: String) {
self.ignore.push(dir);
}
#[tracing::instrument(level = "trace", skip(self), ret)]
fn process_dir(&mut self, dir: &Utf8Path) -> Result<(), std::io::Error> {
let mut new_dirs = Vec::new();
let mut new_entries = Vec::new();
for e in dir.read_dir_utf8()? {
let e = e?;
let ft = e.file_type()?;
if let Some(config_dir) = &self.config_dir {
if ft.is_dir()
&& e.file_name() == config_dir
&& entry_depth(e.path(), &self.base_path) > 1
{
tracing::warn!("Config dir `{config_dir}` found not in base path. It will be ignored. You may be running the application in the wrong directory.");
}
}
if e.file_name().starts_with('.') || self.ignore.iter().any(|d| d == e.file_name()) {
continue;
}
let entry = DirEntry {
path: e.into_path(),
file_type: ft,
};
if entry.file_type.is_dir() {
let depth = entry_depth(entry.path(), &self.base_path);
if depth <= self.max_depth {
new_dirs.push(entry.path().to_path_buf());
}
} else if !(entry.is_cooklang_file() || entry.is_image()) {
continue;
}
new_entries.push(entry);
}
new_dirs.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
new_entries.sort_by(|a, b| {
a.file_type
.is_dir()
.cmp(&b.file_type.is_dir())
.then_with(|| a.file_name().cmp(b.file_name()))
});
self.dirs.extend(new_dirs);
self.current = new_entries.into_iter();
Ok(())
}
}
impl Iterator for Walker {
type Item = Result<DirEntry, std::io::Error>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(entry) = self.current.next() {
return Some(Ok(entry));
}
while let Some(dir) = self.dirs.pop_front() {
if let Err(e) = self.process_dir(&dir) {
return Some(Err(e));
}
if let Some(entry) = self.current.next() {
return Some(Ok(entry));
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
path: Utf8PathBuf,
file_type: FileType,
}
impl DirEntry {
pub fn new(path: &Utf8Path) -> Result<Self, std::io::Error> {
let metadata = path.metadata()?;
Ok(Self {
path: path.to_path_buf(),
file_type: metadata.file_type(),
})
}
pub fn file_name(&self) -> &str {
self.path.file_name().unwrap_or(self.path.as_str())
}
pub fn file_stem(&self) -> &str {
self.path.file_stem().unwrap_or(self.path.as_str())
}
pub fn path(&self) -> &Utf8Path {
&self.path
}
pub fn into_path(self) -> Utf8PathBuf {
self.path
}
pub fn file_type(&self) -> FileType {
self.file_type
}
pub fn is_cooklang_file(&self) -> bool {
self.file_type.is_file() && self.path.extension().is_some_and(|e| e == "cook")
}
pub fn is_image(&self) -> bool {
self.path
.extension()
.is_some_and(|ext| IMAGE_EXTENSIONS.contains(&ext))
}
}
fn entry_depth(path: &Utf8Path, base_path: &Utf8Path) -> usize {
path.strip_prefix(base_path)
.expect("entry path not under base path")
.components()
.count()
}