diff options
| -rw-r--r-- | Cargo.lock | 38 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | justfile | 8 | ||||
| -rw-r--r-- | splorm/Cargo.toml | 5 | ||||
| -rw-r--r-- | splorm/splorm.conf | 5 | ||||
| -rw-r--r-- | splorm/src/config.rs | 21 | ||||
| -rw-r--r-- | splorm/src/connector/gatherer.rs | 177 | ||||
| -rw-r--r-- | splorm/src/connector/mod.rs | 5 | ||||
| -rw-r--r-- | splorm/src/fs/mod.rs | 9 | ||||
| -rw-r--r-- | splorm/src/handler/api.rs | 43 | ||||
| -rw-r--r-- | splorm/src/handler/mod.rs | 11 | ||||
| -rw-r--r-- | splorm/src/handler/templated.rs | 19 | ||||
| -rw-r--r-- | splorm/src/main.rs | 17 | ||||
| -rw-r--r-- | splorm/src/runtime_error.rs | 8 | ||||
| -rw-r--r-- | splorm/src/state.rs | 35 | ||||
| -rw-r--r-- | splorm/src/templated/features.rs | 20 | ||||
| -rw-r--r-- | splorm/src/templated/mod.rs | 26 | ||||
| -rwxr-xr-x | timeparse/src/lib.rs | 6 |
18 files changed, 433 insertions, 25 deletions
diff --git a/Cargo.lock b/Cargo.lock index afe2f68..87bccf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,10 @@ dependencies = [ ] [[package]] +name = "blobby-derive" +version = "0.1.0" + +[[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -271,6 +275,12 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "confindent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea618ded77af626818bde0f0802da7c20d47e38e23e37be40f6f807a76079e82" + +[[package]] +name = "confindent" version = "3.0.0" source = "git+https://github.com/gennyble/confindent?branch=v3.0-writing#cbb0aa4bc55c346f33e41f979e72cd3a95518a75" @@ -474,10 +484,32 @@ dependencies = [ name = "gatherer" version = "0.1.0" dependencies = [ + "camino", + "gatherer-api", "gifed", - "state", + "rusqlite", + "scurvy", "time", "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gatherer-api" +version = "0.1.0" +dependencies = [ + "camino", + "gny", + "tracing", +] + +[[package]] +name = "gatherer-client" +version = "0.1.0" +dependencies = [ + "camino", + "gatherer-api", + "tracing", ] [[package]] @@ -529,6 +561,8 @@ dependencies = [ name = "gny" version = "0.1.0" dependencies = [ + "blobby-derive", + "camino", "rand", "sternum", "time", @@ -1128,6 +1162,7 @@ version = "0.5.0" source = "git+https://git.dreamy.place/crates/scurvy/#832266e7e8c01dbfd236052cd9b740d545ffcffa" dependencies = [ "camino", + "confindent 2.2.1", ] [[package]] @@ -1298,6 +1333,7 @@ dependencies = [ "bempline", "camino", "confindent 3.0.0 (git+https://github.com/gennyble/confindent)", + "gatherer-client", "gny", "markup", "paige", diff --git a/Cargo.toml b/Cargo.toml index 33bde4a..7f0e558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["awake", "awake-auth", "awake-auth-objects", "awake-database", "awake-error", "awake-fs", "paige", "awake-state", "gatherer", "markup", "pushwhatever", "shovel", "splorm", "timeparse", "wikarden", "sessionhash"] +members = ["awake", "awake-auth", "awake-auth-objects", "awake-database", "awake-error", "awake-fs", "paige", "awake-state", "gatherer", "markup", "pushwhatever", "shovel", "splorm", "timeparse", "wikarden", "sessionhash", "gatherer-api", "gatherer-client"] [workspace.dependencies] bempline = { git = "https://github.com/gennyble/bempline" } @@ -12,11 +12,14 @@ time = { version = "0.3.55", features = ["local-offset"] } axum = "0.8.9" gny = { path = "../gny/libgny", features = ["web"] } tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } # Local Crates markup = { path = "markup" } wikarden = { path = "wikarden" } timeparse = { path = "timeparse" } +gatherer-api = { path = "gatherer-api" } +gatherer-client = { path = "gatherer-client" } # Awake subcrates database = { path = "awake-database" } diff --git a/justfile b/justfile index 684ba5f..aec9dd4 100644 --- a/justfile +++ b/justfile @@ -12,12 +12,12 @@ run: graph: get-db cargo run --bin awake --release --bin awake -- graph dev.conf -deploy: - cargo build --bin awake --release --target x86_64-unknown-linux-gnu - rsync -aP target/x86_64-unknown-linux-gnu/release/awake {{ prod }}:{{ prod_bin }} - shovel: cargo run --bin shovel -- c=dev.conf splorm: cargo run --bin splorm -- c=splorm/splorm.conf + +deploy NAME: + cargo build --bin {{ NAME }} --release --target x86_64-unknown-linux-gnu + rsync -aP target/x86_64-unknown-linux-gnu/release/{{ NAME }} {{ prod }}: diff --git a/splorm/Cargo.toml b/splorm/Cargo.toml index 6b4d471..349b541 100644 --- a/splorm/Cargo.toml +++ b/splorm/Cargo.toml @@ -14,13 +14,14 @@ axum.workspace = true camino.workspace = true # logging -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-subscriber.workspace = true tracing.workspace = true confindent.workspace = true bempline.workspace = true gny = { workspace = true, features = ["sternum"] } -scurvy = { git = "https://git.dreamy.place/crates/scurvy/", version = "0.5.0", features = ["camino"] } +scurvy.workspace = true +gatherer-client.workspace = true # subcrates paige.workspace = true diff --git a/splorm/splorm.conf b/splorm/splorm.conf index c9a5f5d..7c31c9d 100644 --- a/splorm/splorm.conf +++ b/splorm/splorm.conf @@ -4,4 +4,7 @@ Network Serving /Users/gen/src/inf/served Hostname dreamy.place Templates /Users/gen/src/inf/templates - Default minimal + Default post + +Connectors + Gatherer /var/run/gatherer/socket diff --git a/splorm/src/config.rs b/splorm/src/config.rs index c3b5d49..a6cc81d 100644 --- a/splorm/src/config.rs +++ b/splorm/src/config.rs @@ -49,6 +49,9 @@ impl Config { let templates: Utf8PathBuf = templates_sec.parse().unwrap(); let default_template = templates_sec.child_owned("Default").unwrap(); + let connectors_sec = confi.child("Connectors").unwrap(); + let gatherer_path: Utf8PathBuf = connectors_sec.child_parse("Gatherer").unwrap(); + Self { port, settings: Settings { @@ -57,6 +60,7 @@ impl Config { hostname, webroot, + connectors: Connectors { gatherer_path }, }, } } @@ -83,6 +87,8 @@ pub struct Settings { default_template: String, template_dir: Utf8PathBuf, + + connectors: Connectors, } impl Settings { @@ -111,6 +117,21 @@ impl Settings { .system_path() .to_owned() } + + pub fn connectors(&self) -> &Connectors { + &self.connectors + } +} + +#[derive(Clone, Debug)] +pub struct Connectors { + gatherer_path: Utf8PathBuf, +} + +impl Connectors { + pub fn gatherer_path(&self) -> &Utf8Path { + &self.gatherer_path + } } pub fn create_tracing_subscriber() { diff --git a/splorm/src/connector/gatherer.rs b/splorm/src/connector/gatherer.rs new file mode 100644 index 0000000..bb69196 --- /dev/null +++ b/splorm/src/connector/gatherer.rs @@ -0,0 +1,177 @@ +use std::{ops::DerefMut, sync::Arc, thread::JoinHandle}; + +use camino::{Utf8Path, Utf8PathBuf}; +use gatherer_client::{Client, Connection, Error as GathererError, GeneratedGraphs, GraphBounds}; +use timeparse::{Duration, OffsetDateTime}; +use tokio::sync::{ + Mutex, + mpsc::{Receiver, Sender}, +}; + +struct CacheItem<T: Clone> { + item: Mutex<Option<T>>, + time: Mutex<OffsetDateTime>, + timeout: Duration, +} + +impl<T: Clone> CacheItem<T> { + pub fn new(timeout: Duration) -> Self { + Self { + item: Mutex::new(None), + time: Mutex::new(timeparse::now_utc()), + timeout, + } + } + + pub fn new_with_item(item: T, timeout: Duration) -> Self { + Self { + item: Mutex::new(Some(item)), + time: Mutex::new(timeparse::now_utc()), + timeout, + } + } + + pub async fn get(&self) -> Option<T> { + if self.duration_since_cache().await > self.timeout { + let _ = self.item.lock().await.take(); + None + } else { + let item = self.item.lock().await.clone(); + item + } + } + + pub async fn store(&self, item: T) { + let mut item_lock = self.item.lock().await; + let mut time_lock = self.time.lock().await; + + *item_lock.deref_mut() = Some(item); + *time_lock.deref_mut() = timeparse::now_utc(); + } + + pub async fn duration_since_cache(&self) -> Duration { + let time_lock = self.time.lock().await; + *time_lock - timeparse::now_utc() + } +} + +pub struct GathererThread { + cmd_tx: Sender<GathererCommand>, + data_rx: Receiver<GathererData>, + handle: JoinHandle<()>, + + regular_graph_cache: Arc<CacheItem<GeneratedGraphs>>, +} + +impl GathererThread { + pub fn spawn(socket_path: &Utf8Path) -> Result<Self, GathererError> { + let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel(8); + let (data_tx, data_rx) = tokio::sync::mpsc::channel(8); + let client = Client::new(socket_path.to_owned()); + let connection = client.connect()?; + + let handle = std::thread::spawn(|| handler(connection, cmd_rx, data_tx)); + + Ok(Self { + cmd_tx, + data_rx, + handle, + + regular_graph_cache: Arc::new(CacheItem::new(Duration::minutes(5))), + }) + } + + pub fn data_rx(&mut self) -> &mut Receiver<GathererData> { + &mut self.data_rx + } + + pub async fn get_messsage(&mut self) -> GathererData { + match self.data_rx.recv().await { + None => panic!("gatherer connector channel closed"), + Some(data) => data, + } + } + + pub fn cmd_tx(&mut self) -> Sender<GathererCommand> { + self.cmd_tx.clone() + } + + pub async fn regular_graphs(&mut self) -> GeneratedGraphs { + if let Some(graphs) = self.regular_graph_cache.get().await { + graphs + } else { + self.cmd_tx + .send(GathererCommand::RegularGraphs) + .await + .unwrap(); + + match self.get_messsage().await { + GathererData::Graphs(graphs) => { + self.regular_graph_cache.store(graphs.clone()).await; + graphs + } + _ => panic!(), + } + } + } +} + +pub enum GathererCommand { + GetBounds, + GenerateGraphs(Utf8PathBuf), + RegularGraphs, +} + +pub enum GathererData { + Bounds(GraphBounds), + Graphs(GeneratedGraphs), + Error(gatherer_client::Error), +} + +fn handler( + mut conn: Connection, + mut cmd_rx: Receiver<GathererCommand>, + data_tx: Sender<GathererData>, +) { + macro_rules! unwrap_or_senderr { + ($operation:expr) => { + match $operation { + Err(err) => { + data_tx.blocking_send(GathererData::Error(err)).unwrap(); + continue; + } + Ok(ok) => ok, + } + }; + } + + loop { + while let Some(cmd) = cmd_rx.blocking_recv() { + match cmd { + GathererCommand::GetBounds => { + let bounds = unwrap_or_senderr!(conn.graph_bounds()); + data_tx.blocking_send(GathererData::Bounds(bounds)).unwrap(); + } + GathererCommand::GenerateGraphs(path) => { + let graphs = unwrap_or_senderr!(conn.generate_graphs(path)); + data_tx.blocking_send(GathererData::Graphs(graphs)).unwrap(); + } + GathererCommand::RegularGraphs => { + let graphs = unwrap_or_senderr!(conn.regular_graphs()); + data_tx.blocking_send(GathererData::Graphs(graphs)).unwrap(); + } + } + } + } +} + +/* +Currently there is an encoding issue in blobby, it seems, that is causing the +message from gatherer to be corrupted, and we're getting a panic or just incorrect +paths during decoding. + +ALSO: +we want to make this connector have a fallible conneciton. It does not need to +always be connected, and it will keep trying to connect/try to reconnect if +it disconnects. +*/ diff --git a/splorm/src/connector/mod.rs b/splorm/src/connector/mod.rs new file mode 100644 index 0000000..a2fbc85 --- /dev/null +++ b/splorm/src/connector/mod.rs @@ -0,0 +1,5 @@ +//! Connectors to external things + +mod gatherer; + +pub use gatherer::{GathererCommand, GathererData, GathererThread}; diff --git a/splorm/src/fs/mod.rs b/splorm/src/fs/mod.rs index 032aaed..8450db7 100644 --- a/splorm/src/fs/mod.rs +++ b/splorm/src/fs/mod.rs @@ -1,5 +1,6 @@ mod request_path; +use camino::Utf8PathBuf; pub use request_path::{Mime, Mimetype, RequestPath, SafePath, SafePathBuf}; use tokio::fs::File; @@ -13,8 +14,12 @@ pub async fn open_file<'p>(path: SafePath<'p>) -> Result<File, RuntimeError> { pub async fn read_file<'p>(path: SafePath<'p>) -> Result<Vec<u8>, RuntimeError> { let system_path = *path; - match tokio::fs::read(system_path).await { - Err(err) => Err(RuntimeError::file_error(err, system_path.to_path_buf())), + unsafe_read_file(system_path.to_owned()).await +} + +pub async fn unsafe_read_file(path: Utf8PathBuf) -> Result<Vec<u8>, RuntimeError> { + match tokio::fs::read(&path).await { + Err(err) => Err(RuntimeError::file_error(err, path)), Ok(vec) => Ok(vec), } } diff --git a/splorm/src/handler/api.rs b/splorm/src/handler/api.rs new file mode 100644 index 0000000..f360b87 --- /dev/null +++ b/splorm/src/handler/api.rs @@ -0,0 +1,43 @@ +use axum::{ + Router, + body::Body, + extract::{Path, State}, + response::Response, + routing::get, +}; +use sessionhash::RequestContext; + +use crate::{RuntimeError, fs, state::SplormState}; + +pub fn routes() -> Router<SplormState> { + Router::new().route("/stats/now/{name}", get(stats)) +} + +async fn stats( + Path(name): Path<String>, + State(state): State<SplormState>, + ctx: RequestContext, +) -> Result<Response, RuntimeError> { + tracing::trace!("{ctx} GET /api/stats/{name}"); + + let graphs = { + let mut connector = state.gatherer_thread().await; + connector.regular_graphs().await + }; + + let file_path = match name.as_str() { + "current_hostmeminfo.gif" => graphs.meminfo, + "current_hostnetinfo.gif" => graphs.meminfo, + "current_hostcpuinfo.gif" => graphs.cpuinfo, + _ => return Err(RuntimeError::generic_404()), + }; + + tracing::trace!("graph {name} system_path={file_path}"); + let file = fs::unsafe_read_file(file_path).await?; + + Response::builder() + .header("content-type", "image/gif") + .status(200) + .body(Body::from(file)) + .map_err(|_| RuntimeError::Unknown) +} diff --git a/splorm/src/handler/mod.rs b/splorm/src/handler/mod.rs index 95c7e00..edf86f9 100644 --- a/splorm/src/handler/mod.rs +++ b/splorm/src/handler/mod.rs @@ -3,16 +3,17 @@ use std::sync::Arc; use axum::{ Extension, RequestExt, body::Body, - extract::{Path, Request}, + extract::{Path, Request, State}, middleware::Next, response::Response, }; use sessionhash::RequestContext; +pub mod api; mod staticfile; mod templated; -use crate::{RuntimeError, config::Settings, fs::RequestPath}; +use crate::{RuntimeError, config::Settings, fs::RequestPath, state::SplormState}; /// Log runtime errors using a middleware layer. Very much based on the [axum /// error_handling example][axum_errorhandling] @@ -32,16 +33,18 @@ pub async fn log_errors(mut request: Request, next: Next) -> Response { /// GET / pub async fn index( + State(state): State<SplormState>, Extension(settings): Extension<Settings>, ctx: RequestContext, ) -> Result<Response, RuntimeError> { tracing::info!("{ctx} GET /"); let rqp = RequestPath::verify_safe(settings.root(), "home.html")?; - templated::send_file(rqp, &settings).await + templated::send_file(rqp, state, &settings).await } /// GET /{*path} pub async fn static_path( + State(state): State<SplormState>, Extension(settings): Extension<Settings>, Path(path): Path<String>, ctx: RequestContext, @@ -55,7 +58,7 @@ pub async fn static_path( if rqp.guess_mime().as_ref() == "text/html" { tracing::info!("{ctx} GET /{path}"); - templated::send_file(rqp, &settings).await + templated::send_file(rqp, state, &settings).await } else { staticfile::send_or_stream_file(rqp).await } diff --git a/splorm/src/handler/templated.rs b/splorm/src/handler/templated.rs index 4a5fa62..dee93ba 100644 --- a/splorm/src/handler/templated.rs +++ b/splorm/src/handler/templated.rs @@ -8,15 +8,25 @@ use crate::{ RuntimeError, config::Settings, fs::{self, RequestPath, SafePathBuf}, + state::SplormState, templated::TemplateVariables, }; -pub async fn send_file(path: RequestPath, settings: &Settings) -> Result<Response, RuntimeError> { +pub async fn send_file( + path: RequestPath, + state: SplormState, + settings: &Settings, +) -> Result<Response, RuntimeError> { let safe = path.system_path().to_owned(); let string = fs::read_file_to_string(path.system_path()).await?; let template = Templated::from_str(&string) .map_err(|_| RuntimeError::not_a_template(path.system_path().to_path_buf()))?; let tvars = TemplateVariables::build(safe.as_path(), &template.frontmatter); + tracing::trace!( + "page {} has features {:?}", + path.web_path(), + tvars.features() + ); let pt_path = match tvars.template() { Some(name) => { @@ -40,7 +50,12 @@ pub async fn send_file(path: RequestPath, settings: &Settings) -> Result<Respons tvars.apply_pathnav(&mut page_template, &path); tvars.apply_opengraph(&mut page_template, &settings, &path); - let content = tvars.apply_markup(&mut page_template, template.content); + let mut content = template.content; + if !tvars.features().is_empty() { + content = tvars.apply_features(content, &state).await; + } + + let content = tvars.apply_markup(&mut page_template, content); page_template.set("main", content); Response::builder() diff --git a/splorm/src/main.rs b/splorm/src/main.rs index 2bd2e5a..25d6cd3 100644 --- a/splorm/src/main.rs +++ b/splorm/src/main.rs @@ -1,12 +1,16 @@ +use std::sync::Arc; + use axum::{Extension, Router, middleware::from_fn, routing::get}; use tokio::{net::TcpListener, runtime::Builder}; use crate::{ config::{CommandLine, Config}, - state::SplormState, + connector::GathererThread, + state::{ConnectorsState, SplormState}, }; mod config; +pub mod connector; pub mod fs; mod handler; mod runtime_error; @@ -22,23 +26,28 @@ fn main() { tracing::trace!(?cli); tracing::trace!(?conf); + let gatherer = GathererThread::spawn(conf.settings().connectors().gatherer_path()).unwrap(); + + let state = SplormState::new(ConnectorsState::new(gatherer)); + // Create and spawn the tokio multithreaded runtime. Execution stays // here until async_main() returns. let rt = Builder::new_multi_thread().enable_all().build().unwrap(); - rt.block_on(async_main(conf)) + rt.block_on(async_main(conf, state)) } -async fn async_main(conf: Config) { +async fn async_main(conf: Config, state: SplormState) { // Setup the routing let bare_routes = Router::new() .route("/", get(handler::index)) + .nest("/api", handler::api::routes()) .route("/{*path}", get(handler::static_path)); // Apply Extensions and state let routes = bare_routes .layer(from_fn(handler::log_errors)) .layer(Extension(conf.settings().clone())) - .with_state(SplormState {}); + .with_state(state); // Create our listener and start axum serving and routing let addr = conf.listen_address(); diff --git a/splorm/src/runtime_error.rs b/splorm/src/runtime_error.rs index 981853d..225c2d4 100644 --- a/splorm/src/runtime_error.rs +++ b/splorm/src/runtime_error.rs @@ -43,6 +43,7 @@ pub enum RuntimeError { source: Option<bempline::ParseError>, system_path: Utf8PathBuf, }, + Generic404, Unknown, } @@ -93,6 +94,10 @@ impl RuntimeError { } } + pub fn generic_404() -> Self { + RuntimeError::Generic404 + } + pub fn unknown() -> Self { RuntimeError::Unknown } @@ -106,6 +111,7 @@ impl RuntimeError { FileErrorReason::Unknown => 500, }, Self::TemplateError { .. } => 500, + Self::Generic404 => 404, Self::Unknown => 500, } } @@ -137,6 +143,7 @@ impl fmt::Display for RuntimeError { Some(pe) => write!(f, "template error: {pe}. system={system_path}"), None => write!(f, "template error. reason={reason}, system={system_path}"), }, + RuntimeError::Generic404 => write!(f, "generic 404"), RuntimeError::Unknown => write!(f, "unknown error!"), } } @@ -166,6 +173,7 @@ impl<'re> fmt::Display for PresentableRuntimeError<'re> { FileErrorReason::Unknown => estr!(500), }, RuntimeError::TemplateError { .. } => estr!(500), + RuntimeError::Generic404 => estr!(404), RuntimeError::Unknown => estr!(500), } } diff --git a/splorm/src/state.rs b/splorm/src/state.rs index 12ef5d9..59e8629 100644 --- a/splorm/src/state.rs +++ b/splorm/src/state.rs @@ -1,2 +1,33 @@ -#[derive(Clone, Debug)] -pub struct SplormState {} +use std::sync::Arc; + +use tokio::sync::{Mutex, MutexGuard}; + +use crate::connector::GathererThread; + +#[derive(Clone)] +pub struct SplormState { + connectors: ConnectorsState, +} + +impl SplormState { + pub fn new(connectors: ConnectorsState) -> Self { + Self { connectors } + } + + pub async fn gatherer_thread<'s>(&'s self) -> MutexGuard<'s, GathererThread> { + self.connectors.gatherer.lock().await + } +} + +#[derive(Clone)] +pub struct ConnectorsState { + gatherer: Arc<Mutex<GathererThread>>, +} + +impl ConnectorsState { + pub fn new(gatherer: GathererThread) -> Self { + Self { + gatherer: Arc::new(Mutex::new(gatherer)), + } + } +} diff --git a/splorm/src/templated/features.rs b/splorm/src/templated/features.rs new file mode 100644 index 0000000..328b955 --- /dev/null +++ b/splorm/src/templated/features.rs @@ -0,0 +1,20 @@ +use bempline::Document; +use gny::sternum::Sternum; + +use crate::state::SplormState; + +#[derive(Sternum, Debug, PartialEq)] +pub enum Feature { + SystemStats, +} + +pub async fn apply_system_stats(doc: &mut Document, state: &SplormState) { + let bounds = { + let mut lock = state.gatherer_thread().await; + lock.regular_graphs().await.bounds + }; + doc.set("stats.mem.total", bounds.mem_upper / 1000); + doc.set("stats.mem.usage", bounds.mem_current / 1000); + doc.set("stats.net.max_bound", bounds.net_upper); + doc.set("stats.cpu.max_bound", bounds.cpu_upper); +} diff --git a/splorm/src/templated/mod.rs b/splorm/src/templated/mod.rs index 6d649d0..6f15b77 100644 --- a/splorm/src/templated/mod.rs +++ b/splorm/src/templated/mod.rs @@ -1,17 +1,22 @@ use std::str::FromStr; -use bempline::{Document, variables}; +use bempline::{Document, Options, options::IncludeMethod, variables}; use paige::Frontmatter; use timeparse::OffsetDateTime; +mod features; + use crate::{ config::Settings, fs::{RequestPath, SafePath}, + state::SplormState, + templated::features::Feature, }; pub struct TemplateVariables<'fm> { template: Option<&'fm str>, markup: Option<MarkupStyle>, + features: Vec<Feature>, style: Vec<&'fm str>, @@ -32,6 +37,11 @@ impl<'fm> TemplateVariables<'fm> { .get("markup") .map(|s| MarkupStyle::from_str(s).ok()) .flatten(), + features: fm + .get_many("features") + .into_iter() + .filter_map(|s| Feature::from_str(s).ok()) + .collect(), title: fm.get("title").unwrap_or_else(|| filename), style: fm.get_many("style"), description: fm.get("description"), @@ -47,6 +57,10 @@ impl<'fm> TemplateVariables<'fm> { self.template } + pub fn features(&self) -> &[Feature] { + &self.features + } + pub fn apply_styles(&self, doc: &mut Document) { if let Some(styles) = doc.get_pattern("styles") { self.style.iter().for_each(|style| { @@ -120,6 +134,16 @@ impl<'fm> TemplateVariables<'fm> { variables!(doc, og_title, og_url, og_site_name, og_description); } } + + pub async fn apply_features(&self, content: String, state: &SplormState) -> String { + let mut doc = Document::from_str(&content, Options::default()).unwrap(); + for feat in &self.features { + match feat { + Feature::SystemStats => features::apply_system_stats(&mut doc, state).await, + } + } + doc.compile() + } } pub enum MarkupStyle { diff --git a/timeparse/src/lib.rs b/timeparse/src/lib.rs index 952a9d1..fdb7bdd 100755 --- a/timeparse/src/lib.rs +++ b/timeparse/src/lib.rs @@ -5,7 +5,7 @@ use time::{ macros::{format_description, offset, time}, }; -pub use time::OffsetDateTime; +pub use time::{Duration, OffsetDateTime}; pub mod format; #[allow(unused)] @@ -143,6 +143,10 @@ pub fn parse(raw: &str) -> Result<OffsetDateTime, time::error::Parse> { Ok(OffsetDateTime::new_in_offset(date, time, offset)) } +pub fn now_utc() -> OffsetDateTime { + OffsetDateTime::now_utc() +} + pub fn whichever_now() -> OffsetDateTime { OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()) } |
