diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..414cecd --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,60 @@ +name: Rust + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + 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: + - 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 test 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/keeper.rs b/src/frontmatter_file/keeper.rs new file mode 100644 index 0000000..41ab050 --- /dev/null +++ b/src/frontmatter_file/keeper.rs @@ -0,0 +1,363 @@ +use std::{ + collections::{hash_map::Values, HashMap}, + sync::{Arc, Mutex}, +}; + +use camino::{Utf8Path, Utf8PathBuf}; + +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, + Ignored, + Unhandled(notify::EventKind), +} + +impl From for FsEvent { + fn from(event_kind: notify::EventKind) -> Self { + use notify::event::{ + 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 | DataChange::Any)) => { + 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), + } + } +} + +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() + } +} + +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) { + Ok(file) => file, + Err(err) => { + eprintln!("Couldn't load file ({path:?}) after Create event: {err}"); + return; + } + }; + self.inner.insert(path.to_owned(), file); + } + } + + 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; + }; + let new_file = match FrontmatterFile::read_from_path(path) { + Ok(new_file) => new_file, + Err(err) => { + eprintln!("Couldn't load file ({path:?}) after Edit event: {err}"); + return; + } + }; + *file = new_file; + } + + 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: &Utf8Path) { + if self.inner.contains_key(path) { + eprintln!( + "A Create event occurred for a path ({path:?}) but it already exists in memory." + ); + return; + } + let new_file = match FrontmatterFile::read_from_path(path) { + Ok(new_file) => new_file, + Err(err) => { + eprintln!("Couldn't load file ({path:?}) during Create event: {err}"); + return; + } + }; + self.inner.insert(path.to_owned(), new_file); + } +} + +#[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 { + Ok(notify::Event { + kind, + paths, + attrs: _, + }) => { + let path = paths.first().expect("event must have at least one path"); + 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) => { + eprintln!("Failed to lock data map during notify event: {err}"); + return; + } + }; + match FsEvent::from(kind) { + FsEvent::Rename => { + map.process_rename_event(&path); + } + FsEvent::Edit => { + map.process_edit_event(&path); + } + FsEvent::Delete => { + map.process_removal_event(&path); + } + FsEvent::Create => { + map.process_create_event(&path); + } + FsEvent::Ignored => (), + FsEvent::Unhandled(event) => println!("unhandled watch event: {event:?}"), + } + } + Err(e) => println!("watch error: {e:?}"), + } + } +} + +#[cfg(test)] +mod test { + use std::io::Write; + + use camino::Utf8PathBuf; + use notify::{EventHandler, RecursiveMode, Watcher}; + + use crate::frontmatter_file::keeper::FsEvent; + + 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(FsEvent::from(event.kind))).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 + .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(); + + #[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() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::Edit, 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(); + + #[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() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::Edit, 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(); + + #[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() + .find(|event| !matches!(event, Ok(FsEvent::Ignored))) + .unwrap() + .unwrap(); + pretty_assertions::assert_eq!(FsEvent::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()); + } + } +} diff --git a/src/frontmatter_file/map.rs b/src/frontmatter_file/map.rs deleted file mode 100644 index 591a642..0000000 --- a/src/frontmatter_file/map.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -use crate::{fs::path_has_extensions, utf8_filepath::UTF8FilePath}; - -use super::FrontmatterFile; - -pub struct Map { - pub inner: HashMap, -} - -#[derive(Clone)] -pub struct ArcMutex(pub Arc>); - -impl ArcMutex { - pub fn new(map: HashMap) -> Self { - Self(Arc::new(Mutex::new(Map { inner: map }))) - } -} - -impl Map { - fn process_rename_event(&mut self, path: &UTF8FilePath) { - let was_removed = self.inner.remove(path).is_some(); - if !was_removed { - let file = match FrontmatterFile::read_from_path(path) { - Ok(file) => file, - Err(err) => { - eprintln!("Couldn't load file ({path:?}) after Create event: {err}"); - return; - } - }; - self.inner.insert(path.clone(), file); - } - } - - fn process_edit_event(&mut self, path: &UTF8FilePath) { - let Some(file) = self.inner.get_mut(path) else { - eprintln!("Couldn't find ({path:?}) in Edit event."); - return; - }; - let new_file = match FrontmatterFile::read_from_path(path) { - Ok(new_file) => new_file, - Err(err) => { - eprintln!("Couldn't load file ({path:?}) after Edit event: {err}"); - return; - } - }; - *file = new_file; - } - - fn process_removal_event(&mut self, path: &UTF8FilePath) { - 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) { - if self.inner.contains_key(path) { - eprintln!( - "A Create event occurred for a path ({path:?}) but it already exists in memory." - ); - return; - } - let new_file = match FrontmatterFile::read_from_path(path) { - Ok(new_file) => new_file, - Err(err) => { - eprintln!("Couldn't load file ({path:?}) during Create event: {err}"); - return; - } - }; - self.inner.insert(path.clone(), new_file); - } -} - -impl notify::EventHandler for ArcMutex { - fn handle_event(&mut self, event: notify::Result) { - match event { - Ok(notify::Event { - kind, - paths, - 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()) { - Ok(path) => path, - Err(err) => { - eprintln!("Event filepath ({path:?}) was not UTF-8: {err}\n\nNon-UTF-8 paths not supported."); - return; - } - }; - let mut map = match self.0.as_ref().lock() { - Ok(map) => map, - Err(err) => { - eprintln!("Failed to lock data map during notify event: {err}"); - return; - } - }; - match kind { - notify::EventKind::Modify(notify::event::ModifyKind::Name( - notify::event::RenameMode::Any, - )) => { - map.process_rename_event(&path); - } - notify::EventKind::Modify(notify::event::ModifyKind::Data( - notify::event::DataChange::Content, - )) => { - map.process_edit_event(&path); - } - notify::EventKind::Remove(notify::event::RemoveKind::File) => { - map.process_removal_event(&path); - } - notify::EventKind::Create(notify::event::CreateKind::File) => { - map.process_create_event(&path); - } - event => println!("unhandled watch event: {event:?}"), - } - } - Err(e) => println!("watch error: {e:?}"), - } - } -} 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() - // } -}