use std::collections::{BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use super::KuzuImportError;
pub const STORE_DIR_NAME: &str = ".kuzu-memory";
pub const STORE_DB_NAME: &str = "memories.db";
const PRUNED: &[&str] = &[
".git",
"node_modules",
"target",
".venv",
"venv",
"__pycache__",
".cargo",
".rustup",
".Trash",
"Library",
];
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct DiscoveredStore {
pub dir: PathBuf,
pub db: PathBuf,
}
impl DiscoveredStore {
pub fn project_dir(&self) -> &Path {
self.dir.parent().unwrap_or(&self.dir)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedPath {
pub path: PathBuf,
pub reason: String,
}
#[derive(Debug, Default)]
pub struct Discovery {
pub stores: Vec<DiscoveredStore>,
pub skipped: Vec<SkippedPath>,
}
pub fn discover(roots: &[PathBuf], max_depth: usize) -> Discovery {
let mut found = BTreeSet::new();
let mut skipped = Vec::new();
for root in roots {
walk_root(root, max_depth, &mut found, &mut skipped);
}
Discovery {
stores: found.into_iter().collect(),
skipped,
}
}
fn walk_root(
root: &Path,
max_depth: usize,
found: &mut BTreeSet<DiscoveredStore>,
skipped: &mut Vec<SkippedPath>,
) {
let mut queue = VecDeque::from([(root.to_path_buf(), 0usize)]);
while let Some((dir, depth)) = queue.pop_front() {
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) => {
skipped.push(SkippedPath {
path: dir,
reason: format!("unreadable: {e}"),
});
continue;
}
};
for entry in entries.flatten() {
let Ok(kind) = entry.file_type() else {
continue;
};
let name = entry.file_name();
if kind.is_symlink() && name == STORE_DIR_NAME {
skipped.push(SkippedPath {
path: entry.path(),
reason: "symlink not followed; pass its target with --from".to_string(),
});
continue;
}
if !kind.is_dir() {
continue;
}
let path = entry.path();
if name == STORE_DIR_NAME {
match store_in_dir(&path) {
Some(store) => {
found.insert(store);
}
None => skipped.push(SkippedPath {
path,
reason: format!("no {STORE_DB_NAME} inside"),
}),
}
continue;
}
let child_depth = depth + 1;
if child_depth < max_depth && !PRUNED.iter().any(|p| name == *p) {
queue.push_back((path, child_depth));
}
}
}
}
fn store_in_dir(dir: &Path) -> Option<DiscoveredStore> {
let db = dir.join(STORE_DB_NAME);
if !db.exists() {
return None;
}
let dir = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
let db = dir.join(STORE_DB_NAME);
Some(DiscoveredStore { dir, db })
}
pub fn resolve_from(from: &Path) -> Result<DiscoveredStore, KuzuImportError> {
if from.file_name().is_some_and(|n| n == STORE_DB_NAME) && from.exists() {
if let Some(parent) = from.parent() {
if let Some(store) = store_in_dir(parent) {
return Ok(store);
}
}
}
if from.is_dir() {
if let Some(store) = store_in_dir(from) {
return Ok(store);
}
}
Err(KuzuImportError::NotAStore(from.to_path_buf()))
}