use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchDecision {
Ignore,
Pending,
Ready(PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FileFingerprint {
length: u64,
modified: SystemTime,
}
#[derive(Debug)]
pub struct StableFileTracker {
root: PathBuf,
observations: HashMap<PathBuf, FileFingerprint>,
}
impl StableFileTracker {
pub fn new(root: &Path) -> Result<Self, WatchError> {
let root = root.canonicalize().map_err(WatchError::Filesystem)?;
Ok(Self {
root,
observations: HashMap::new(),
})
}
#[must_use]
pub fn observe(&mut self, path: &Path) -> WatchDecision {
let Ok(link_metadata) = fs::symlink_metadata(path) else {
return WatchDecision::Ignore;
};
if link_metadata.file_type().is_symlink() || !link_metadata.is_file() {
return WatchDecision::Ignore;
}
let Ok(path) = path.canonicalize() else {
return WatchDecision::Ignore;
};
if !path.starts_with(&self.root) {
return WatchDecision::Ignore;
}
let Ok(metadata) = fs::metadata(&path) else {
return WatchDecision::Ignore;
};
let Ok(modified) = metadata.modified() else {
return WatchDecision::Ignore;
};
let fingerprint = FileFingerprint {
length: metadata.len(),
modified,
};
if self.observations.get(&path) == Some(&fingerprint) {
self.observations.remove(&path);
WatchDecision::Ready(path)
} else {
self.observations.insert(path, fingerprint);
WatchDecision::Pending
}
}
}
#[derive(Debug, Error)]
pub enum WatchError {
#[error("watcher filesystem operation failed: {0}")]
Filesystem(#[source] std::io::Error),
}