Menu
Public repository / edcee323076a7fb02387dd40cb06219db46e8579
AkurAI-Drive / client/src/daemon.rs
Latest change feb7ac86b5e60d55b429eacb7146cf0dc32c6628 - Add native Linux sync client (akurai-drive-sync) by olibuijr
//! Long-running daemon: sync once, then react to local file changes (debounced)
//! and poll the remote on a fixed interval. Exits cleanly on SIGINT.
use anyhow::{Context, Result};
use notify::{RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, RecvTimeoutError};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::config::Config;
use crate::sync::run_sync;
/// Quiet period after the last filesystem event before we sync.
const DEBOUNCE: Duration = Duration::from_secs(2);
/// How often to poll the remote for changes made elsewhere.
const POLL_INTERVAL: Duration = Duration::from_secs(30);
/// Watcher event drain tick.
const TICK: Duration = Duration::from_millis(500);
pub fn run(cfg: &Config) -> Result<()> {
let folder = cfg
.folder
.clone()
.map(PathBuf::from)
.context("no sync folder configured — run `init` first")?;
// SIGINT / SIGTERM → flip the running flag and let the loop unwind.
let running = Arc::new(AtomicBool::new(true));
{
let r = running.clone();
ctrlc::set_handler(move || r.store(false, Ordering::SeqCst))
.context("installing signal handler")?;
}
println!("Initial sync...");
if let Err(e) = run_sync(cfg, false) {
eprintln!("initial sync failed: {e:#}");
}
// Watch the folder recursively; events arrive on a channel.
let (tx, rx) = channel();
let mut watcher = notify::recommended_watcher(move |res| {
let _ = tx.send(res);
})
.context("creating filesystem watcher")?;
watcher
.watch(&folder, RecursiveMode::Recursive)
.with_context(|| format!("watching {}", folder.display()))?;
println!(
"Watching {} — syncing on change (debounced {}s) and polling every {}s. Ctrl-C to stop.",
folder.display(),
DEBOUNCE.as_secs(),
POLL_INTERVAL.as_secs()
);
let mut last_change: Option<Instant> = None;
let mut last_poll = Instant::now();
while running.load(Ordering::SeqCst) {
match rx.recv_timeout(TICK) {
Ok(event) => {
if is_relevant(&event) {
last_change = Some(Instant::now());
}
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => break,
}
// Debounced local change → sync.
if let Some(changed_at) = last_change {
if changed_at.elapsed() >= DEBOUNCE {
last_change = None;
sync_now(cfg, "local change");
last_poll = Instant::now();
}
}
// Periodic remote poll → sync.
if last_poll.elapsed() >= POLL_INTERVAL {
last_poll = Instant::now();
sync_now(cfg, "remote poll");
}
}
println!("\nShutting down cleanly.");
Ok(())
}
fn sync_now(cfg: &Config, reason: &str) {
match run_sync(cfg, true) {
Ok(s) => {
let touched = s.uploaded
+ s.downloaded
+ s.deleted_remote
+ s.deleted_local
+ s.conflicts
+ s.adopted;
if touched > 0 || s.errors > 0 {
println!("[{reason}] {}", s.to_line());
}
}
Err(e) => eprintln!("[{reason}] sync failed: {e:#}"),
}
}
/// Ignore pure access events and our own temp files.
fn is_relevant(event: ¬ify::Result<notify::Event>) -> bool {
let Ok(event) = event else { return true };
if matches!(event.kind, notify::EventKind::Access(_)) {
return false;
}
!event.paths.iter().all(|p| {
p.file_name()
.map(|n| n.to_string_lossy().starts_with(crate::util::TMP_PREFIX))
.unwrap_or(false)
})
}