use super::db::Store;
use super::{COLLECTION_EXTERNAL, external_excludes, extra_paths_config};
use std::path::PathBuf;
#[derive(Debug)]
pub(crate) struct ResolvedRoot {
pub root: PathBuf,
pub pattern: glob::Pattern,
}
#[derive(Debug, Default)]
pub(crate) struct ExternalReport {
pub indexed: usize,
pub missing_roots: Vec<String>,
pub unreadable_roots: Vec<String>,
pub skipped_nested: Vec<String>,
pub bad_patterns: Vec<String>,
pub pruned: usize,
pub on_disk: usize,
}
impl ExternalReport {
pub fn log(&self) {
for p in &self.missing_roots {
tracing::warn!("memory: extra path does not exist, not indexed: {p}");
}
for p in &self.unreadable_roots {
tracing::warn!("memory: extra path unreadable, not indexed: {p}");
}
for p in &self.skipped_nested {
tracing::warn!("memory: extra path nested inside another extra path, skipped: {p}");
}
for p in &self.bad_patterns {
tracing::warn!("memory: invalid glob pattern in extra path, using **/*.md: {p}");
}
if self.indexed > 0 || self.pruned > 0 || self.on_disk > 0 {
tracing::info!(
"memory: external paths — {} on disk, {} indexed, {} pruned",
self.on_disk,
self.indexed,
self.pruned
);
}
}
}
pub(crate) fn expand_path(raw: &str) -> PathBuf {
let home = crate::config::opencrabs_home();
if let Some(rest) = raw.strip_prefix("~/") {
return home.join(rest);
}
let p = PathBuf::from(raw);
if p.is_absolute() { p } else { home.join(p) }
}
pub(crate) fn resolve_roots() -> (Vec<ResolvedRoot>, ExternalReport) {
let mut report = ExternalReport::default();
let mut resolved: Vec<(String, ResolvedRoot)> = Vec::new();
for entry in extra_paths_config() {
let raw = entry.path().to_string();
let expanded = expand_path(&raw);
let root = match std::fs::canonicalize(&expanded) {
Ok(r) if r.is_dir() => r,
Ok(_) => {
report.missing_roots.push(raw);
continue;
}
Err(_) => {
if expanded.exists() {
report.unreadable_roots.push(raw);
} else {
report.missing_roots.push(raw);
}
continue;
}
};
let pattern = match glob::Pattern::new(entry.pattern()) {
Ok(p) => p,
Err(_) => {
report.bad_patterns.push(raw.clone());
glob::Pattern::new("**/*.md").expect("static pattern is valid")
}
};
resolved.push((raw, ResolvedRoot { root, pattern }));
}
let mut kept: Vec<ResolvedRoot> = Vec::new();
for (raw, cand) in resolved {
let nested = kept
.iter()
.any(|k| cand.root.starts_with(&k.root) && cand.root != k.root);
if nested {
report.skipped_nested.push(raw);
} else {
kept.push(cand);
}
}
(kept, report)
}
pub(crate) fn excluded(rel: &str, name: &str, is_dir: bool, excludes: &[glob::Pattern]) -> bool {
excludes.iter().any(|p| {
if p.matches(name) || p.matches(rel) {
return true;
}
is_dir && p.matches(&format!("{rel}/x"))
})
}
pub(crate) fn walk_root(root: &ResolvedRoot, excludes: &[glob::Pattern]) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut stack = vec![root.root.clone()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_symlink() {
continue;
}
let Ok(rel) = path.strip_prefix(&root.root) else {
continue;
};
let rel_str = rel.to_string_lossy().replace('\\', "/");
let name = entry.file_name().to_string_lossy().to_string();
if path.is_dir() {
if !excluded(&rel_str, &name, true, excludes) {
stack.push(path);
}
} else if path.is_file()
&& !excluded(&rel_str, &name, false, excludes)
&& root.pattern.matches(&rel_str)
{
files.push(path);
}
}
}
files.sort();
files
}
pub(crate) fn reindex_external(store: &Store) -> ExternalReport {
let (roots, mut report) = resolve_roots();
let excludes: Vec<glob::Pattern> = external_excludes()
.iter()
.filter_map(|s| glob::Pattern::new(s).ok())
.collect();
let mut on_disk: Vec<String> = Vec::new();
for root in &roots {
for path in walk_root(root, &excludes) {
let key = path.to_string_lossy().to_string();
on_disk.push(key.clone());
match std::fs::read_to_string(&path) {
Ok(body) if !body.trim().is_empty() => {
match super::index::index_file_sync_keyed(
store,
COLLECTION_EXTERNAL,
&key,
&body,
) {
Ok(true) => report.indexed += 1,
Ok(false) => {}
Err(e) => {
tracing::warn!("memory: failed to index external file {key}: {e}")
}
}
}
Ok(_) => {
on_disk.pop();
}
Err(e) => {
tracing::warn!("memory: unreadable external file {key}: {e}");
on_disk.pop();
}
}
}
}
if let Ok(db_paths) = store.get_active_document_paths(COLLECTION_EXTERNAL) {
for db_path in &db_paths {
if !on_disk.contains(db_path) {
let _ = store.deactivate_document(COLLECTION_EXTERNAL, db_path);
report.pruned += 1;
tracing::debug!("memory: pruned external document {db_path}");
}
}
}
report.on_disk = on_disk.len();
report
}