diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6c81801
--- /dev/null
+++ b/index.html
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+things running on the server
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ms
+
+
+
+
+
+
+
+
+
+
+
+everything else
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ms
+
+
+
+
+
+
+
+
+
+
+
+tomatenmharksite documentation
+
+
+
+
+
+
+
+
+unconfigured tomatenmharksite server
+please fill out the required values in int.html
+and restart the tomatenmharksite server
+
+
+
diff --git a/src/int.html b/int.html
similarity index 68%
rename from src/int.html
rename to int.html
index fbd2bd6..5a15c43 100644
--- a/src/int.html
+++ b/int.html
@@ -1,4 +1,14 @@
+
+
+
+
+
+
+
+
+
+
@@ -11,6 +21,8 @@
documentation of how the tomatenmharksite server works for people who care or people who keep forgetting (me)
+index
+
links
In the "running" section on the website, the ids may or may not be links.
If a /srv/tomatenmhark-slashinfo/*/index.html
entry exists, the link will point to that info site,
@@ -18,8 +30,22 @@ If only a /srv/tomatenmhark-redirect/*
entry exists, the link will
and if neither exist, there will be no link.
In the "everything else" section, every entry links to its info page, if there is one.
+texts
+Each id has a status text. This can be a file's content or dynamic output, and will be cached for 3 seconds.
+If a status consists of multiple lines, the all but the first one are additional information. In this case,
+the first line will be emphasized and the additional lines will be displayed on hover.
+
+customizing
+
+The contents of the index are specified in a template file, index.html
.
+This file is served as-is, but comments like <!-- t: ... -->
are
+given special meaning.
+See index.html
and int.html
for usage examples.
+
+files
+
/tmp/tomatenmhark-status-*
-For each file /tmp/tomatenmhark-status-*
, an entry will be created on tomatenmhark.org .
+For each file /tmp/tomatenmhark-status-*
, an entry will be created on .
This entry will have the id *
and the file's contents will be shown after that.
/srv/tomatenmhark-dystatus/*
@@ -37,7 +63,7 @@ Each file in /srv/tomatenmhark-redirect/
contains a port number or
If the file contains a port number, /filename/...
will be redirected to samedomain:portnumber/...
.
If the file contains anything else, /filename/...
will be redirected to filecontent/...
.
In the port number case, http
is used. Otherwise, the protocol must be included in the file.
-%DOMAIN%
will be replaced with tomatenmhark.org
or whatever domain was used in the request.
+%DOMAIN%
will be replaced with
or whatever domain was used in the request.
To redirect to port 8000
using https
, use https://%DOMAIN%:8000
diff --git a/src/data.rs b/src/data.rs
index e50eb59..b9fc07f 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -2,22 +2,32 @@ use std::time::Instant;
use tokio::sync::{Mutex, MutexGuard};
-use crate::status::Status;
+use crate::{status::Status, template::Template};
pub struct Data {
+ pub int_html: String,
+ pub globals: Vec,
+ pub index: Template,
pub status: Mutex,
pub status_updated: Mutex<(Instant, bool)>,
}
+#[allow(dead_code)]
impl Data {
- pub fn new_sync() -> Self {
+ pub fn new_sync(int_html: String, index: Template, globals: Vec) -> Self {
Self {
+ int_html,
+ index,
+ globals,
status: Mutex::new(Status::query_sync(false)),
status_updated: Mutex::new((Instant::now(), false)),
}
}
- pub async fn new_async() -> Self {
+ pub async fn new_async(int_html: String, index: Template, globals: Vec) -> Self {
Self {
+ int_html,
+ index,
+ globals,
status: Mutex::new(Status::query_async(false).await),
status_updated: Mutex::new((Instant::now(), false)),
}
diff --git a/src/main.rs b/src/main.rs
index 856ba0b..b058f53 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,7 @@
mod data;
mod redirecter;
mod status;
-
-use std::time::Duration;
+mod template;
use data::Data;
use redirecter::Redirecter;
@@ -12,7 +11,8 @@ use rocket::{
response::content::RawHtml,
routes, State,
};
-use tokio::time::Instant;
+use std::process::exit;
+use template::Template;
#[get("/")]
async fn index(data: &State) -> RawHtml {
@@ -25,161 +25,46 @@ async fn index_dbg(data: &State) -> RawHtml {
}
async fn index_gen(data: &State, dbg: bool) -> RawHtml {
- let mut o = "".to_owned();
- o.push_str(r#" "#);
- o.push_str(r#" "#);
- o.push_str(r#" "#);
- o.push_str("tomatenmhark ");
- o.push_str(
- "
-",
- );
- o.push_str("");
- o.push_str(":) tomatenmhark :) ");
let status = data.status_async(dbg).await;
- let time_queried = Instant::now();
- fn push_elem_to_html(
- id: &str,
- info: bool,
- redirect: bool,
- status: &str,
- additional: Option<&str>,
- dur: Option,
- o: &mut String,
- dbg: bool,
- ) {
- o.push_str("");
- if info {
- o.push_str(r#""#);
- } else if redirect {
- o.push_str(r#" "#);
- } else {
- o.push_str("");
- }
- o.push_str(&html_escape::encode_text(&id));
- if info || redirect {
- o.push_str(" ");
- } else {
- o.push_str("");
- }
- o.push_str(": ");
- if additional.is_some() {
- o.push_str(r#""#);
- }
- o.push_str(&html_escape::encode_text(&status));
- if let Some(additional) = additional {
- o.push_str(r#" "#);
- o.push_str(
- &html_escape::encode_text(additional)
- .replace('\r', "")
- .replace('\n', " "),
- );
- o.push_str("
");
- }
- if dbg {
- if let Some(dur) = dur {
- o.push_str(&format!(" | {}ms", dur.as_millis()));
- }
- }
- o.push_str(" ");
- }
- if !status.0.is_empty() {
- o.push_str("things running on the server ");
- o.push_str("");
- for (id, (info, redirect, status, additional, dur)) in status.0.iter() {
- push_elem_to_html(
- id,
- *info,
- *redirect,
- status,
- additional.as_ref().map(|v| v.as_str()),
- *dur,
- &mut o,
- dbg,
- );
- }
- o.push_str(" ");
- }
- if !status.1.is_empty() {
- o.push_str("everything else ");
- o.push_str("");
- for (id, (info, redirect, status, additional)) in status.1.iter() {
- push_elem_to_html(
- id,
- *info,
- *redirect,
- status,
- additional.as_ref().map(|v| v.as_str()),
- None,
- &mut o,
- dbg,
- );
- }
- o.push_str(" ");
- }
- o.push_str(
- r#"tomatenmharksite documentation "#,
- );
- o.push_str(r#"tomatenmhark.org und subdomains: Mark "#);
- if dbg {
- o.push_str(r#""#);
- let time_pagegen = Instant::now();
- o.push_str(&format!(
- "{}/{}ms querying + {:.2}ms pagegen",
- status
- .0
- .values()
- .filter_map(|v| v.4)
- .sum::()
- .as_millis(),
- status.2.as_millis(),
- (time_pagegen - time_queried).as_micros() as f32 / 1000.0,
- ));
- o.push_str(r#" "#);
- }
- o.push_str(r#"
"#);
- o.push_str("");
- RawHtml(o)
+ return RawHtml(data.index.gen(&status, dbg, data.globals.clone()));
}
#[get("/int")]
-async fn int() -> RawHtml<&'static str> {
- RawHtml(include_str!("int.html"))
+async fn int(data: &State) -> RawHtml<&str> {
+ RawHtml(&data.int_html)
}
#[rocket::launch]
fn rocket() -> _ {
- let data = Data::new_sync();
+ let int_file = match std::fs::read_to_string("int.html") {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("Could not read int.html: {e}");
+ exit(1);
+ }
+ };
+ let index_file = match std::fs::read_to_string("index.html") {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("Could not read index.html: {e}");
+ exit(1);
+ }
+ };
+ let (int_html, variables, globals) = match Template::parse(&int_file, None) {
+ Ok(int_template) => int_template.gen_int(),
+ Err(e) => {
+ eprintln!("Could not parse int.html: {e}");
+ exit(1);
+ }
+ };
+ let index = match Template::parse(&index_file, Some(variables)) {
+ Ok(index_template) => index_template,
+ Err(e) => {
+ eprintln!("Could not parse index.html: {e}");
+ exit(1);
+ }
+ };
+ let data = Data::new_sync(int_html, index, globals);
rocket::build()
.manage(data)
.mount("/", routes![index, index_dbg, int])
diff --git a/src/status.rs b/src/status.rs
index d709143..29d4a5f 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -8,11 +8,12 @@ const REDIRECT: &'static str = "/srv/tomatenmhark-redirect/";
pub struct Status(
pub BTreeMap, Option)>,
pub BTreeMap)>,
- pub Duration,
);
impl Status {
+ pub fn empty() -> Self {
+ Self(Default::default(), Default::default())
+ }
pub fn query_sync(dbg: bool) -> Self {
- let start = Instant::now();
let mut map = BTreeMap::new();
query_status_sync(
|k, e, r, v, dur| {
@@ -75,10 +76,9 @@ impl Status {
}
}
}
- Self(map, rest, start.elapsed())
+ Self(map, rest)
}
pub async fn query_async(dbg: bool) -> Self {
- let start = Instant::now();
let mut map = BTreeMap::new();
query_status_async(
|k, e, r, v, dur| {
@@ -137,7 +137,7 @@ impl Status {
}
}
}
- Self(map, rest, start.elapsed())
+ Self(map, rest)
}
}
diff --git a/src/template.rs b/src/template.rs
new file mode 100644
index 0000000..5c2583d
--- /dev/null
+++ b/src/template.rs
@@ -0,0 +1,378 @@
+use std::{collections::HashMap, fmt::Display};
+
+use crate::status::Status;
+
+pub struct Template(Templates, TemplateInfo);
+
+pub struct Templates(Vec);
+
+pub enum TemplateType {
+ Html(String),
+ Raw(usize),
+ Safe(usize),
+ Set(usize, String),
+ Concat(usize, usize),
+ If(usize, Templates, Option),
+ For(Vec, Templates),
+}
+
+pub enum TemplateLoop {
+ Running,
+ Static,
+ Lines(usize),
+}
+
+const TEMPLATE_FALSE: (usize, &str) = (0, "false");
+const TEMPLATE_TRUE: (usize, &str) = (1, "true");
+const TEMPLATE_DEBUG: (usize, &str) = (2, "debug");
+const TEMPLATE_RUNNING: (usize, &str) = (3, "running");
+const TEMPLATE_STATIC: (usize, &str) = (4, "static");
+const TEMPLATE_LOOP_ELEM_ID: (usize, &str) = (5, "id");
+const TEMPLATE_LOOP_ELEM_STATUS: (usize, &str) = (6, "status");
+const TEMPLATE_LOOP_ELEM_ADDITIONAL: (usize, &str) = (7, "additional");
+const TEMPLATE_LOOP_ELEM_INFO: (usize, &str) = (8, "info");
+const TEMPLATE_LOOP_ELEM_REDIRECT: (usize, &str) = (9, "redirect");
+const TEMPLATE_LOOP_ELEM_DURATION: (usize, &str) = (10, "duration");
+const TEMPLATE_LINE: (usize, &str) = (11, "line");
+const TEMPLATE_DEFAULT_VARS: [(usize, &str); 12] = [
+ TEMPLATE_FALSE,
+ TEMPLATE_TRUE,
+ TEMPLATE_DEBUG,
+ TEMPLATE_RUNNING,
+ TEMPLATE_STATIC,
+ TEMPLATE_LOOP_ELEM_ID,
+ TEMPLATE_LOOP_ELEM_STATUS,
+ TEMPLATE_LOOP_ELEM_ADDITIONAL,
+ TEMPLATE_LOOP_ELEM_INFO,
+ TEMPLATE_LOOP_ELEM_REDIRECT,
+ TEMPLATE_LOOP_ELEM_DURATION,
+ TEMPLATE_LINE,
+];
+
+impl Template {
+ pub fn gen_int(self) -> (String, HashMap, Vec) {
+ let mut vars = Vec::with_capacity(self.1.variables.len());
+ // make space for all variables
+ vars.resize_with(self.1.variables.len(), String::new);
+ vars[TEMPLATE_TRUE.0] = "true".to_owned();
+
+ // generate html based on the template
+ let mut out = String::new();
+ self.0.gen(&Status::empty(), false, &mut vars, &mut out);
+ (out, self.1.variables, vars)
+ }
+ pub fn gen(&self, status: &Status, debug: bool, mut vars: Vec) -> String {
+ // setup variables
+ if self.1.variables.len() > vars.len() {
+ vars.resize_with(self.1.variables.len(), String::new);
+ }
+ vars[TEMPLATE_DEBUG.0] = if debug {
+ "dbg".to_owned()
+ } else {
+ String::new()
+ };
+ vars[TEMPLATE_RUNNING.0] = if status.0.is_empty() {
+ String::new()
+ } else {
+ status.0.len().to_string()
+ };
+ vars[TEMPLATE_STATIC.0] = if status.1.is_empty() {
+ String::new()
+ } else {
+ status.1.len().to_string()
+ };
+
+ // generate html based on the template
+ let mut out = String::new();
+ self.0.gen(status, debug, &mut vars, &mut out);
+ out
+ }
+}
+
+pub struct TemplateInfo {
+ variables: HashMap,
+ last_end_was_else: Option,
+}
+
+const COMMENT_START: &'static str = "";
+impl Template {
+ pub fn parse(
+ mut src: &str,
+ variables: Option>,
+ ) -> Result {
+ let mut info = TemplateInfo {
+ variables: variables.unwrap_or_else(|| {
+ TEMPLATE_DEFAULT_VARS
+ .iter()
+ .map(|(i, name)| ((*name).to_owned(), *i))
+ .collect()
+ }),
+ last_end_was_else: None,
+ };
+ let templates = Templates::parse(&mut src, &mut info)?;
+ if !src.trim().is_empty() {
+ return Err(TemplateParseError::DidNotReachEOF(src.trim().to_owned()));
+ }
+ Ok(Self(templates, info))
+ }
+}
+impl Templates {
+ fn parse(src: &mut &str, info: &mut TemplateInfo) -> Result {
+ let mut out = vec![];
+ 'parsing: loop {
+ let comment_start = src.find(COMMENT_START);
+ let mut pre_comment = comment_start.map(|i| &src[..i]).unwrap_or(src);
+ if comment_start.is_some() {
+ pre_comment = pre_comment
+ .strip_suffix('\n')
+ .unwrap_or(pre_comment)
+ .trim_end_matches('\r');
+ }
+ out.push(TemplateType::Html(pre_comment.to_owned()));
+ *src = &src[pre_comment.len()..];
+
+ if comment_start.is_some() {
+ *src = &src[COMMENT_START.len()..];
+
+ if let Some(comment_end) = src.find(COMMENT_END) {
+ // extract comment content
+ let comment_og = src[..comment_end].trim();
+ *src = &src[comment_end + COMMENT_END.len()..].trim_start_matches('\r');
+ *src = src.strip_prefix('\n').unwrap_or(src);
+ // do template things
+ let comment = comment_og.to_lowercase();
+ let mut comment = comment.split(char::is_whitespace);
+ match comment.next().unwrap_or("") {
+ "//" | "#" => {}
+ "end" => {
+ break 'parsing;
+ }
+ "else" if info.last_end_was_else == Some(false) => {
+ info.last_end_was_else = Some(true);
+ break 'parsing;
+ }
+ "raw" => out.push(TemplateType::Raw(
+ info.var(
+ comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("raw"))?,
+ ),
+ )),
+ "safe" => out.push(TemplateType::Safe(
+ info.var(
+ comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("safe"))?,
+ ),
+ )),
+ "set" => {
+ let var_name = comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("set"))?;
+ let value = comment_og[3..].trim_start()[var_name.len()..].trim_start();
+ out.push(TemplateType::Set(info.var(var_name), value.to_owned()));
+ }
+ "concat" => {
+ out.push(TemplateType::Concat(
+ info.var(
+ comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("concat"))?,
+ ),
+ info.var(
+ comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("concat"))?,
+ ),
+ ));
+ }
+ "if" => {
+ let prev_lewe = info.last_end_was_else;
+ info.last_end_was_else = Some(false);
+ out.push(TemplateType::If(
+ info.var(
+ comment
+ .next()
+ .ok_or(TemplateParseError::MissingVariable("if"))?,
+ ),
+ Templates::parse(src, info)?,
+ if info.last_end_was_else.is_some_and(|v| v) {
+ Some(Templates::parse(src, info)?)
+ } else {
+ None
+ },
+ ));
+ info.last_end_was_else = prev_lewe;
+ }
+ "for" => {
+ let mut sources = Vec::new();
+ while let Some(source_name) = comment.next() {
+ sources.push(match source_name {
+ "running" => TemplateLoop::Running,
+ "static" => TemplateLoop::Static,
+ "line" => {
+ TemplateLoop::Lines(info.var(comment.next().ok_or(
+ TemplateParseError::MissingVariable("for line"),
+ )?))
+ }
+ s => {
+ return Err(TemplateParseError::UnknownForSource(
+ s.to_owned(),
+ ))
+ }
+ });
+ }
+ if sources.is_empty() {
+ return Err(TemplateParseError::UnknownForSource(String::new()));
+ }
+ out.push(TemplateType::For(sources, Self::parse(src, info)?));
+ }
+ unknown => {
+ return Err(TemplateParseError::InvalidTemplateKeyword(
+ unknown.to_owned(),
+ ))
+ }
+ }
+ } else {
+ return Err(TemplateParseError::UnclosedComment);
+ }
+ } else {
+ break 'parsing;
+ }
+ }
+ Ok(Self(out))
+ }
+}
+
+#[derive(Debug)]
+pub enum TemplateParseError {
+ UnclosedComment,
+ InvalidTemplateKeyword(String),
+ UnknownForSource(String),
+ DidNotReachEOF(String),
+ MissingVariable(&'static str),
+}
+impl Display for TemplateParseError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::UnclosedComment => write!(f, "There is an unclosed comment. Make sure all ``"),
+ Self::InvalidTemplateKeyword(kw) => write!(f, "Invalid keyword `{kw}` after `