use ignore::overrides::OverrideBuilder;
use ignore::{WalkBuilder, WalkState};
use std::fs::Metadata;
use std::path::PathBuf;
use std::sync::mpsc::channel;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::paths::Paths;
#[derive(Debug, Clone)]
pub struct WalkEntry {
pub path: PathBuf,
pub rel: String,
pub metadata: Metadata,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum SkipReason {
TooLarge,
Empty,
Binary,
ReadError,
WalkError,
StatError,
}
impl SkipReason {
pub fn as_str(self) -> &'static str {
match self {
SkipReason::TooLarge => "too_large",
SkipReason::Empty => "empty",
SkipReason::Binary => "binary",
SkipReason::ReadError => "read_error",
SkipReason::WalkError => "walk_error",
SkipReason::StatError => "stat_error",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Skipped {
pub rel: String,
pub reason: SkipReason,
}
#[derive(Debug, Default)]
pub struct WalkResult {
pub entries: Vec<WalkEntry>,
pub skipped: Vec<Skipped>,
}
pub fn walk(paths: &Paths, config: &Config) -> Result<WalkResult> {
let mut overrides = OverrideBuilder::new(&paths.root);
for pat in &config.include {
overrides
.add(pat)
.map_err(|e| Error::other(format!("bad include glob {pat:?}: {e}")))?;
}
for pat in &config.exclude {
overrides
.add(&format!("!{pat}"))
.map_err(|e| Error::other(format!("bad exclude glob {pat:?}: {e}")))?;
}
let overrides = overrides
.build()
.map_err(|e| Error::other(format!("invalid overrides: {e}")))?;
let mut builder = WalkBuilder::new(&paths.root);
builder
.hidden(!config.index_hidden)
.git_ignore(config.respect_gitignore)
.git_global(config.respect_gitignore)
.git_exclude(config.respect_gitignore)
.ignore(config.respect_gitignore)
.parents(config.respect_gitignore)
.overrides(overrides)
.follow_links(false);
let (tx, rx) = channel::<WalkEntry>();
let (stx, srx) = channel::<Skipped>();
let root = paths.root.clone();
let max_size = config.max_file_size;
let index_empty = config.index_empty;
builder.build_parallel().run(|| {
let tx = tx.clone();
let stx = stx.clone();
let root = root.clone();
let rel_of = move |p: &std::path::Path| match p.strip_prefix(&root) {
Ok(r) => r.to_string_lossy().replace('\\', "/"),
Err(_) => p.to_string_lossy().to_string(),
};
Box::new(move |result| {
let dent = match result {
Ok(d) => d,
Err(e) => {
let _ = stx.send(Skipped {
rel: format!("<walk error: {e}>"),
reason: SkipReason::WalkError,
});
return WalkState::Continue;
}
};
match dent.file_type() {
Some(ft) if ft.is_file() => {}
_ => return WalkState::Continue,
}
let metadata = match dent.metadata() {
Ok(m) => m,
Err(_) => {
let _ = stx.send(Skipped {
rel: rel_of(dent.path()),
reason: SkipReason::StatError,
});
return WalkState::Continue;
}
};
let path = dent.into_path();
let rel = rel_of(&path);
let len = metadata.len();
if len == 0 {
if !index_empty {
let _ = stx.send(Skipped {
rel,
reason: SkipReason::Empty,
});
}
return WalkState::Continue;
}
if max_size != 0 && len > max_size {
let _ = stx.send(Skipped {
rel,
reason: SkipReason::TooLarge,
});
return WalkState::Continue;
}
let _ = tx.send(WalkEntry {
path,
rel,
metadata,
});
WalkState::Continue
})
});
drop(tx);
drop(stx);
Ok(WalkResult {
entries: rx.into_iter().collect(),
skipped: srx.into_iter().collect(),
})
}