use std::path::Path;
use anyhow::{Context, Result};
use crate::IndexProgress;
use crate::events::{PendingChange, PendingOp};
use crate::meta;
use crate::ocr_queue::OcrQueue;
use crate::updater::IndexUpdater;
#[derive(Debug, Clone, Copy)]
pub struct AddRootStats {
pub indexed: usize,
pub skipped: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct RemoveRootStats {
pub removed: usize,
}
pub fn add_root(index_dir: &Path, root: &Path, updater: &mut IndexUpdater) -> Result<AddRootStats> {
add_root_with_progress(index_dir, root, updater, |_| {})
}
pub fn add_root_with_progress(
index_dir: &Path,
root: &Path,
updater: &mut IndexUpdater,
on_progress: impl FnMut(IndexProgress),
) -> Result<AddRootStats> {
let root = root
.canonicalize()
.with_context(|| format!("目录不存在: {}", root.display()))?;
let existing = meta::registered_roots(index_dir)?;
meta::assert_no_root_nesting(&existing, &root)?;
let batch = [PendingChange {
path: root.clone(),
op: PendingOp::UpsertTree,
}];
let outcome = updater.apply_with_progress(&batch, on_progress)?;
meta::append_root(index_dir, &root)?;
Ok(AddRootStats {
indexed: outcome.upserted,
skipped: outcome.skipped,
})
}
pub fn remove_root(
index_dir: &Path,
root: &Path,
updater: &mut IndexUpdater,
) -> Result<RemoveRootStats> {
meta::remove_root_from_meta(index_dir, root)?;
let batch = [PendingChange {
path: root.to_path_buf(),
op: PendingOp::RemoveTree,
}];
let outcome = updater.apply(&batch)?;
let remaining = meta::registered_roots(index_dir)?;
let queue = OcrQueue::for_index_dir(index_dir);
queue.compact(&remaining);
queue.save().context("保存 OCR 队列状态失败")?;
Ok(RemoveRootStats {
removed: outcome.removed,
})
}
pub fn rebuild_root(
index_dir: &Path,
root: &Path,
updater: &mut IndexUpdater,
) -> Result<AddRootStats> {
rebuild_root_with_progress(index_dir, root, updater, |_| {})
}
pub fn rebuild_root_with_progress(
index_dir: &Path,
root: &Path,
updater: &mut IndexUpdater,
on_progress: impl FnMut(IndexProgress),
) -> Result<AddRootStats> {
remove_root(index_dir, root, updater)?;
add_root_with_progress(index_dir, root, updater, on_progress)
}
#[cfg(test)]
mod tests {
use super::*;
fn write(dir: &Path, name: &str, content: &str) {
std::fs::write(dir.join(name), content).unwrap();
}
#[test]
fn add_root_indexes_new_root_without_touching_existing() -> Result<()> {
let index_dir = tempfile::tempdir()?;
let a = tempfile::Builder::new().prefix("dowse-a-").tempdir()?;
write(a.path(), "a.md", "根 A 的内容 apricot");
crate::rebuild_index(index_dir.path(), a.path())?;
let b = tempfile::Builder::new().prefix("dowse-b-").tempdir()?;
write(b.path(), "b.md", "根 B 的内容 blueberry");
let mut updater = IndexUpdater::open(index_dir.path())?;
let stats = add_root(index_dir.path(), b.path(), &mut updater)?;
assert_eq!(stats.indexed, 1);
drop(updater);
let searcher = crate::Searcher::open(index_dir.path())?;
assert_eq!(
searcher.search("apricot", 10)?.len(),
1,
"A 的内容不应受影响"
);
assert_eq!(searcher.search("blueberry", 10)?.len(), 1, "B 的内容应可搜");
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 2, "roots 应该追加而不是覆盖");
Ok(())
}
#[test]
fn add_root_rejects_nested_directory() -> Result<()> {
let index_dir = tempfile::tempdir()?;
let a = tempfile::Builder::new().prefix("dowse-a-").tempdir()?;
write(a.path(), "a.md", "内容");
crate::rebuild_index(index_dir.path(), a.path())?;
let nested = a.path().join("sub");
std::fs::create_dir_all(&nested)?;
let mut updater = IndexUpdater::open(index_dir.path())?;
let err = add_root(index_dir.path(), &nested, &mut updater).expect_err("子目录应该被拒绝");
assert!(err.to_string().contains("嵌套"));
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 1, "校验失败不应该改动 roots");
Ok(())
}
#[test]
fn remove_root_deletes_only_that_roots_documents() -> Result<()> {
let index_dir = tempfile::tempdir()?;
let a = tempfile::Builder::new().prefix("dowse-a-").tempdir()?;
write(a.path(), "a.md", "根 A 的内容 apricot");
crate::rebuild_index(index_dir.path(), a.path())?;
let b = tempfile::Builder::new().prefix("dowse-b-").tempdir()?;
write(b.path(), "b.md", "根 B 的内容 blueberry");
let mut updater = IndexUpdater::open(index_dir.path())?;
add_root(index_dir.path(), b.path(), &mut updater)?;
let b_canonical = b.path().canonicalize()?;
let stats = remove_root(index_dir.path(), &b_canonical, &mut updater)?;
assert_eq!(stats.removed, 1);
drop(updater);
let searcher = crate::Searcher::open(index_dir.path())?;
assert_eq!(searcher.search("apricot", 10)?.len(), 1, "A 不受影响");
assert_eq!(searcher.search("blueberry", 10)?.len(), 0, "B 应该被删干净");
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 1);
Ok(())
}
#[test]
fn rebuild_root_reindexes_a_single_root() -> Result<()> {
let index_dir = tempfile::tempdir()?;
let a = tempfile::Builder::new().prefix("dowse-a-").tempdir()?;
write(a.path(), "a.md", "根 A 的初始内容 apricot");
crate::rebuild_index(index_dir.path(), a.path())?;
write(a.path(), "extra.md", "新增文件 blueberry");
let a_registered = meta::registered_roots(index_dir.path())?
.into_iter()
.next()
.expect("应该已经注册了一个根");
let mut updater = IndexUpdater::open(index_dir.path())?;
let stats = rebuild_root(index_dir.path(), &a_registered, &mut updater)?;
assert_eq!(stats.indexed, 2);
drop(updater);
let searcher = crate::Searcher::open(index_dir.path())?;
assert_eq!(searcher.search("apricot", 10)?.len(), 1);
assert_eq!(searcher.search("blueberry", 10)?.len(), 1);
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 1, "重建单根不应该改变根的数量");
Ok(())
}
}