use std::path::Path;
use std::time::Instant;
use anyhow::{Context, Result};
use crate::events::{PendingChange, PendingOp};
use crate::meta;
use crate::ocr_queue::OcrQueue;
use crate::updater::IndexUpdater;
use crate::{IndexProgress, IndexStats};
#[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 index_root_incremental(
index_dir: &Path,
new_root: &Path,
updater: &mut IndexUpdater,
) -> Result<IndexStats> {
index_root_incremental_with_progress(index_dir, new_root, updater, |_| {})
}
pub fn index_root_incremental_with_progress(
index_dir: &Path,
new_root: &Path,
updater: &mut IndexUpdater,
on_progress: impl FnMut(IndexProgress),
) -> Result<IndexStats> {
let start = Instant::now();
let new_root = new_root
.canonicalize()
.with_context(|| format!("目录不存在: {}", new_root.display()))?;
let existing = meta::registered_roots(index_dir)?;
meta::assert_no_root_nesting_allow_exact(&existing, &new_root)?;
let (files, usn_cursors) = crate::indexer::collect_index_files(&new_root);
let (indexed, skipped, skipped_oversize) =
updater.upsert_files_with_progress(&files, on_progress)?;
meta::register_root_with_cursors(index_dir, &new_root, usn_cursors)?;
Ok(IndexStats {
indexed,
skipped,
skipped_oversize,
seconds: start.elapsed().as_secs_f64(),
})
}
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 index_root_incremental_adds_second_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 = index_root_incremental(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 的文档应进来"
);
assert_eq!(searcher.num_docs(), 2, "总文档数应为 2,无重复");
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 2, "roots 应该追加而不是覆盖");
Ok(())
}
#[test]
fn index_root_incremental_is_idempotent_on_rescan() -> 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())?;
index_root_incremental(index_dir.path(), b.path(), &mut updater)?;
let stats = index_root_incremental(index_dir.path(), b.path(), &mut updater)?;
assert_eq!(stats.indexed, 1, "重扫仍然收录同样的 1 篇");
drop(updater);
let searcher = crate::Searcher::open(index_dir.path())?;
assert_eq!(
searcher.search("blueberry", 10)?.len(),
1,
"重复补扫不应产生重复文档"
);
assert_eq!(searcher.num_docs(), 2, "总文档数仍为 2");
let roots = meta::registered_roots(index_dir.path())?;
assert_eq!(roots.len(), 2, "重复补扫不应重复登记根");
Ok(())
}
#[test]
fn index_root_incremental_respects_excluded_dirs() -> 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(), "keep.md", "保留 blueberry");
let nm = b.path().join("node_modules");
std::fs::create_dir_all(&nm)?;
std::fs::write(nm.join("skip.md"), "跳过 cranberry")?;
let mut updater = IndexUpdater::open(index_dir.path())?;
let stats = index_root_incremental(index_dir.path(), b.path(), &mut updater)?;
assert_eq!(stats.indexed, 1, "只应收录 node_modules 之外的 1 篇");
drop(updater);
let searcher = crate::Searcher::open(index_dir.path())?;
assert_eq!(searcher.search("blueberry", 10)?.len(), 1);
assert_eq!(
searcher.search("cranberry", 10)?.len(),
0,
"排除目录里的文件不应进索引"
);
Ok(())
}
#[test]
fn index_root_incremental_counts_oversize_skips_separately() -> 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(), "normal.txt", "正常内容 blueberry");
let big = b.path().join("huge.log");
let f = std::fs::File::create(&big)?;
f.set_len(crate::rules::IndexRules::default().max_file_bytes() + 1)?;
drop(f);
let mut updater = IndexUpdater::open(index_dir.path())?;
let stats = index_root_incremental(index_dir.path(), b.path(), &mut updater)?;
assert_eq!(stats.indexed, 1, "只有正常文件应被收录");
assert_eq!(stats.skipped, 1, "超限文件计入总跳过数");
assert_eq!(stats.skipped_oversize, 1, "超限文件单独计一份明细");
Ok(())
}
#[test]
fn index_root_incremental_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 = index_root_incremental(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 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(())
}
}