use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::Result;
use tantivy::collector::DocSetCollector;
use tantivy::query::AllQuery;
use tantivy::schema::Value;
use tantivy::{ReloadPolicy, TantivyDocument};
use crate::events::{PendingChange, PendingOp};
use crate::extract::is_extractable;
use crate::indexer::{file_stat, walk_index_files};
use crate::ocr::is_image;
use crate::updater::IndexUpdater;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ReconcileStats {
pub added: usize,
pub modified: usize,
pub removed: usize,
}
pub fn reconcile(root: &Path, updater: &mut IndexUpdater) -> Result<ReconcileStats> {
let indexed = snapshot_indexed(updater, root)?;
let mut batch: Vec<PendingChange> = Vec::new();
let mut stats = ReconcileStats::default();
let mut seen: HashSet<PathBuf> = HashSet::with_capacity(indexed.len());
for path in walk_index_files(root).filter(|p| is_extractable(p) || is_image(p)) {
let Some((mtime, size)) = file_stat(&path) else {
continue;
};
seen.insert(path.clone());
match indexed.get(&path) {
None => {
batch.push(PendingChange {
path,
op: PendingOp::Upsert,
});
stats.added += 1;
}
Some(&(indexed_mtime, indexed_size)) => {
if indexed_mtime != mtime || indexed_size != size {
batch.push(PendingChange {
path,
op: PendingOp::Upsert,
});
stats.modified += 1;
}
}
}
}
for path in indexed.keys() {
if !seen.contains(path) {
batch.push(PendingChange {
path: path.clone(),
op: PendingOp::Remove,
});
stats.removed += 1;
}
}
if !batch.is_empty() {
updater.apply(&batch)?;
}
Ok(stats)
}
pub fn reconcile_orphans(roots: &[PathBuf], updater: &mut IndexUpdater) -> Result<usize> {
if roots.is_empty() {
return Ok(0);
}
let reader = updater
.index()
.reader_builder()
.reload_policy(ReloadPolicy::Manual)
.try_into()?;
let searcher = reader.searcher();
let fields = updater.fields();
let hits = searcher.search(&AllQuery, &DocSetCollector)?;
let mut batch: Vec<PendingChange> = Vec::new();
for addr in hits {
let doc: TantivyDocument = searcher.doc(addr)?;
let Some(path) = doc.get_first(fields.path).and_then(|v| v.as_str()) else {
continue;
};
let pbuf = PathBuf::from(path);
if !roots.iter().any(|r| pbuf.starts_with(r)) {
batch.push(PendingChange {
path: pbuf,
op: PendingOp::Remove,
});
}
}
let removed = batch.len();
if !batch.is_empty() {
updater.apply(&batch)?;
}
Ok(removed)
}
fn snapshot_indexed(updater: &IndexUpdater, root: &Path) -> Result<HashMap<PathBuf, (i64, u64)>> {
let reader = updater
.index()
.reader_builder()
.reload_policy(ReloadPolicy::Manual)
.try_into()?;
let searcher = reader.searcher();
let fields = updater.fields();
let hits = searcher.search(&AllQuery, &DocSetCollector)?;
let mut map = HashMap::with_capacity(hits.len());
for addr in hits {
let doc: TantivyDocument = searcher.doc(addr)?;
let Some(path) = doc.get_first(fields.path).and_then(|v| v.as_str()) else {
continue;
};
let pbuf = PathBuf::from(path);
if !pbuf.starts_with(root) {
continue;
}
let mtime = doc
.get_first(fields.mtime)
.and_then(|v| v.as_i64())
.unwrap_or(0);
let size = doc
.get_first(fields.size)
.and_then(|v| v.as_u64())
.unwrap_or(0);
map.insert(pbuf, (mtime, size));
}
Ok(map)
}