Move file watcher to optional module
This commit is contained in:
parent
ce729dbbeb
commit
8bcf7f045d
5 changed files with 131 additions and 119 deletions
|
@ -10,7 +10,7 @@ anyhow = { version = "1.0.97", features = ["backtrace"] }
|
||||||
axum = { version = "0.8.1", features = ["http2", "original-uri", "tracing"] }
|
axum = { version = "0.8.1", features = ["http2", "original-uri", "tracing"] }
|
||||||
cached = "0.55.1"
|
cached = "0.55.1"
|
||||||
config = "0.15.11"
|
config = "0.15.11"
|
||||||
notify = "8.0.0"
|
notify = { version = "8.0.0", optional = true }
|
||||||
pulldown-cmark = "0.13.0"
|
pulldown-cmark = "0.13.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
|
@ -30,3 +30,7 @@ tracing-subscriber = { version = "0.3.19", features = [
|
||||||
"json",
|
"json",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["watch"]
|
||||||
|
watch = ["dep:notify"]
|
||||||
|
|
|
@ -40,14 +40,14 @@ WORKDIR /app
|
||||||
|
|
||||||
COPY --from=planner /app/recipe.json ./
|
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.lock ./
|
||||||
COPY ./Cargo.toml ./
|
COPY ./Cargo.toml ./
|
||||||
|
|
||||||
COPY ./src ./src
|
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
|
## Final image
|
||||||
|
|
125
src/main.rs
125
src/main.rs
|
@ -1,19 +1,9 @@
|
||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use notify::Watcher;
|
use std::sync::Arc;
|
||||||
use std::{
|
use tokio::{net::TcpListener, signal, sync::RwLock};
|
||||||
path::Path,
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use tokio::{
|
|
||||||
net::TcpListener,
|
|
||||||
signal,
|
|
||||||
sync::{RwLock, mpsc::Receiver},
|
|
||||||
};
|
|
||||||
use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer};
|
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;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -25,6 +15,8 @@ mod rendering;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod state;
|
mod state;
|
||||||
mod tag;
|
mod tag;
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
mod watch;
|
||||||
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
@ -72,12 +64,15 @@ fn setup_tracing(cfg: &Settings) {
|
||||||
|
|
||||||
#[instrument(skip(cfg))]
|
#[instrument(skip(cfg))]
|
||||||
async fn init_app(cfg: Settings) -> Result<axum::routing::Router> {
|
async fn init_app(cfg: Settings) -> Result<axum::routing::Router> {
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
let watch = cfg.watch;
|
let watch = cfg.watch;
|
||||||
|
|
||||||
let state = AppState::load(cfg)?;
|
let state = AppState::load(cfg)?;
|
||||||
let state = Arc::new(RwLock::new(state));
|
let state = Arc::new(RwLock::new(state));
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
if watch {
|
if watch {
|
||||||
tokio::spawn(start_file_watcher(state.clone()));
|
tokio::spawn(watch::start_file_watcher(state.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(handlers::routes()
|
Ok(handlers::routes()
|
||||||
|
@ -87,108 +82,6 @@ async fn init_app(cfg: Settings) -> Result<axum::routing::Router> {
|
||||||
.with_state(state))
|
.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() {
|
async fn shutdown_signal() {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
signal::ctrl_c()
|
signal::ctrl_c()
|
||||||
|
|
|
@ -10,6 +10,7 @@ pub struct Settings {
|
||||||
pub logging: String,
|
pub logging: String,
|
||||||
pub log_format: String,
|
pub log_format: String,
|
||||||
pub drafts: bool,
|
pub drafts: bool,
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
pub watch: bool,
|
pub watch: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
114
src/watch.rs
Normal file
114
src/watch.rs
Normal file
|
@ -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");
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue