mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-06 09:16:43 -06:00
Complete overhaul of render/rasterize/parsing/templates/routes subsystems
This commit is contained in:
13
src/main.rs
13
src/main.rs
@@ -3,12 +3,14 @@ use std::net::SocketAddr;
|
|||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use config::Configuration;
|
use config::Configuration;
|
||||||
use crate::routes::root_handler;
|
use crate::routes::{relative_handler, implicit_handler, absolute_handler};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod svg;
|
mod raster;
|
||||||
mod abbr;
|
mod abbr;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod parse;
|
||||||
|
mod template;
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -26,7 +28,12 @@ async fn main() {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
let app = Router::new()
|
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));
|
let addr = SocketAddr::from((config.socket_addr(), config.port));
|
||||||
axum::Server::bind(&addr)
|
axum::Server::bind(&addr)
|
||||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||||
|
|||||||
26
src/parse.rs
Normal file
26
src/parse.rs
Normal file
@@ -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<Utc>, 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())))
|
||||||
|
}
|
||||||
@@ -16,11 +16,11 @@ impl std::fmt::Display for RenderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Rasterizer {
|
||||||
font_db: fontdb::Database,
|
font_db: fontdb::Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Rasterizer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut fontdb = fontdb::Database::new();
|
let mut fontdb = fontdb::Database::new();
|
||||||
fontdb.load_system_fonts();
|
fontdb.load_system_fonts();
|
||||||
153
src/routes.rs
153
src/routes.rs
@@ -1,116 +1,119 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::process::Output;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse};
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
use axum::body::Full;
|
use axum::body::{Bytes, Full};
|
||||||
use axum::extract::{Path};
|
use axum::extract::{Path};
|
||||||
use axum::http::{header};
|
use axum::http::{header};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use lazy_static::lazy_static;
|
use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, Utc};
|
||||||
use tera::{Context, Tera};
|
|
||||||
use timeago::Formatter;
|
|
||||||
|
|
||||||
use crate::svg::Renderer;
|
|
||||||
|
|
||||||
lazy_static! {
|
use crate::parse::split_on_extension;
|
||||||
static ref TEMPLATES: Tera = {
|
use crate::raster::Rasterizer;
|
||||||
let mut _tera = match Tera::new("templates/**/*.svg") {
|
use crate::template::{OutputForm, render_template, RenderContext};
|
||||||
Ok(t) => {
|
|
||||||
let names: Vec<&str> = t.get_template_names().collect();
|
|
||||||
println!("{} templates found ([{}]).", names.len(), names.join(", "));
|
fn parse_path(path: &str) -> (&str, &str) {
|
||||||
t
|
split_on_extension(path)
|
||||||
},
|
.or_else(|| Some((path, "svg")))
|
||||||
Err(e) => {
|
.unwrap()
|
||||||
println!("Parsing error(s): {}", e);
|
|
||||||
::std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_tera
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_epoch(epoch: u64) -> SystemTime {
|
enum TimeBannerError {
|
||||||
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(epoch)
|
ParseError(String),
|
||||||
|
RenderError(String),
|
||||||
|
RasterizeError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_path(path: String) -> Result<(SystemTime, String), String> {
|
fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), TimeBannerError> {
|
||||||
if path.contains(".") {
|
match extension {
|
||||||
let split_path = path.split_once(".").unwrap();
|
"svg" => Ok(("image/svg+xml", Bytes::from(data))),
|
||||||
let epoch_int = split_path.0.parse::<u64>();
|
"png" => {
|
||||||
if epoch_int.is_err() {
|
let renderer = Rasterizer::new();
|
||||||
return Err("Epoch is not a valid integer.".to_string());
|
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())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok((convert_epoch(epoch_int.unwrap()), split_path.1.parse().unwrap()));
|
Ok(("image/x-png", Bytes::from(raw_image.unwrap())))
|
||||||
}
|
}
|
||||||
|
_ => Err(TimeBannerError::ParseError(format!("Unsupported extension: {}", extension)))
|
||||||
let epoch_int = path.parse::<u64>();
|
|
||||||
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<String>) -> impl IntoResponse {
|
||||||
|
let (raw_time, extension) = parse_path(path.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn absolute_handler(Path(path): Path<String>) -> impl IntoResponse {
|
||||||
|
let (raw_time, extension) = parse_path(path.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// basic handler that responds with a static string
|
// basic handler that responds with a static string
|
||||||
pub async fn root_handler(Path(path): Path<String>) -> impl IntoResponse {
|
pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
|
||||||
let renderer = Renderer::new();
|
// Get extension if available
|
||||||
let mut context = Context::new();
|
let (raw_time, extension) = parse_path(path.as_str());
|
||||||
let f = Formatter::new();
|
|
||||||
|
|
||||||
let parse_result = parse_path(path);
|
// Parse epoch
|
||||||
if parse_result.is_err() {
|
let parsed_epoch = raw_time.parse::<i64>();
|
||||||
|
if parsed_epoch.is_err() {
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.body(Full::from(parse_result.err().unwrap()))
|
.body(Full::from(format!("Failed to parse epoch :: {}", parsed_epoch.unwrap_err())))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
let (epoch, extension) = parse_result.unwrap();
|
|
||||||
|
|
||||||
context.insert("text", &f.convert(epoch.elapsed().ok().unwrap()));
|
// Convert epoch to DateTime
|
||||||
context.insert("width", "512");
|
let naive_time = NaiveDateTime::from_timestamp_opt(parsed_epoch.unwrap(), 0);
|
||||||
context.insert("height", "34");
|
let utc_time = DateTime::<Utc>::from_utc(naive_time.unwrap(), Utc);
|
||||||
|
|
||||||
let data = TEMPLATES.render("basic.svg", &context);
|
// Build context for rendering
|
||||||
if data.is_err() {
|
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()
|
return Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.body(Full::from(
|
.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();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rasterize_result = handle_rasterize(rendered_template.unwrap(), extension);
|
||||||
|
match rasterize_result {
|
||||||
|
Ok((mime_type, bytes)) => {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(header::CONTENT_TYPE, "image/x-png")
|
.header(header::CONTENT_TYPE, mime_type)
|
||||||
.body(Full::from(raw_image.unwrap()))
|
.body(Full::from(bytes))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
_ => {
|
Err(e) => {
|
||||||
Response::builder()
|
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)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.body(Full::from("Unsupported extension."))
|
.body(Full::from(format!("Failed to parse epoch :: {}", msg)))
|
||||||
.unwrap()
|
.unwrap(),
|
||||||
|
TimeBannerError::RasterizeError(msg) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Full::from(format!("Failed to rasterize :: {}", msg)))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
47
src/template.rs
Normal file
47
src/template.rs
Normal file
@@ -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<Utc>,
|
||||||
|
pub tz_offset: FixedOffset,
|
||||||
|
pub tz_name: &'a str,
|
||||||
|
pub view: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg width="{{ width }}" height="{{ height }}" xmlns="http://www.w3.org/2000/svg" font-family="Roboto Mono" font-size="27">
|
<svg width="512" height="34" xmlns="http://www.w3.org/2000/svg" font-family="Roboto Mono" font-size="27">
|
||||||
<text x="8" y="27">{{ text }}</text>
|
<text x="8" y="27">{{ text }}</text>
|
||||||
<style>
|
<style>
|
||||||
text
|
text
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 209 B After Width: | Height: | Size: 191 B |
Reference in New Issue
Block a user