use std::collections::HashSet;
use std::path::Path;
use std::time::Duration;
use tracing::{error, info, warn};
use crate::model::document::FileRecord;
use crate::scanner::changes::FileChanges;
use crate::scanner::watcher::{FileWatcher, WatchEvent};
use crate::service::OkcService;
impl OkcService {
pub fn watch(&mut self, initial_scan: bool) -> Result<(), anyhow::Error> {
let roots = self.index.config.roots.clone();
if roots.is_empty() {
anyhow::bail!(
"No root directories configured. Set `roots` in config or pass `--root`."
);
}
if initial_scan {
info!("Running initial full scan before watching...");
let result = self.scan()?;
info!(
"Initial scan: {} files ({} added, {} modified, {} deleted) in {:.2}s",
result.total_files,
result.added,
result.modified,
result.deleted,
result.duration_secs
);
}
let debounce_ms = self.index.config.watcher_debounce_ms;
let reconcile_secs = self.index.config.watcher_reconcile_secs;
info!(
"Starting file watcher (debounce={debounce_ms}ms, reconcile={reconcile_secs}s): {:?}",
roots
);
let watcher = FileWatcher::new(roots.clone(), debounce_ms, reconcile_secs);
let rx = watcher.start()?;
loop {
match rx.recv() {
Ok(WatchEvent::Changes(paths)) => {
if let Err(e) = self.handle_watch_changes(&paths) {
error!("Error processing watch changes: {e}");
}
}
Ok(WatchEvent::Reconcile) => {
info!("Running periodic full reconciliation...");
match self.scan() {
Ok(result) => {
info!(
"Reconciliation complete: {} files ({:.2}s)",
result.total_files, result.duration_secs
);
}
Err(e) => {
error!("Reconciliation scan failed: {e}");
}
}
}
Err(_) => {
info!("Watch channel closed, exiting.");
break;
}
}
}
Ok(())
}
fn handle_watch_changes(
&mut self,
changed: &HashSet<std::path::PathBuf>,
) -> Result<(), anyhow::Error> {
let canonical_roots: Vec<std::path::PathBuf> = self
.index
.config
.roots
.iter()
.filter_map(|r| std::fs::canonicalize(r).ok())
.collect();
let mut added_or_modified: Vec<FileRecord> = Vec::new();
let mut deleted: Vec<String> = Vec::new();
for pb in changed {
let canonical = std::fs::canonicalize(pb).unwrap_or_else(|_| pb.clone());
let full_path_str = canonical.to_string_lossy().to_string();
let rel_path = canonical_roots
.iter()
.find_map(|root| canonical.strip_prefix(root).ok())
.and_then(|rel| rel.to_str())
.unwrap_or(&full_path_str)
.to_string();
if canonical.exists() {
match Self::stat_file(pb, &rel_path) {
Ok(record) => added_or_modified.push(record),
Err(e) => warn!("Cannot stat changed file {rel_path}: {e}"),
}
} else {
info!("Detected deleted file: {rel_path}");
deleted.push(rel_path);
}
}
if added_or_modified.is_empty() && deleted.is_empty() {
return Ok(());
}
let known_paths: Vec<String> = self.index.load_paths()?;
let changes = FileChanges {
added: added_or_modified,
modified: Vec::new(),
deleted,
unchanged: Vec::new(),
};
let result = self.index.process_changes(&changes, &known_paths)?;
info!(
"Incremental update: {} added, {} modified, {} deleted ({} parse failures, {} broken links)",
result.files_added,
result.files_modified,
result.files_deleted,
result.parse_failures,
result.broken_links,
);
Ok(())
}
fn stat_file(path: &Path, rel_path: &str) -> Result<FileRecord, anyhow::Error> {
let meta = std::fs::metadata(path)?;
Ok(FileRecord {
path: rel_path.to_string(),
absolute_path: std::fs::canonicalize(path)
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.to_string(),
size: meta.len(),
modified_at: meta
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs() as i64,
})
}
}