diff --git a/Cargo.toml b/Cargo.toml index 8f95f1a..12fe2d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ anyhow = { version = "1.0.97", features = ["backtrace"] } axum = { version = "0.8.1", features = ["http2", "original-uri", "tracing"] } cached = "0.55.1" config = "0.15.11" -notify = "8.0.0" +notify = { version = "8.0.0", optional = true } pulldown-cmark = "0.13.0" regex = "1.11.1" serde = "1.0.219" @@ -30,3 +30,7 @@ tracing-subscriber = { version = "0.3.19", features = [ "json", "tracing-log", ] } + +[features] +default = ["watch"] +watch = ["dep:notify"] diff --git a/Containerfile b/Containerfile index 05e5e38..c3323fd 100644 --- a/Containerfile +++ b/Containerfile @@ -40,14 +40,14 @@ WORKDIR /app COPY --from=planner /app/recipe.json ./ -RUN cargo chef cook --target x86_64-unknown-linux-musl --release --recipe-path recipe.json +RUN cargo chef cook --target x86_64-unknown-linux-musl --release --no-default--features --recipe-path recipe.json COPY ./Cargo.lock ./ COPY ./Cargo.toml ./ COPY ./src ./src -RUN cargo build --target x86_64-unknown-linux-musl --release +RUN cargo build --target x86_64-unknown-linux-musl --release --no-default--features #################################################################################################### ## Final image diff --git a/src/main.rs b/src/main.rs index 6087d6e..13c1415 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,9 @@ #![warn(clippy::pedantic)] use anyhow::Result; -use notify::Watcher; -use std::{ - path::Path, - sync::Arc, - time::{Duration, Instant}, -}; -use time::OffsetDateTime; -use tokio::{ - net::TcpListener, - signal, - sync::{RwLock, mpsc::Receiver}, -}; +use std::sync::Arc; +use tokio::{net::TcpListener, signal, sync::RwLock}; use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer}; -use tracing::{debug, error, instrument, level_filters::LevelFilter, log::info, warn}; +use tracing::{instrument, level_filters::LevelFilter, log::info, warn}; use tracing_subscriber::EnvFilter; mod error; @@ -25,6 +15,8 @@ mod rendering; mod settings; mod state; mod tag; +#[cfg(feature = "watch")] +mod watch; use settings::Settings; use state::AppState; @@ -72,12 +64,15 @@ fn setup_tracing(cfg: &Settings) { #[instrument(skip(cfg))] async fn init_app(cfg: Settings) -> Result<axum::routing::Router> { + #[cfg(feature = "watch")] let watch = cfg.watch; + let state = AppState::load(cfg)?; let state = Arc::new(RwLock::new(state)); + #[cfg(feature = "watch")] if watch { - tokio::spawn(start_file_watcher(state.clone())); + tokio::spawn(watch::start_file_watcher(state.clone())); } Ok(handlers::routes() @@ -87,108 +82,6 @@ async fn init_app(cfg: Settings) -> Result<axum::routing::Router> { .with_state(state)) } -async fn start_file_watcher(state: Arc<RwLock<AppState>>) { - fn event_filter(event: ¬ify::Event) -> bool { - event.kind.is_modify() || event.kind.is_remove() - } - - let (page_tx, page_rx) = tokio::sync::mpsc::channel::<notify::Event>(8); - - let mut page_watcher = - notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| { - let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else { - return; - }; - if !event_filter(&event) { - return; - } - _ = page_tx - .blocking_send(event) - .inspect_err(|e| error!("Failed to add watch event to channel: {}", e)); - }) - .expect("create page file watcher"); - - page_watcher - .watch(Path::new("pages/"), notify::RecursiveMode::Recursive) - .expect("add pages dir to watcher"); - - let (template_tx, template_rx) = tokio::sync::mpsc::channel::<notify::Event>(8); - - let mut template_watcher = - notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| { - let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else { - return; - }; - if !event_filter(&event) { - return; - } - _ = template_tx - .blocking_send(event) - .inspect_err(|e| error!("Failed to add watch event to channel: {}", e)); - }) - .expect("create template file watcher"); - - template_watcher - .watch(Path::new("templates/"), notify::RecursiveMode::Recursive) - .expect("add templates dir to watcher"); - - tokio::join!( - page_watch_loop(state.clone(), page_rx), - template_watch_loop(state.clone(), template_rx) - ); -} - -const WATCHER_DEBOUNCE_MILLIS: u64 = 100; - -async fn page_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) { - let mut last_reload = Instant::now(); - debug!("Now watching pages"); - while let Some(_event) = rx.recv().await { - if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) { - continue; - } - - let pages = { - let state = state.read().await; - - info!("Reloading pages"); - let root_path = Path::new("pages/"); - page::load_all(&state, root_path, root_path) - .inspect_err(|err| error!("Error reloading pages: {}", err)) - .ok() - }; - - if let Some(pages) = pages { - let mut state = state.write().await; - state.pages = pages; - state.last_modified = OffsetDateTime::now_utc(); - last_reload = Instant::now(); - } - } - warn!("Page watch loop stopped"); -} - -async fn template_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) { - let mut last_reload = Instant::now(); - debug!("Now watching templates"); - while let Some(_event) = rx.recv().await { - if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) { - continue; - } - - let mut state = state.write().await; - - info!("Reloading templates"); - _ = state - .tera - .full_reload() - .inspect_err(|err| error!("Error reloading templates: {}", err)); - state.last_modified = OffsetDateTime::now_utc(); - last_reload = Instant::now(); - } - warn!("Template watch loop stopped"); -} - async fn shutdown_signal() { let ctrl_c = async { signal::ctrl_c() diff --git a/src/settings.rs b/src/settings.rs index 11b675f..9f30552 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,7 @@ pub struct Settings { pub logging: String, pub log_format: String, pub drafts: bool, + #[cfg(feature = "watch")] pub watch: bool, } diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 0000000..34b78d4 --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,114 @@ +use std::{ + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; + +use notify::Watcher; +use time::OffsetDateTime; +use tokio::sync::{RwLock, mpsc::Receiver}; +use tracing::{debug, error, info, warn}; + +use crate::{page, state::AppState}; + +pub async fn start_file_watcher(state: Arc<RwLock<AppState>>) { + fn event_filter(event: ¬ify::Event) -> bool { + event.kind.is_modify() || event.kind.is_remove() + } + + let (page_tx, page_rx) = tokio::sync::mpsc::channel::<notify::Event>(8); + + let mut page_watcher = + notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| { + let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else { + return; + }; + if !event_filter(&event) { + return; + } + _ = page_tx + .blocking_send(event) + .inspect_err(|e| error!("Failed to add watch event to channel: {}", e)); + }) + .expect("create page file watcher"); + + page_watcher + .watch(Path::new("pages/"), notify::RecursiveMode::Recursive) + .expect("add pages dir to watcher"); + + let (template_tx, template_rx) = tokio::sync::mpsc::channel::<notify::Event>(8); + + let mut template_watcher = + notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| { + let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else { + return; + }; + if !event_filter(&event) { + return; + } + _ = template_tx + .blocking_send(event) + .inspect_err(|e| error!("Failed to add watch event to channel: {}", e)); + }) + .expect("create template file watcher"); + + template_watcher + .watch(Path::new("templates/"), notify::RecursiveMode::Recursive) + .expect("add templates dir to watcher"); + + tokio::join!( + page_watch_loop(state.clone(), page_rx), + template_watch_loop(state.clone(), template_rx) + ); +} + +const WATCHER_DEBOUNCE_MILLIS: u64 = 100; + +async fn page_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) { + let mut last_reload = Instant::now(); + debug!("Now watching pages"); + while let Some(_event) = rx.recv().await { + if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) { + continue; + } + + let pages = { + let state = state.read().await; + + info!("Reloading pages"); + let root_path = Path::new("pages/"); + page::load_all(&state, root_path, root_path) + .inspect_err(|err| error!("Error reloading pages: {}", err)) + .ok() + }; + + if let Some(pages) = pages { + let mut state = state.write().await; + state.pages = pages; + state.last_modified = OffsetDateTime::now_utc(); + last_reload = Instant::now(); + } + } + warn!("Page watch loop stopped"); +} + +async fn template_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) { + let mut last_reload = Instant::now(); + debug!("Now watching templates"); + while let Some(_event) = rx.recv().await { + if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) { + continue; + } + + let mut state = state.write().await; + + info!("Reloading templates"); + _ = state + .tera + .full_reload() + .inspect_err(|err| error!("Error reloading templates: {}", err)); + state.last_modified = OffsetDateTime::now_utc(); + last_reload = Instant::now(); + } + warn!("Template watch loop stopped"); +}