use anyhow::{Context, bail};
use oxibrain::{Brain, BrainConfig};
use oxibrain_connectors::scan_directory;
use oxibrain_core::{SyncAction, SyncFile, classify_sync, content_hash};
use oxibrain_ports::Timestamp;
use std::collections::HashMap;
use std::path::Path;
use std::time::UNIX_EPOCH;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SyncReport {
pub new: Vec<String>,
pub unchanged: Vec<String>,
pub modified: Vec<String>,
}
pub async fn run(dir: &Path, root: &Path, space: &str) -> anyhow::Result<()> {
let report = sync(dir, root, space).await?;
print_report(&report);
Ok(())
}
pub async fn sync(dir: &Path, root: &Path, space: &str) -> anyhow::Result<SyncReport> {
if !root.is_dir() {
bail!("not a directory: {}", root.display());
}
let files = scan_directory(root);
let brain = Brain::open(BrainConfig::at(dir)).await?;
let space_id = brain.ensure_space(space).await?;
let known = brain.note_hashes(&space_id).await?;
let mut contents: HashMap<String, (String, Timestamp)> = HashMap::new();
let sync_files: Vec<SyncFile> = files
.into_iter()
.filter_map(|f| {
let path = f.path.to_str()?.to_string();
let modified = systemtime_to_timestamp(f.modified);
let hash = content_hash(&f.content);
contents.insert(path.clone(), (f.content, modified));
Some(SyncFile {
path,
content_hash: hash,
modified,
})
})
.collect();
let mut report = SyncReport::default();
for action in classify_sync(sync_files, &known) {
match action {
SyncAction::New(f) => {
ingest_one(&brain, &space_id, &contents, &f).await?;
report.new.push(f.path);
}
SyncAction::Modified(f) => {
ingest_one(&brain, &space_id, &contents, &f).await?;
report.modified.push(f.path);
}
SyncAction::Unchanged(p) => report.unchanged.push(p),
}
}
Ok(report)
}
async fn ingest_one(
brain: &Brain,
space_id: &str,
contents: &HashMap<String, (String, Timestamp)>,
f: &SyncFile,
) -> anyhow::Result<()> {
let (content, occurred_at) = contents
.get(&f.path)
.with_context(|| format!("content missing for scanned path {}", f.path))?;
brain
.ingest_note(space_id, &f.path, content.clone(), *occurred_at)
.await?;
Ok(())
}
fn systemtime_to_timestamp(t: std::time::SystemTime) -> Timestamp {
let millis = t
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
Timestamp(millis)
}
fn print_report(report: &SyncReport) {
println!(
"sync complete: {} new, {} unchanged, {} modified",
report.new.len(),
report.unchanged.len(),
report.modified.len()
);
for p in &report.new {
println!(" new: {p}");
}
for p in &report.modified {
println!(" modified: {p}");
}
if !report.modified.is_empty() {
println!(
" note: modified paths append a new episode; previous versions remain — \
check `oxibrain contradictions` and `retract` stale claims"
);
}
}