diff --git a/index.html b/index.html new file mode 100644 index 0000000..6c81801 --- /dev/null +++ b/index.html @@ -0,0 +1,173 @@ + + + + + + + +<!-- t: safe sitename --> + + + + + + + + +

+ +

+ + + + + +

things running on the server

+ + + + + + +

everything else

+ + + + + + +
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)
go to index without or with debugging information
+

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 `