use std::path::{Component, Path, PathBuf};
use std::time::SystemTime;
use globset::GlobSet;
use anyhow::Result;
pub fn rel_to_string(rel: &Path) -> String {
rel.components()
.filter_map(|c| match c {
Component::Normal(part) => Some(part.to_string_lossy()),
_ => None,
})
.collect::<Vec<_>>()
.join("/")
}
#[derive(Debug, Clone)]
pub struct ScannedFile {
pub rel_path: String,
pub abs_path: PathBuf,
pub modified: SystemTime,
}
pub fn is_supported_ext(ext: &str) -> bool {
matches!(ext, "md" | "json")
}
pub fn scan_directory(root: &Path, ignore: &GlobSet) -> Result<Vec<ScannedFile>> {
let mut files = Vec::new();
walk_dir(root, root, ignore, &mut files)?;
files.sort_by_key(|b| std::cmp::Reverse(b.modified));
Ok(files)
}
fn walk_dir(root: &Path, dir: &Path, ignore: &GlobSet, out: &mut Vec<ScannedFile>) -> Result<()> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(_) => continue, };
let path = entry.path();
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with('.'))
{
continue;
}
if path.is_dir() {
let rel = rel_to_string(path.strip_prefix(root).unwrap_or(&path));
if ignore.is_match(&rel) {
continue;
}
let before = out.len();
walk_dir(root, &path, ignore, out)?;
if out.len() == before {
continue;
}
} else if path
.extension()
.and_then(|e| e.to_str())
.is_some_and(is_supported_ext)
{
let rel = rel_to_string(path.strip_prefix(root).unwrap_or(&path));
if ignore.is_match(&rel) {
continue;
}
let Ok(meta) = entry.metadata() else {
continue;
};
let modified = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
out.push(ScannedFile {
rel_path: rel,
abs_path: path,
modified,
});
}
}
Ok(())
}
pub fn build_globset(patterns: &[String]) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
for p in patterns {
match globset::Glob::new(p) {
Ok(g) => {
builder.add(g);
}
Err(e) => {
eprintln!("Ignoring invalid glob pattern {p:?}: {e}");
}
}
}
builder.build().unwrap_or_else(|_| GlobSet::empty())
}
#[cfg(test)]
mod rel_tests {
use super::rel_to_string;
use std::path::Path;
#[test]
fn rel_to_string_joins_components_with_forward_slash() {
let p: std::path::PathBuf = ["a", "b", "c.md"].iter().collect();
assert_eq!(rel_to_string(&p), "a/b/c.md");
assert_eq!(rel_to_string(Path::new("flat.md")), "flat.md");
}
}