Progress on modularizing watchers
This commit is contained in:
parent
1843f9fac3
commit
2052092796
6 changed files with 163 additions and 84 deletions
42
src/file_watcher.rs
Normal file
42
src/file_watcher.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
use tokio::sync::mpsc::channel;
|
||||||
|
use tracing::{error, info, instrument};
|
||||||
|
|
||||||
|
use crate::Besult;
|
||||||
|
|
||||||
|
pub type FileWatcher = RecommendedWatcher;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::{file_watcher, FileWatcher};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_watcher<P, F>(dir: P, cb: F) -> Besult<FileWatcher>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
F: Fn(Event) -> () + std::marker::Send + 'static,
|
||||||
|
{
|
||||||
|
let (tx, mut rx) = channel(1);
|
||||||
|
let mut watcher = RecommendedWatcher::new(
|
||||||
|
move |res| match res {
|
||||||
|
Ok(e) => futures::executor::block_on(async {
|
||||||
|
tx.send(e).await.unwrap();
|
||||||
|
}),
|
||||||
|
Err(e) => error!("Error from file_watcher: {e}"),
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Watching directory '{}'", dir.as_ref().display());
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(ev) = rx.recv().await {
|
||||||
|
cb(ev)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?;
|
||||||
|
|
||||||
|
Ok(watcher)
|
||||||
|
}
|
5
src/live_reload.rs
Normal file
5
src/live_reload.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use tower_livereload::LiveReloadLayer;
|
||||||
|
|
||||||
|
pub fn live_reload_layer() -> LiveReloadLayer {
|
||||||
|
LiveReloadLayer::new()
|
||||||
|
}
|
95
src/main.rs
95
src/main.rs
|
@ -3,56 +3,51 @@ use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{http::StatusCode, response::Html, serve, Router};
|
use axum::{http::StatusCode, response::Html, serve, Router};
|
||||||
use minijinja::context;
|
use minijinja::context;
|
||||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
use state::State as AppState;
|
||||||
use std::{path::PathBuf, str::FromStr, sync::OnceLock};
|
|
||||||
use templates::Templates;
|
|
||||||
use tokio::sync::mpsc::channel;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
use tower_livereload::{LiveReloadLayer, Reloader};
|
|
||||||
pub use tracing::{debug, error, event, info, span, warn, Level};
|
pub use tracing::{debug, error, event, info, span, warn, Level};
|
||||||
|
|
||||||
|
use crate::live_reload::live_reload_layer;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Berr(Box<dyn std::error::Error>);
|
struct Berr(Box<dyn std::error::Error>);
|
||||||
type Besult<T> = std::result::Result<T, Berr>;
|
type Besult<T> = std::result::Result<T, Berr>;
|
||||||
|
|
||||||
|
mod file_watcher;
|
||||||
|
mod live_reload;
|
||||||
mod observe;
|
mod observe;
|
||||||
|
mod state;
|
||||||
|
mod static_files;
|
||||||
mod tailwind;
|
mod tailwind;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
fn static_file_dir() -> &'static PathBuf {
|
|
||||||
static STATIC_FILE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
|
||||||
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct AppState {
|
|
||||||
templates: Templates,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Besult<()> {
|
async fn main() -> Besult<()> {
|
||||||
// load configuration?
|
// load configuration?
|
||||||
let _setup_logging = observe::setup_logging();
|
let _setup_logging = observe::setup_logging();
|
||||||
|
|
||||||
// TODO: reload templates when they change? separate watcher?
|
let state = AppState::try_new().await?;
|
||||||
let templates = Templates::try_load().await?;
|
|
||||||
let mut tt = templates.clone();
|
|
||||||
let templates_watcher = tt.start_watcher();
|
|
||||||
|
|
||||||
// TODO: only start tailwind if in dev mode?
|
// TODO: only start tailwind if in dev mode?
|
||||||
tokio::spawn(async { tailwind::start_watcher() });
|
tokio::spawn(async move { tailwind::start_watcher() });
|
||||||
tokio::spawn(async move { templates_watcher.await });
|
|
||||||
|
|
||||||
// pulling the watcher into main's scope lets it live until the program dies
|
let lr = live_reload_layer();
|
||||||
let (rl_layer, _watcher) = live_reload_layer()?;
|
|
||||||
|
{
|
||||||
|
// TODO: only start watcher for dev mode?
|
||||||
|
let state = state.clone();
|
||||||
|
let lr = lr.reloader();
|
||||||
|
tokio::spawn(async move { state.templates.start_watcher(Some(lr)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
let (static_file_service, _static_file_watcher) = static_files::router(Some(lr.reloader()))?;
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
info!("Listening on {listener:#?}");
|
|
||||||
|
info!("Listening on {listener:?}");
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.nest_service("/static", ServeDir::new(static_file_dir()))
|
.nest_service("/static", static_file_service)
|
||||||
.layer(rl_layer)
|
.with_state(state);
|
||||||
.with_state(AppState { templates });
|
|
||||||
|
|
||||||
Ok(serve(listener, router).await?)
|
Ok(serve(listener, router).await?)
|
||||||
}
|
}
|
||||||
|
@ -66,50 +61,6 @@ async fn index(State(state): State<AppState>) -> Besult<Html<String>> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn live_reload_layer() -> Besult<(LiveReloadLayer, RecommendedWatcher)> {
|
|
||||||
let rl_layer = LiveReloadLayer::new();
|
|
||||||
let rl = rl_layer.reloader();
|
|
||||||
let watcher = static_file_watcher(rl)?;
|
|
||||||
Ok((rl_layer, watcher))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn static_file_watcher(
|
|
||||||
reloader: Reloader,
|
|
||||||
) -> Result<RecommendedWatcher, Box<dyn std::error::Error>> {
|
|
||||||
info!("Creating async watcher...");
|
|
||||||
|
|
||||||
let (tx, mut rx) = channel(1);
|
|
||||||
|
|
||||||
info!("Creating watcher...");
|
|
||||||
// watcher needs to move out of scope to live long enough to watch
|
|
||||||
let mut watcher = RecommendedWatcher::new(
|
|
||||||
move |res| {
|
|
||||||
info!("Res from watcher: {res:#?}");
|
|
||||||
futures::executor::block_on(async {
|
|
||||||
tx.send(res).await.unwrap();
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Config::default(),
|
|
||||||
)?;
|
|
||||||
info!("Created watcher");
|
|
||||||
|
|
||||||
watcher.watch(static_file_dir(), RecursiveMode::Recursive)?;
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(res) = rx.recv().await {
|
|
||||||
match res {
|
|
||||||
Ok(event) => {
|
|
||||||
info!("fs event: {event:#?}");
|
|
||||||
reloader.reload()
|
|
||||||
}
|
|
||||||
Err(e) => println!("watch error: {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(watcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for Berr {
|
impl IntoResponse for Berr {
|
||||||
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
||||||
error!("webserver error: {:?}", self.0);
|
error!("webserver error: {:?}", self.0);
|
||||||
|
|
16
src/state.rs
Normal file
16
src/state.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use crate::templates::Templates;
|
||||||
|
use crate::Besult;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct State {
|
||||||
|
pub templates: Arc<Templates>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub async fn try_new() -> Besult<Self> {
|
||||||
|
let templates = Arc::new(Templates::try_load("src/templates").await?);
|
||||||
|
|
||||||
|
Ok(Self { templates })
|
||||||
|
}
|
||||||
|
}
|
30
src/static_files.rs
Normal file
30
src/static_files.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::file_watcher::prelude::*;
|
||||||
|
|
||||||
|
use crate::Besult;
|
||||||
|
use axum::Router;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use tower_livereload::Reloader;
|
||||||
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
fn static_file_dir() -> &'static PathBuf {
|
||||||
|
static STATIC_FILE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub fn router(reloader: Option<Reloader>) -> Besult<(Router, Option<FileWatcher>)> {
|
||||||
|
let watcher = if let Some(rl) = reloader {
|
||||||
|
Some(file_watcher(static_file_dir(), move |ev| {
|
||||||
|
info!("Static File Watcher Event: {ev:#?}");
|
||||||
|
rl.reload()
|
||||||
|
})?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let router = Router::new().nest_service("/", ServeDir::new(static_file_dir()));
|
||||||
|
Ok((router, watcher))
|
||||||
|
}
|
|
@ -1,36 +1,72 @@
|
||||||
use std::sync::Arc;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use minijinja::Environment;
|
use minijinja::Environment;
|
||||||
|
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{mpsc::channel, Mutex};
|
||||||
use tracing::info;
|
use tower_livereload::Reloader;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::{file_watcher::file_watcher, Besult};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
env: Arc<Mutex<Environment<'static>>>,
|
env: Arc<Mutex<Environment<'static>>>,
|
||||||
|
dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Error = Box<dyn std::error::Error>;
|
pub type Error = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
impl Templates {
|
impl Templates {
|
||||||
pub fn empty() -> Self {
|
pub fn for_dir<P: Into<PathBuf>>(dir: P) -> Self {
|
||||||
let env = Arc::new(Mutex::new(Environment::new()));
|
let env = Arc::new(Mutex::new(Environment::new()));
|
||||||
Self { env }
|
Self {
|
||||||
|
env,
|
||||||
|
dir: dir.into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn try_load() -> Result<Self, Error> {
|
pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self, Error> {
|
||||||
let mut result = Self::empty();
|
let result = Self::for_dir(dir);
|
||||||
result.load_env().await?;
|
result.load_env().await?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_watcher(&mut self) {
|
pub async fn start_watcher(
|
||||||
info!("TODO: Implement template watcher");
|
self: Arc<Self>,
|
||||||
|
reloader: Option<Reloader>,
|
||||||
|
) -> Besult<Option<RecommendedWatcher>> {
|
||||||
|
if let Some(rl) = reloader {
|
||||||
|
Ok(Some(self.watch(rl).await?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_env(&mut self) -> Result<(), Error> {
|
async fn watch(self: Arc<Self>, reloader: Reloader) -> Besult<RecommendedWatcher> {
|
||||||
|
let (tx, mut rx) = channel(1);
|
||||||
|
let watcher = file_watcher(self.dir.clone(), move |ev| {
|
||||||
|
info!("Template File Watcher Event: {ev:#?}");
|
||||||
|
match tx.blocking_send(ev) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => error!("Error sending template file watcher event: {err}"),
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
let us = self.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(_) = rx.recv().await {
|
||||||
|
us.load_env()
|
||||||
|
.await
|
||||||
|
.expect("Failed to reload templates after template changed during runtime");
|
||||||
|
reloader.reload()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_env(&self) -> Result<(), Error> {
|
||||||
info!("Loading templates...");
|
info!("Loading templates...");
|
||||||
for d in walkdir::WalkDir::new("src/templates") {
|
for d in walkdir::WalkDir::new(&self.dir) {
|
||||||
match d {
|
match d {
|
||||||
Ok(d) => {
|
Ok(d) => {
|
||||||
if d.file_type().is_dir() {
|
if d.file_type().is_dir() {
|
||||||
|
@ -42,7 +78,6 @@ impl Templates {
|
||||||
.into_owned();
|
.into_owned();
|
||||||
info!("Loading template {filename:#?} ({d:#?})");
|
info!("Loading template {filename:#?} ({d:#?})");
|
||||||
self.env
|
self.env
|
||||||
.clone()
|
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.add_template_owned(filename, std::fs::read_to_string(d.path())?)?;
|
.add_template_owned(filename, std::fs::read_to_string(d.path())?)?;
|
||||||
|
|
Loading…
Reference in a new issue