From bf54edf3bb5f28dadb35b4d709e509bc06f45a90 Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 22 Jul 2023 16:06:27 -0500 Subject: [PATCH] Complete overhaul of render/rasterize/parsing/templates/routes subsystems --- src/main.rs | 13 ++- src/parse.rs | 26 ++++++ src/{svg.rs => raster.rs} | 4 +- src/routes.rs | 163 +++++++++++++++++++------------------- src/template.rs | 47 +++++++++++ src/templates/basic.svg | 2 +- 6 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 src/parse.rs rename src/{svg.rs => raster.rs} (97%) create mode 100644 src/template.rs diff --git a/src/main.rs b/src/main.rs index 7a06180..9968658 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,12 +3,14 @@ use std::net::SocketAddr; use axum::{Router, routing::get}; use dotenvy::dotenv; use config::Configuration; -use crate::routes::root_handler; +use crate::routes::{relative_handler, implicit_handler, absolute_handler}; mod config; -mod svg; +mod raster; mod abbr; mod routes; +mod parse; +mod template; #[tokio::main] @@ -26,7 +28,12 @@ async fn main() { .init(); let app = Router::new() - .route("/:path", get(root_handler)); + .route("/:path", get(implicit_handler)) + .route("/rel/:path", get(relative_handler)) + .route("/relative/:path", get(relative_handler)) + .route("/absolute/:path", get(absolute_handler)) + .route("/abs/:path", get(absolute_handler)); + let addr = SocketAddr::from((config.socket_addr(), config.port)); axum::Server::bind(&addr) .serve(app.into_make_service_with_connect_info::()) diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..4f1aae3 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, FixedOffset, Utc}; + +/// Split a path into a tuple of the preceding path and the extension. +/// Can handle paths with multiple dots (period characters). +/// Returns None if there is no extension. +/// Returns None if the preceding path is empty (for example, dotfiles like ".env"). +pub fn split_on_extension(path: &str) -> Option<(&str, &str)> { + let split = path.rsplit_once('.'); + if split.is_none() { return None; } + + // Check that the file is not a dotfile (.env) + if split.unwrap().0.len() == 0 { + return None; + } + + Some(split.unwrap()) +} + +pub fn parse_absolute(raw: String) -> Result<(DateTime, FixedOffset), String> { + let datetime_with_offset = DateTime::parse_from_rfc3339(&raw); + if datetime_with_offset.is_err() { + return Err("Failed to parse datetime".to_string()); + } + + Ok((datetime_with_offset.unwrap().with_timezone(&Utc), *(datetime_with_offset.unwrap().offset()))) +} \ No newline at end of file diff --git a/src/svg.rs b/src/raster.rs similarity index 97% rename from src/svg.rs rename to src/raster.rs index 67c0271..34cc7ad 100644 --- a/src/svg.rs +++ b/src/raster.rs @@ -16,11 +16,11 @@ impl std::fmt::Display for RenderError { } } -pub struct Renderer { +pub struct Rasterizer { font_db: fontdb::Database, } -impl Renderer { +impl Rasterizer { pub fn new() -> Self { let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); diff --git a/src/routes.rs b/src/routes.rs index 058922d..b3b1436 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,116 +1,119 @@ +use std::io::Read; +use std::process::Output; use std::time::SystemTime; use axum::{http::StatusCode, response::IntoResponse}; -use axum::body::Full; +use axum::body::{Bytes, Full}; use axum::extract::{Path}; use axum::http::{header}; use axum::response::Response; -use lazy_static::lazy_static; -use tera::{Context, Tera}; -use timeago::Formatter; +use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, Utc}; -use crate::svg::Renderer; -lazy_static! { - static ref TEMPLATES: Tera = { - let mut _tera = match Tera::new("templates/**/*.svg") { - Ok(t) => { - let names: Vec<&str> = t.get_template_names().collect(); - println!("{} templates found ([{}]).", names.len(), names.join(", ")); - t - }, - Err(e) => { - println!("Parsing error(s): {}", e); - ::std::process::exit(1); +use crate::parse::split_on_extension; +use crate::raster::Rasterizer; +use crate::template::{OutputForm, render_template, RenderContext}; + + +fn parse_path(path: &str) -> (&str, &str) { + split_on_extension(path) + .or_else(|| Some((path, "svg"))) + .unwrap() +} + +enum TimeBannerError { + ParseError(String), + RenderError(String), + RasterizeError(String), +} + +fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), TimeBannerError> { + match extension { + "svg" => Ok(("image/svg+xml", Bytes::from(data))), + "png" => { + let renderer = Rasterizer::new(); + let raw_image = renderer.render(data.into_bytes()); + if raw_image.is_err() { + return Err(TimeBannerError::RasterizeError(raw_image.unwrap_err().message.unwrap_or("Unknown error".to_string()))); } - }; - _tera - }; -} -fn convert_epoch(epoch: u64) -> SystemTime { - SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(epoch) -} - -fn parse_path(path: String) -> Result<(SystemTime, String), String> { - if path.contains(".") { - let split_path = path.split_once(".").unwrap(); - let epoch_int = split_path.0.parse::(); - if epoch_int.is_err() { - return Err("Epoch is not a valid integer.".to_string()); + Ok(("image/x-png", Bytes::from(raw_image.unwrap()))) } - - return Ok((convert_epoch(epoch_int.unwrap()), split_path.1.parse().unwrap())); + _ => Err(TimeBannerError::ParseError(format!("Unsupported extension: {}", extension))) } - - let epoch_int = path.parse::(); - if epoch_int.is_err() { - return Err("Epoch is not a valid integer.".to_string()); - } - - Ok( - (convert_epoch(epoch_int.unwrap()), String::from("svg")) - ) } +pub async fn relative_handler(Path(path): Path) -> impl IntoResponse { + let (raw_time, extension) = parse_path(path.as_str()); +} + +pub async fn absolute_handler(Path(path): Path) -> impl IntoResponse { + let (raw_time, extension) = parse_path(path.as_str()); +} + + // basic handler that responds with a static string -pub async fn root_handler(Path(path): Path) -> impl IntoResponse { - let renderer = Renderer::new(); - let mut context = Context::new(); - let f = Formatter::new(); +pub async fn implicit_handler(Path(path): Path) -> impl IntoResponse { + // Get extension if available + let (raw_time, extension) = parse_path(path.as_str()); - let parse_result = parse_path(path); - if parse_result.is_err() { + // Parse epoch + let parsed_epoch = raw_time.parse::(); + if parsed_epoch.is_err() { return Response::builder() .status(StatusCode::BAD_REQUEST) - .body(Full::from(parse_result.err().unwrap())) + .body(Full::from(format!("Failed to parse epoch :: {}", parsed_epoch.unwrap_err()))) .unwrap(); } - let (epoch, extension) = parse_result.unwrap(); - context.insert("text", &f.convert(epoch.elapsed().ok().unwrap())); - context.insert("width", "512"); - context.insert("height", "34"); + // Convert epoch to DateTime + let naive_time = NaiveDateTime::from_timestamp_opt(parsed_epoch.unwrap(), 0); + let utc_time = DateTime::::from_utc(naive_time.unwrap(), Utc); - let data = TEMPLATES.render("basic.svg", &context); - if data.is_err() { + // Build context for rendering + let context = RenderContext { + output_form: OutputForm::Relative, + value: utc_time, + tz_offset: utc_time.offset().fix(), + tz_name: "UTC", + view: "basic", + }; + + let rendered_template = render_template(context); + + if rendered_template.is_err() { return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Full::from( - format!("Template Could Not Be Rendered :: {}", data.err().unwrap()) + format!("Template Could Not Be Rendered :: {}", rendered_template.err().unwrap()) )) .unwrap(); } - match extension.as_str() { - "svg" => { - Response::builder() - .header(header::CONTENT_TYPE, "image/svg+xml") - .body(Full::from(data.unwrap())) - .unwrap() - } - "png" => { - let raw_image = renderer.render(data.unwrap().into_bytes()); - if raw_image.is_err() { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Full::from( - format!("Internal Server Error :: {}", raw_image.err().unwrap()) - )) - .unwrap(); - } - + let rasterize_result = handle_rasterize(rendered_template.unwrap(), extension); + match rasterize_result { + Ok((mime_type, bytes)) => { Response::builder() .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "image/x-png") - .body(Full::from(raw_image.unwrap())) + .header(header::CONTENT_TYPE, mime_type) + .body(Full::from(bytes)) .unwrap() } - _ => { - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Full::from("Unsupported extension.")) - .unwrap() + Err(e) => { + match e { + TimeBannerError::RenderError(msg) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from(format!("Template Could Not Be Rendered :: {}", msg))) + .unwrap(), + TimeBannerError::ParseError(msg) => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from(format!("Failed to parse epoch :: {}", msg))) + .unwrap(), + TimeBannerError::RasterizeError(msg) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from(format!("Failed to rasterize :: {}", msg))) + .unwrap(), + } } } } \ No newline at end of file diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..4f9c869 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,47 @@ +use chrono::{DateTime, FixedOffset, Utc}; +use timeago::Formatter; +use tera::{Context, Tera}; +use lazy_static::lazy_static; + +lazy_static! { + static ref TEMPLATES: Tera = { + let mut _tera = match Tera::new("templates/**/*.svg") { + Ok(t) => { + let names: Vec<&str> = t.get_template_names().collect(); + println!("{} templates found ([{}]).", names.len(), names.join(", ")); + t + }, + Err(e) => { + println!("Parsing error(s): {}", e); + ::std::process::exit(1); + } + }; + _tera + }; +} + +pub enum OutputForm { + Relative, + Absolute, +} + +pub struct RenderContext<'a> { + pub output_form: OutputForm, + pub value: DateTime, + pub tz_offset: FixedOffset, + pub tz_name: &'a str, + pub view: &'a str, +} + +pub fn render_template(context: RenderContext) -> Result { + let mut template_context = Context::new(); + let formatter = Formatter::new(); + + template_context.insert("text", match context.output_form { + OutputForm::Relative => formatter.convert_chrono(context.value, Utc::now()), + OutputForm::Absolute => context.value.to_rfc3339() + }.as_str()); + + TEMPLATES.render("basic.svg", &template_context) +} + diff --git a/src/templates/basic.svg b/src/templates/basic.svg index 59b67f6..3fd0a9b 100644 --- a/src/templates/basic.svg +++ b/src/templates/basic.svg @@ -1,4 +1,4 @@ - + {{ text }}