From fcfbecc2b8a586336aadc7bfab52f26a4560b793 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:18:01 +1300 Subject: [PATCH 1/7] refac: file keeper --- Cargo.lock | 30 ++++++ Cargo.toml | 4 + src/frontmatter_file.rs | 15 ++- src/frontmatter_file/{map.rs => keeper.rs} | 67 ++++++++++---- src/fs.rs | 7 +- src/main.rs | 103 +++++++-------------- src/utf8_filepath.rs | 60 ------------ 7 files changed, 130 insertions(+), 156 deletions(-) rename src/frontmatter_file/{map.rs => keeper.rs} (68%) delete mode 100644 src/utf8_filepath.rs diff --git a/Cargo.lock b/Cargo.lock index 513a66a..dd4fc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + [[package]] name = "cc" version = "1.0.83" @@ -198,8 +204,10 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "camino", "chrono", "notify", + "pretty_assertions", "serde", "serde_json", "serde_yaml", @@ -207,6 +215,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "equivalent" version = "1.0.1" @@ -619,6 +633,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -1163,3 +1187,9 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 7584964..6af6956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/Teajey/custard" [dependencies] anyhow = "1.0.75" axum = "0.6.20" +camino = "1.1.6" chrono = { version = "0.4.31", features = ["serde"] } notify = "5.2.0" serde = { version = "1.0.188", features = ["serde_derive"] } @@ -18,3 +19,6 @@ serde_json = "1.0.107" serde_yaml = "0.9.25" thiserror = "1.0.49" tokio = { version = "1.32.0", features = ["full"] } + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/src/frontmatter_file.rs b/src/frontmatter_file.rs index 62279ce..092a706 100644 --- a/src/frontmatter_file.rs +++ b/src/frontmatter_file.rs @@ -1,11 +1,11 @@ -pub mod map; +pub mod keeper; use anyhow::Result; +use camino::{Utf8Path as Path, Utf8PathBuf}; use chrono::{DateTime, Utc}; use serde::Serialize; -use crate::utf8_filepath::UTF8FilePath; -pub use map::Map; +pub use keeper::Keeper; #[derive(Debug, Clone, Serialize)] pub struct FrontmatterFile { @@ -69,6 +69,8 @@ pub enum ReadFromPathError { Yaml(String, serde_yaml::Error), #[error("Failed to load: {0}")] Io(#[from] std::io::Error), + #[error("Tried to read from path with no file name: {0}")] + NoFileNamePath(Utf8PathBuf), } impl FrontmatterFile { @@ -92,8 +94,11 @@ impl FrontmatterFile { &self.modified } - pub fn read_from_path(path: &UTF8FilePath) -> Result { - let name = path.name().to_owned(); + pub fn read_from_path(path: &Path) -> Result { + let name = path + .file_name() + .ok_or_else(|| ReadFromPathError::NoFileNamePath(path.to_path_buf()))? + .to_owned(); let metadata = std::fs::metadata(path)?; let modified = metadata.modified()?.into(); let created = metadata.created()?.into(); diff --git a/src/frontmatter_file/map.rs b/src/frontmatter_file/keeper.rs similarity index 68% rename from src/frontmatter_file/map.rs rename to src/frontmatter_file/keeper.rs index 591a642..f8bea8d 100644 --- a/src/frontmatter_file/map.rs +++ b/src/frontmatter_file/keeper.rs @@ -1,27 +1,57 @@ use std::{ - collections::HashMap, + collections::{hash_map::Values, HashMap}, sync::{Arc, Mutex}, }; -use crate::{fs::path_has_extensions, utf8_filepath::UTF8FilePath}; +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::fs::{self, path_has_extensions}; use super::FrontmatterFile; -pub struct Map { - pub inner: HashMap, +pub struct Keeper { + inner: HashMap, +} + +#[derive(Debug, thiserror::Error)] +pub enum NewKeeperError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Failed to load frontmatter file: {0}")] + ReadFrontmatterFromPath(#[from] super::ReadFromPathError), +} + +impl Keeper { + pub fn new(path: &Utf8Path) -> Result { + let markdown_fps = fs::filepaths_with_extensions(path, &["md"])? + .into_iter() + .map(|path| -> Result<_, super::ReadFromPathError> { + let md = FrontmatterFile::read_from_path(&path)?; + + Ok((path, md)) + }) + .collect::, _>>()?; + Ok(Keeper { + inner: markdown_fps, + }) + } + + pub fn files(&self) -> Values<'_, Utf8PathBuf, FrontmatterFile> { + self.inner.values() + } } #[derive(Clone)] -pub struct ArcMutex(pub Arc>); +pub struct ArcMutex(pub Arc>); impl ArcMutex { - pub fn new(map: HashMap) -> Self { - Self(Arc::new(Mutex::new(Map { inner: map }))) + pub fn new(keeper: Keeper) -> Self { + Self(Arc::new(Mutex::new(keeper))) } } -impl Map { - fn process_rename_event(&mut self, path: &UTF8FilePath) { +impl Keeper { + fn process_rename_event(&mut self, path: &Utf8Path) { let was_removed = self.inner.remove(path).is_some(); if !was_removed { let file = match FrontmatterFile::read_from_path(path) { @@ -31,11 +61,11 @@ impl Map { return; } }; - self.inner.insert(path.clone(), file); + self.inner.insert(path.to_owned(), file); } } - fn process_edit_event(&mut self, path: &UTF8FilePath) { + fn process_edit_event(&mut self, path: &Utf8Path) { let Some(file) = self.inner.get_mut(path) else { eprintln!("Couldn't find ({path:?}) in Edit event."); return; @@ -50,14 +80,14 @@ impl Map { *file = new_file; } - fn process_removal_event(&mut self, path: &UTF8FilePath) { + fn process_removal_event(&mut self, path: &Utf8Path) { let was_removed = self.inner.remove(path).is_some(); if !was_removed { eprintln!("Couldn't find ({path:?}) in Remove event.."); } } - fn process_create_event(&mut self, path: &UTF8FilePath) { + fn process_create_event(&mut self, path: &Utf8Path) { if self.inner.contains_key(path) { eprintln!( "A Create event occurred for a path ({path:?}) but it already exists in memory." @@ -71,7 +101,7 @@ impl Map { return; } }; - self.inner.insert(path.clone(), new_file); + self.inner.insert(path.to_owned(), new_file); } } @@ -84,16 +114,16 @@ impl notify::EventHandler for ArcMutex { attrs: _, }) => { let path = paths.first().expect("event must have at least one path"); - if !path_has_extensions(path, &["md"]) { - return; - } - let path = match UTF8FilePath::try_from(path.clone()) { + let path = match Utf8PathBuf::try_from(path.clone()) { Ok(path) => path, Err(err) => { eprintln!("Event filepath ({path:?}) was not UTF-8: {err}\n\nNon-UTF-8 paths not supported."); return; } }; + if !path_has_extensions(&path, &["md"]) { + return; + } let mut map = match self.0.as_ref().lock() { Ok(map) => map, Err(err) => { @@ -125,3 +155,4 @@ impl notify::EventHandler for ArcMutex { } } } + diff --git a/src/fs.rs b/src/fs.rs index 1a42ffd..86af45d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,8 +1,7 @@ -use std::path::{Path, PathBuf}; +use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf}; pub fn path_has_extensions(path: &Path, extensions: &[&str]) -> bool { path.extension() - .and_then(std::ffi::OsStr::to_str) .is_some_and(|ext| extensions.contains(&ext)) } @@ -10,11 +9,11 @@ pub fn filepaths_with_extensions( dir: &Path, extensions: &[&str], ) -> Result, std::io::Error> { - std::fs::read_dir(dir)? + dir.read_dir_utf8()? .filter_map(|entry| { entry .map(|entry| { - let path = entry.path(); + let path = entry.path().to_path_buf(); if !path.is_file() { return None; } diff --git a/src/main.rs b/src/main.rs index 88ef7d1..1938870 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,6 @@ mod frontmatter_file; mod frontmatter_query; mod fs; -mod utf8_filepath; - -use std::collections::HashMap; use anyhow::{anyhow, Result}; use axum::{ @@ -11,27 +8,20 @@ use axum::{ http::{HeaderMap, StatusCode}, routing, Json, Router, }; +use camino::Utf8PathBuf; use frontmatter_query::FrontmatterQuery; use notify::{RecursiveMode, Watcher}; -use frontmatter_file::FrontmatterFile; -use utf8_filepath::UTF8FilePath; - async fn frontmatter_query_post( - State(markdown_files): State, + State(markdown_files): State, Json(query): Json, ) -> Result>, StatusCode> { - let map = markdown_files.0.as_ref(); - let map = match map.lock() { - Ok(map) => map, - Err(err) => { - eprintln!("Failed to lock data on a get_many request: {err}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - let mut files = map - .inner - .values() + let keeper = markdown_files.0.as_ref().lock().map_err(|err| { + eprintln!("Failed to lock data on a get_many request: {err}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let mut files = keeper + .files() .filter(|file| { let Some(frontmatter) = file.frontmatter() else { return query.is_empty(); @@ -51,19 +41,14 @@ async fn frontmatter_query_post( } async fn frontmatter_list_get( - State(markdown_files): State, + State(markdown_files): State, ) -> Result>, StatusCode> { - let map = markdown_files.0.as_ref(); - let map = match map.lock() { - Ok(map) => map, - Err(err) => { - eprintln!("Failed to lock data on a get_many request: {err}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - let mut files = map - .inner - .values() + let keeper = markdown_files.0.as_ref().lock().map_err(|err| { + eprintln!("Failed to lock data on a get_many request: {err}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let mut files = keeper + .files() .map(|file| file.clone().into()) .collect::>(); files.sort(); @@ -72,20 +57,15 @@ async fn frontmatter_list_get( } async fn frontmatter_file_get( - State(markdown_files): State, + State(markdown_files): State, Path(name): Path, ) -> Result<(HeaderMap, String), StatusCode> { - let map = markdown_files.0.as_ref(); - let map = match map.lock() { - Ok(map) => map, - Err(err) => { - eprintln!("Failed to lock data on a get_file request: {err}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - let file = map - .inner - .values() + let keeper = markdown_files.0.as_ref().lock().map_err(|err| { + eprintln!("Failed to lock data on a get_file request: {err}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let file = keeper + .files() .find(|file| file.name() == name) .ok_or(StatusCode::NOT_FOUND)?; @@ -121,20 +101,15 @@ async fn frontmatter_file_get( } async fn frontmatter_collate_strings_get( - State(markdown_files): State, + State(markdown_files): State, Path(key): Path, ) -> Result>, StatusCode> { - let map = markdown_files.0.as_ref(); - let map = match map.lock() { - Ok(map) => map, - Err(err) => { - eprintln!("Failed to lock data on a get_collate_strings request: {err}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - let mut values = map - .inner - .values() + let keeper = markdown_files.0.as_ref().lock().map_err(|err| { + eprintln!("Failed to lock data on a get_collate_strings request: {err}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let mut values = keeper + .files() .filter_map(|fmf| fmf.frontmatter()) .filter_map(|fm| fm.get(&key)) .filter_map(|v| match v { @@ -165,25 +140,15 @@ async fn run() -> Result<()> { } let current_dir = std::env::current_dir()?; - let markdown_fps = fs::filepaths_with_extensions(¤t_dir, &["md"])? - .into_iter() - .map(UTF8FilePath::try_from) - .collect::, _>>() - .map_err(|err| anyhow!("Target paths are not all UTF-8 files: {err}"))?; - let markdown_files = markdown_fps - .into_iter() - .map(|path| { - let md = FrontmatterFile::read_from_path(&path)?; - - Ok((path, md)) - }) - .collect::>>()?; + let current_dir = Utf8PathBuf::try_from(current_dir)?; + + let keeper = frontmatter_file::Keeper::new(¤t_dir)?; - let markdown_files = frontmatter_file::map::ArcMutex::new(markdown_files); + let markdown_files = frontmatter_file::keeper::ArcMutex::new(keeper); let mut watcher = notify::recommended_watcher(markdown_files.clone())?; - watcher.watch(¤t_dir, RecursiveMode::NonRecursive)?; + watcher.watch(current_dir.as_std_path(), RecursiveMode::NonRecursive)?; let app = Router::new() .route("/frontmatter/query", routing::post(frontmatter_query_post)) diff --git a/src/utf8_filepath.rs b/src/utf8_filepath.rs deleted file mode 100644 index 8c96d6f..0000000 --- a/src/utf8_filepath.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::fmt::Debug; -use std::path::{Path, PathBuf}; - -#[derive(Hash, PartialEq, Eq, Clone)] -pub struct UTF8FilePath { - path_buf: PathBuf, - name: String, - extension: Option, -} - -impl Debug for UTF8FilePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.path_buf.fmt(f) - } -} - -impl AsRef for UTF8FilePath { - fn as_ref(&self) -> &Path { - self.path_buf.as_ref() - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Path has no file name (it ends with `..`)")] - NoFileName, - #[error("Path is non-UTF-8")] - NonUTF8, -} - -impl TryFrom for UTF8FilePath { - type Error = Error; - - fn try_from(path_buf: PathBuf) -> Result { - let name = path_buf - .file_name() - .ok_or(Error::NoFileName)? - .to_str() - .ok_or(Error::NonUTF8)? - .to_owned(); - let extension = path_buf - .extension() - .map(|ext| ext.to_str().expect("utf-8 was already checked").to_owned()); - Ok(Self { - path_buf, - name, - extension, - }) - } -} - -impl UTF8FilePath { - pub fn name(&self) -> &str { - &self.name - } - - // pub fn extension(&self) -> Option<&str> { - // self.extension.as_deref() - // } -} From acf4a08a5cfed5623adc6236c8fa55f2344e8276 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:18:21 +1300 Subject: [PATCH 2/7] test: file monitoring --- .github/workflows/rust.yml | 31 ++++++ src/frontmatter_file/keeper.rs | 171 +++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..2287ca0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,31 @@ +name: Rust + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + RUST_FLAGS: "-Dwarnings" + +jobs: + test: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + - name: Cache + uses: actions/cache@v3.2.6 + with: + path: | + ~/.cargo + target + key: build-${{ runner.os }} + restore-keys: | + build-${{ runner.os }} + - name: Lint + run: cargo clippy --all-targets + - name: Test + run: cargo test diff --git a/src/frontmatter_file/keeper.rs b/src/frontmatter_file/keeper.rs index f8bea8d..29f89a0 100644 --- a/src/frontmatter_file/keeper.rs +++ b/src/frontmatter_file/keeper.rs @@ -156,3 +156,174 @@ impl notify::EventHandler for ArcMutex { } } +#[cfg(test)] +mod test { + use std::io::Write; + + use camino::Utf8PathBuf; + use notify::{EventHandler, RecursiveMode, Watcher}; + + use super::{ArcMutex, Keeper}; + + struct TestFile { + path: Utf8PathBuf, + } + + impl TestFile { + fn generate(&self) -> std::io::Result<()> { + let _ = std::fs::File::create(&self.path)?; + Ok(()) + } + + fn write(&self, str: T) -> std::io::Result<()> { + let mut file = std::fs::OpenOptions::new() + .write(true) + .append(true) + .open(&self.path)?; + + write!(file, "{str}")?; + + Ok(()) + } + + fn delete(&self) -> std::io::Result<()> { + std::fs::remove_file(&self.path) + } + } + + impl Drop for TestFile { + fn drop(&mut self) { + if self.path.exists() { + std::fs::remove_file(&self.path).unwrap(); + } + } + } + + #[test] + #[allow(clippy::too_many_lines)] + fn file_monitoring() { + let test_file_name = "test.md"; + let wd = Utf8PathBuf::try_from(std::env::temp_dir()).unwrap(); + let test_file_path = wd.join(test_file_name); + let test_file = TestFile { + path: test_file_path, + }; + let keeper = ArcMutex::new(Keeper::new(&wd).unwrap()); + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut keeper_mut = keeper.clone(); + let tx_clone = tx.clone(); + let mut watcher = + notify::recommended_watcher(move |event: Result| { + match event { + Ok(event) => { + keeper_mut.handle_event(Ok(event.clone())); + tx_clone.send(Ok(event)).unwrap(); + } + Err(err) => { + keeper_mut.handle_event(Err(err)); + tx_clone.send(Err(())).unwrap(); + } + } + }) + .unwrap(); + + watcher + .watch(wd.as_std_path(), RecursiveMode::NonRecursive) + .unwrap(); + + { + let keeper = keeper.0.as_ref().lock().unwrap(); + let file = keeper.files().find(|file| file.name() == test_file_name); + assert!(file.is_none()); + } + + test_file.generate().unwrap(); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Create(notify::event::CreateKind::File), + event.kind, + "Expecting a file create event" + ); + + let first_line = "Just call me Mark!\n"; + test_file.write(first_line).unwrap(); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Create(notify::event::CreateKind::File), + event.kind, + "Expecting a file create event" + ); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Modify(notify::event::ModifyKind::Data( + notify::event::DataChange::Content + )), + event.kind, + "Expecting a content modification event" + ); + + { + let keeper = keeper.0.as_ref().lock().unwrap(); + let file = keeper + .files() + .find(|file| file.name() == test_file_name) + .expect("Keeper should have file now"); + assert_eq!(first_line, file.body); + } + + let second_line = "I'm a markdown file!\n"; + test_file.write(second_line).unwrap(); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Create(notify::event::CreateKind::File), + event.kind, + "Expecting a create event" + ); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Modify(notify::event::ModifyKind::Data( + notify::event::DataChange::Content + )), + event.kind, + "Expecting a content modification event" + ); + + { + let keeper = keeper.0.as_ref().lock().unwrap(); + let file = keeper + .files() + .find(|file| file.name() == test_file_name) + .unwrap(); + assert_eq!([first_line, second_line].join(""), file.body); + } + + test_file.delete().unwrap(); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Create(notify::event::CreateKind::File), + event.kind, + "Expecting a create event" + ); + + let event = rx.recv().unwrap().unwrap(); + pretty_assertions::assert_eq!( + notify::EventKind::Remove(notify::event::RemoveKind::File), + event.kind, + "Expecting a file delete event" + ); + + { + let keeper = keeper.0.as_ref().lock().unwrap(); + let file = keeper.files().find(|file| file.name() == test_file_name); + assert!(file.is_none()); + } + } +} From 6556b58d938b20694cf407b77abffbb248c30b56 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:51:15 +1300 Subject: [PATCH 3/7] feat: map notify events to a simpler format --- src/frontmatter_file/keeper.rs | 109 +++++++++++++++------------------ 1 file changed, 50 insertions(+), 59 deletions(-) diff --git a/src/frontmatter_file/keeper.rs b/src/frontmatter_file/keeper.rs index 29f89a0..df01404 100644 --- a/src/frontmatter_file/keeper.rs +++ b/src/frontmatter_file/keeper.rs @@ -9,6 +9,31 @@ use crate::fs::{self, path_has_extensions}; use super::FrontmatterFile; +// Let's keep the possible events simpler for our needs +#[derive(Debug, PartialEq)] +enum FsEvent { + Rename, + Edit, + Create, + Delete, + Unhandled(notify::EventKind), +} + +impl From for FsEvent { + fn from(event_kind: notify::EventKind) -> Self { + use notify::event::{ + CreateKind, DataChange, EventKind, ModifyKind, RemoveKind, RenameMode, + }; + match event_kind { + EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => Self::Rename, + EventKind::Modify(ModifyKind::Data(DataChange::Content)) => Self::Edit, + EventKind::Remove(RemoveKind::File) => Self::Delete, + EventKind::Create(CreateKind::File) => Self::Create, + unhandled => Self::Unhandled(unhandled), + } + } +} + pub struct Keeper { inner: HashMap, } @@ -41,15 +66,6 @@ impl Keeper { } } -#[derive(Clone)] -pub struct ArcMutex(pub Arc>); - -impl ArcMutex { - pub fn new(keeper: Keeper) -> Self { - Self(Arc::new(Mutex::new(keeper))) - } -} - impl Keeper { fn process_rename_event(&mut self, path: &Utf8Path) { let was_removed = self.inner.remove(path).is_some(); @@ -105,6 +121,15 @@ impl Keeper { } } +#[derive(Clone)] +pub struct ArcMutex(pub Arc>); + +impl ArcMutex { + pub fn new(keeper: Keeper) -> Self { + Self(Arc::new(Mutex::new(keeper))) + } +} + impl notify::EventHandler for ArcMutex { fn handle_event(&mut self, event: notify::Result) { match event { @@ -131,24 +156,20 @@ impl notify::EventHandler for ArcMutex { return; } }; - match kind { - notify::EventKind::Modify(notify::event::ModifyKind::Name( - notify::event::RenameMode::Any, - )) => { + match FsEvent::from(kind) { + FsEvent::Rename => { map.process_rename_event(&path); } - notify::EventKind::Modify(notify::event::ModifyKind::Data( - notify::event::DataChange::Content, - )) => { + FsEvent::Edit => { map.process_edit_event(&path); } - notify::EventKind::Remove(notify::event::RemoveKind::File) => { + FsEvent::Delete => { map.process_removal_event(&path); } - notify::EventKind::Create(notify::event::CreateKind::File) => { + FsEvent::Create => { map.process_create_event(&path); } - event => println!("unhandled watch event: {event:?}"), + FsEvent::Unhandled(event) => println!("unhandled watch event: {event:?}"), } } Err(e) => println!("watch error: {e:?}"), @@ -163,6 +184,8 @@ mod test { use camino::Utf8PathBuf; use notify::{EventHandler, RecursiveMode, Watcher}; + use crate::frontmatter_file::keeper::FsEvent; + use super::{ArcMutex, Keeper}; struct TestFile { @@ -219,7 +242,7 @@ mod test { match event { Ok(event) => { keeper_mut.handle_event(Ok(event.clone())); - tx_clone.send(Ok(event)).unwrap(); + tx_clone.send(Ok(FsEvent::from(event.kind))).unwrap(); } Err(err) => { keeper_mut.handle_event(Err(err)); @@ -242,30 +265,16 @@ mod test { test_file.generate().unwrap(); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Create(notify::event::CreateKind::File), - event.kind, - "Expecting a file create event" - ); + pretty_assertions::assert_eq!(FsEvent::Create, event); let first_line = "Just call me Mark!\n"; test_file.write(first_line).unwrap(); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Create(notify::event::CreateKind::File), - event.kind, - "Expecting a file create event" - ); + pretty_assertions::assert_eq!(FsEvent::Create, event); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Modify(notify::event::ModifyKind::Data( - notify::event::DataChange::Content - )), - event.kind, - "Expecting a content modification event" - ); + pretty_assertions::assert_eq!(FsEvent::Edit, event); { let keeper = keeper.0.as_ref().lock().unwrap(); @@ -280,20 +289,10 @@ mod test { test_file.write(second_line).unwrap(); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Create(notify::event::CreateKind::File), - event.kind, - "Expecting a create event" - ); + pretty_assertions::assert_eq!(FsEvent::Create, event); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Modify(notify::event::ModifyKind::Data( - notify::event::DataChange::Content - )), - event.kind, - "Expecting a content modification event" - ); + pretty_assertions::assert_eq!(FsEvent::Edit, event); { let keeper = keeper.0.as_ref().lock().unwrap(); @@ -307,18 +306,10 @@ mod test { test_file.delete().unwrap(); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Create(notify::event::CreateKind::File), - event.kind, - "Expecting a create event" - ); + pretty_assertions::assert_eq!(FsEvent::Create, event); let event = rx.recv().unwrap().unwrap(); - pretty_assertions::assert_eq!( - notify::EventKind::Remove(notify::event::RemoveKind::File), - event.kind, - "Expecting a file delete event" - ); + pretty_assertions::assert_eq!(FsEvent::Delete, event); { let keeper = keeper.0.as_ref().lock().unwrap(); From 4830865b3ba00863629321f43200fa8ff126cf62 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:11:24 +1300 Subject: [PATCH 4/7] feat: completely ignore certain events --- src/frontmatter_file/keeper.rs | 48 ++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/frontmatter_file/keeper.rs b/src/frontmatter_file/keeper.rs index df01404..7a79c74 100644 --- a/src/frontmatter_file/keeper.rs +++ b/src/frontmatter_file/keeper.rs @@ -16,19 +16,22 @@ enum FsEvent { Edit, Create, Delete, + Ignored, Unhandled(notify::EventKind), } impl From for FsEvent { fn from(event_kind: notify::EventKind) -> Self { use notify::event::{ - CreateKind, DataChange, EventKind, ModifyKind, RemoveKind, RenameMode, + AccessKind, AccessMode, CreateKind, DataChange, EventKind, ModifyKind, RemoveKind, + RenameMode, }; match event_kind { EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => Self::Rename, EventKind::Modify(ModifyKind::Data(DataChange::Content)) => Self::Edit, EventKind::Remove(RemoveKind::File) => Self::Delete, EventKind::Create(CreateKind::File) => Self::Create, + EventKind::Access(AccessKind::Close(AccessMode::Write)) => Self::Ignored, unhandled => Self::Unhandled(unhandled), } } @@ -169,6 +172,7 @@ impl notify::EventHandler for ArcMutex { FsEvent::Create => { map.process_create_event(&path); } + FsEvent::Ignored => (), FsEvent::Unhandled(event) => println!("unhandled watch event: {event:?}"), } } @@ -264,16 +268,28 @@ mod test { test_file.generate().unwrap(); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Create, event); let first_line = "Just call me Mark!\n"; test_file.write(first_line).unwrap(); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Create, event); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Edit, event); { @@ -288,10 +304,18 @@ mod test { let second_line = "I'm a markdown file!\n"; test_file.write(second_line).unwrap(); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Create, event); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Edit, event); { @@ -305,10 +329,18 @@ mod test { test_file.delete().unwrap(); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Create, event); - let event = rx.recv().unwrap().unwrap(); + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); pretty_assertions::assert_eq!(FsEvent::Delete, event); { From 1d322fac8dd1a6c2356962d83d849a1130caee3a Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:14:06 +1300 Subject: [PATCH 5/7] feat: Modify(Data(Any)) -> Edit --- src/frontmatter_file/keeper.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/frontmatter_file/keeper.rs b/src/frontmatter_file/keeper.rs index 7a79c74..5f5f764 100644 --- a/src/frontmatter_file/keeper.rs +++ b/src/frontmatter_file/keeper.rs @@ -28,7 +28,9 @@ impl From for FsEvent { }; match event_kind { EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => Self::Rename, - EventKind::Modify(ModifyKind::Data(DataChange::Content)) => Self::Edit, + EventKind::Modify(ModifyKind::Data(DataChange::Content | DataChange::Any)) => { + Self::Edit + } EventKind::Remove(RemoveKind::File) => Self::Delete, EventKind::Create(CreateKind::File) => Self::Create, EventKind::Access(AccessKind::Close(AccessMode::Write)) => Self::Ignored, From 4a917324be0260380884f093a9795e6f6955d90a Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:25:08 +1300 Subject: [PATCH 6/7] ci: try to fix caching --- .github/workflows/rust.yml | 39 +++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2287ca0..414cecd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,10 +8,42 @@ on: env: CARGO_TERM_COLOR: always - RUST_FLAGS: "-Dwarnings" + RUSTFLAGS: "-Dwarnings" jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + - name: Cache + uses: actions/cache@v3.2.6 + with: + path: | + ~/.cargo + target + key: build-${{ runner.os }} + restore-keys: | + build-${{ runner.os }} + - run: cargo build + lint: + needs: build + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + - name: Cache + uses: actions/cache@v3.2.6 + with: + path: | + ~/.cargo + target + key: build-${{ runner.os }} + restore-keys: | + build-${{ runner.os }} + - run: cargo clippy --all-targets test: + needs: build runs-on: ubuntu-20.04 steps: @@ -25,7 +57,4 @@ jobs: key: build-${{ runner.os }} restore-keys: | build-${{ runner.os }} - - name: Lint - run: cargo clippy --all-targets - - name: Test - run: cargo test + - run: cargo test From dea96fbc6a79b25653f67a5f24d79fb310e364c2 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:30:11 +1300 Subject: [PATCH 7/7] test: always expect create events on mac I'm assuming this' just a quirk of macos... --- src/frontmatter_file/keeper.rs | 45 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/frontmatter_file/keeper.rs b/src/frontmatter_file/keeper.rs index 5f5f764..41ab050 100644 --- a/src/frontmatter_file/keeper.rs +++ b/src/frontmatter_file/keeper.rs @@ -280,12 +280,15 @@ mod test { let first_line = "Just call me Mark!\n"; test_file.write(first_line).unwrap(); - let event = rx - .iter() - .find(|event| !matches!(event, Ok(FsEvent::Ignored))) - .unwrap() - .unwrap(); - pretty_assertions::assert_eq!(FsEvent::Create, event); + #[cfg(target_os = "macos")] + { + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::Create, event); + } let event = rx .iter() @@ -306,12 +309,15 @@ mod test { let second_line = "I'm a markdown file!\n"; test_file.write(second_line).unwrap(); - let event = rx - .iter() - .find(|event| !matches!(event, Ok(FsEvent::Ignored))) - .unwrap() - .unwrap(); - pretty_assertions::assert_eq!(FsEvent::Create, event); + #[cfg(target_os = "macos")] + { + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::Create, event); + } let event = rx .iter() @@ -331,12 +337,15 @@ mod test { test_file.delete().unwrap(); - let event = rx - .iter() - .find(|event| !matches!(event, Ok(FsEvent::Ignored))) - .unwrap() - .unwrap(); - pretty_assertions::assert_eq!(FsEvent::Create, event); + #[cfg(target_os = "macos")] + { + let event = rx + .iter() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::Create, event); + } let event = rx .iter()