use crate::consts::DOT_ENV_FILE;
use crate::errors::ServerError;
use common::consts::GRAFBASE_SCHEMA_FILE_NAME;
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::runtime::Handle;
const FILE_WATCHER_INTERVAL: Duration = Duration::from_secs(1);
pub async fn start_watcher<P, T>(path: P, on_change: T) -> Result<(), ServerError>
where
P: AsRef<Path> + Send + 'static,
T: Fn(&PathBuf) + Send + 'static,
{
let (notify_sender, mut notify_receiver) = tokio::sync::mpsc::channel(1);
let handle = Handle::current();
let mut debouncer = new_debouncer(FILE_WATCHER_INTERVAL, None, move |res| {
handle.block_on(async { notify_sender.send(res).await.expect("must be open") });
})?;
debouncer.watcher().watch(path.as_ref(), RecursiveMode::Recursive)?;
loop {
match notify_receiver.recv().await {
Some(Ok(events)) => {
if let Some(event) = events
.iter()
.rev()
.find(|event| non_ignored_path(&event.path, path.as_ref()))
{
on_change(&event.path);
}
}
Some(Err(errors)) => {
if let Some(error) = errors
.into_iter()
.find(|error| error.paths.contains(&path.as_ref().to_owned()))
{
return Err(ServerError::FileWatcher(error));
}
}
None => {}
}
}
}
const ROOT_FILE_WHITELIST: [&str; 2] = [GRAFBASE_SCHEMA_FILE_NAME, DOT_ENV_FILE];
const EXTENSION_WHITELIST: [&str; 11] = [
"js", "ts", "jsx", "tsx", "mjs", "mts", ".wasm", "cjs", "json", "yaml", "yml",
];
const DIRECTORY_BLACKLIST: [&str; 1] = ["node_modules"];
fn non_ignored_path(path: &Path, root: &Path) -> bool {
likely_not_a_dir(path)
&& (whitelisted_root_file(path, root) || (!in_blacklisted_directory(path, root) && whitelisted_extension(path)))
}
fn likely_not_a_dir(path: &Path) -> bool {
path.metadata().map(|metadata| metadata.is_dir()).ok() == Some(false)
}
fn whitelisted_root_file(path: &Path, root: &Path) -> bool {
let in_root = path.parent().filter(|parent| *parent == root).is_some();
in_root
&& path
.file_name()
.and_then(OsStr::to_str)
.filter(|file_name| ROOT_FILE_WHITELIST.contains(file_name))
.is_some()
}
fn in_blacklisted_directory(path: &Path, root: &Path) -> bool {
path.strip_prefix(root)
.expect("must contain root directory")
.iter()
.any(|path_part| {
path_part
.to_str()
.filter(|path_part| DIRECTORY_BLACKLIST.contains(path_part))
.is_some()
})
}
fn whitelisted_extension(path: &Path) -> bool {
path.extension()
.and_then(OsStr::to_str)
.filter(|extension| EXTENSION_WHITELIST.contains(extension))
.is_some()
}