use crate::{
file_system::{FileSystemAPI, WatchableFileSystemAPI},
ArchivalError,
};
use notify::{RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fs,
path::{Path, PathBuf},
};
use tracing::warn;
use walkdir::WalkDir;
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct NativeFileSystem {
pub root: PathBuf,
}
impl NativeFileSystem {
pub fn new(root: &Path) -> Self {
Self {
root: Path::new(root).to_owned(),
}
}
fn get_path(&self, rel: impl AsRef<Path>) -> PathBuf {
self.root.join(rel)
}
}
impl FileSystemAPI for NativeFileSystem {
fn root_dir(&self) -> &Path {
&self.root
}
fn exists(&self, path: impl AsRef<Path>) -> Result<bool, Box<dyn Error>> {
Ok(fs::metadata(self.get_path(path)).is_ok())
}
fn is_dir(&self, path: impl AsRef<Path>) -> Result<bool, Box<dyn Error>> {
Ok(self.get_path(path).is_dir())
}
fn remove_dir_all(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
Ok(fs::remove_dir_all(self.get_path(path))?)
}
fn create_dir_all(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
Ok(fs::create_dir_all(self.get_path(path))?)
}
fn read(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>, Box<dyn Error>> {
Ok(Some(fs::read(self.get_path(path))?))
}
fn read_to_string(&self, path: impl AsRef<Path>) -> Result<Option<String>, Box<dyn Error>> {
Ok(Some(fs::read_to_string(self.get_path(path))?))
}
fn delete(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
if self.is_dir(&path)? {
return Err(ArchivalError::new("use remove_dir_all to delete directories").into());
}
Ok(fs::remove_file(self.get_path(path))?)
}
fn write_str(
&mut self,
path: impl AsRef<Path>,
contents: String,
) -> Result<(), Box<dyn Error>> {
Ok(fs::write(self.get_path(path), contents)?)
}
fn write(&mut self, path: impl AsRef<Path>, contents: Vec<u8>) -> Result<(), Box<dyn Error>> {
Ok(fs::write(self.get_path(path), contents)?)
}
fn walk_dir(
&self,
path: impl AsRef<Path>,
include_dirs: bool,
) -> Result<Box<dyn Iterator<Item = PathBuf>>, Box<dyn Error>> {
let root = self.get_path(path);
let iterator = WalkDir::new(&root)
.follow_links(true)
.into_iter()
.filter(move |e| {
if include_dirs {
true
} else {
e.as_ref().is_ok_and(|de| de.file_type().is_file())
}
})
.filter_map(|e| e.ok())
.filter(|e| !e.file_name().to_string_lossy().starts_with("."))
.filter_map(move |e| {
e.into_path()
.strip_prefix(&root)
.ok()
.map(|p| p.to_path_buf())
});
Ok(Box::new(iterator))
}
}
impl WatchableFileSystemAPI for NativeFileSystem {
fn watch(
&self,
root: PathBuf,
watch_paths: Vec<String>,
changed: impl Fn(Vec<PathBuf>) + Send + Sync + 'static,
) -> Result<Box<dyn FnOnce()>, Box<dyn Error>> {
let root = fs::canonicalize(self.get_path(&root)).unwrap();
let watch_path = root.to_owned();
changed(vec![watch_path.to_path_buf()]);
let mut watcher = notify::recommended_watcher(
move |res: Result<notify::Event, notify::Error>| match res {
Ok(event) => {
let changed_paths: Vec<PathBuf> = event
.paths
.into_iter()
.filter(|p| {
if p.file_name()
.is_some_and(|f| f.to_string_lossy().starts_with("."))
{
return false;
}
if let Ok(rel) = p.strip_prefix(&root) {
for dir in &watch_paths {
let mut dir = dir.to_string();
if let Ok(stripped) = Path::new(&dir).strip_prefix(&root) {
dir = stripped.to_string_lossy().into_owned();
}
if rel.starts_with(dir) {
return true;
}
}
false
} else {
warn!(
"File changed outside of root ({}): {}",
root.display(),
p.display()
);
true
}
})
.collect();
if !changed_paths.is_empty() {
changed(changed_paths);
}
}
Err(e) => println!("watch error: {:?}", e),
},
)?;
watcher.watch(&watch_path, RecursiveMode::Recursive)?;
let path = watch_path.to_owned();
let unwatch = move || {
watcher.unwatch(&path).unwrap();
};
Ok(Box::new(unwatch))
}
}
impl std::fmt::Display for NativeFileSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.walk_dir("", false) {
Ok(paths) => {
write!(
f,
"{}:\n\t{}",
self.root_dir().display(),
paths
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n\t")
)
}
Err(e) => write!(f, "{}: {}", self.root_dir().display(), e),
}
}
}