use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct WatchedFile<T: Clone> {
path: Option<PathBuf>,
content: Option<T>,
last_modified: Option<SystemTime>,
last_size: Option<u64>,
}
impl<T: Clone> WatchedFile<T> {
pub fn new(path: Option<PathBuf>) -> Self {
Self {
path,
content: None,
last_modified: None,
last_size: None,
}
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
pub fn has_changed(&self) -> bool {
let Some(path) = &self.path else {
return false;
};
match fs::metadata(path) {
Ok(metadata) => {
let modified = metadata.modified().ok();
let size = metadata.len();
if self.last_modified.is_none() && self.last_size.is_none() {
return true;
}
modified != self.last_modified || Some(size) != self.last_size
}
Err(_) => {
self.last_modified.is_some() || self.last_size.is_some()
}
}
}
pub fn load<F>(self, loader: F) -> Result<Self, String>
where
F: FnOnce(&Path) -> Result<T, String>,
{
let path = self
.path
.as_ref()
.ok_or_else(|| "No path configured for watched file".to_string())?;
if self.content.is_none() || self.has_changed() {
let content = loader(path)?;
Ok(self.with_content(content))
} else {
Ok(self)
}
}
pub fn reload_if_changed<F>(self, loader: F) -> Result<(Self, bool), String>
where
F: FnOnce(&Path) -> Result<T, String>,
{
if !self.has_changed() {
return Ok((self, false));
}
let path = self
.path
.as_ref()
.ok_or_else(|| "No path configured for watched file".to_string())?;
match loader(path) {
Ok(content) => Ok((self.with_content(content), true)),
Err(_) => {
Ok((self.cleared(), true))
}
}
}
pub fn content(&self) -> Option<&T> {
self.content.as_ref()
}
pub fn with_content_value(self, content: T) -> Self {
self.with_content(content)
}
fn with_content(self, content: T) -> Self {
let (last_modified, last_size) = if let Some(path) = &self.path {
if let Ok(metadata) = fs::metadata(path) {
(metadata.modified().ok(), Some(metadata.len()))
} else {
(None, None)
}
} else {
(None, None)
};
Self {
path: self.path,
content: Some(content),
last_modified,
last_size,
}
}
fn cleared(self) -> Self {
Self {
path: self.path,
content: None,
last_modified: None,
last_size: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_watched_file_basic() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "initial content").unwrap();
drop(file);
let watched = WatchedFile::<String>::new(Some(file_path.clone()));
let watched = watched
.load(|path| fs::read_to_string(path).map_err(|e| e.to_string()))
.unwrap();
assert_eq!(watched.content().map(|s| s.trim()), Some("initial content"));
assert!(!watched.has_changed());
std::thread::sleep(std::time::Duration::from_millis(10));
let mut file = File::create(&file_path).unwrap();
writeln!(file, "modified content").unwrap();
drop(file);
assert!(watched.has_changed());
let watched = watched
.load(|path| fs::read_to_string(path).map_err(|e| e.to_string()))
.unwrap();
assert_eq!(
watched.content().map(|s| s.trim()),
Some("modified content")
);
}
#[test]
fn test_watched_file_deleted() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "content").unwrap();
let watched = WatchedFile::<String>::new(Some(file_path.clone()));
let watched = watched
.load(|path| fs::read_to_string(path).map_err(|e| e.to_string()))
.unwrap();
assert_eq!(watched.content().map(|s| s.trim()), Some("content"));
fs::remove_file(&file_path).unwrap();
assert!(watched.has_changed());
let (watched, changed) = watched
.reload_if_changed(|path| fs::read_to_string(path).map_err(|e| e.to_string()))
.unwrap();
assert!(changed);
assert!(watched.content().is_none());
}
#[test]
fn test_watched_file_no_path() {
let watched = WatchedFile::<String>::new(None);
assert!(!watched.has_changed());
assert!(watched.content().is_none());
}
}