Replace hardcoded static files with a static endpoint

This commit is contained in:
Juno Takano 2026-01-17 16:07:08 -03:00
commit 1103c89428
35 changed files with 206 additions and 145 deletions

View file

@ -9,6 +9,7 @@ mod handlers {
pub mod navigation;
pub mod fixed;
pub mod error;
pub mod mime;
}
#[derive(Clone)]
@ -32,36 +33,7 @@ pub fn new(graph: Graph) -> Router {
.route("/graph/{format}", get(handlers::fixed::serial))
.route("/search", get(handlers::navigation::search))
.route("/redirect", get(handlers::navigation::redirect))
.route(
"/static/style.css",
get(|| handlers::fixed::file("./static/style.css", "text/css")),
)
.route(
"/static/fonts/sans",
get(|| handlers::fixed::file("./static/fonts/sans", "")),
)
.route(
"/static/fonts/serifed",
get(|| handlers::fixed::file("./static/fonts/serifed", "")),
)
.route(
"/static/fonts/mono",
get(|| handlers::fixed::file("./static/fonts/mono", "")),
)
.route(
"/static/fonts/title",
get(|| handlers::fixed::file("./static/fonts/title", "")),
)
.route(
"/static/fonts/prose",
get(|| handlers::fixed::file("./static/fonts/prose", "")),
)
.route(
"/static/favicon.svg",
get(|| {
handlers::fixed::file("./static/favicon.svg", "image/svg+xml")
}),
);
.route("/static/{*path}", get(handlers::fixed::file));
if state.graph.meta.config.tree {
router = router.route("/tree", get(handlers::navigation::tree));
@ -127,8 +99,8 @@ mod tests {
"/tree",
"/data",
"/node/Syntax",
"/static/style.css",
"/static/favicon.svg",
"/static/assets/style.css",
"/static/assets/favicon.svg",
"/graph/json",
"/graph/toml",
];

View file

@ -1,24 +1,44 @@
use axum::{
body::Body,
extract::{Path, State},
http::{HeaderValue, Response, StatusCode, header},
{
body::Body,
extract::{Path, State},
},
};
use crate::prelude::*;
use crate::{
graph::{Format, Graph, SerialErrorCause},
router::{GlobalState, handlers},
router::handlers::mime::Mime,
};
/// # Panics
/// Will panic if file read fails.
#[expect(clippy::unused_async)]
pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
pub async fn file(
Path(path): Path<String>,
State(state): State<GlobalState>,
) -> Response<Body> {
let instant = now();
let content = match std::fs::read(file_path) {
let target = format!("static/public/{path}");
let content = match std::fs::read(&target) {
Ok(s) => s,
Err(e) => {
panic!("Failed to read {file_path} contents: {e}")
let mut error_message = String::from(
"The requested file does not exist, the server does not have \
permission to access it or a filesystem error ocurred.",
);
if log::env_level() >= DEBUG {
error_message = format!(
"<p>{error_message}</p>\
<p>Targeted path: <code>{target}</code></p>\
<p>Error message:</p> <pre>{e}</pre>"
);
}
log!(ERROR, "{error_message}");
return super::error::by_code(
Some(404),
Some(&error_message),
&state.graph,
);
},
};
@ -26,19 +46,20 @@ pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
*response.status_mut() = StatusCode::OK;
let header = header::CONTENT_TYPE;
if let Ok(header_value) = HeaderValue::from_str(content_type) {
let content_type = Mime::guess(&path);
if let Ok(header_value) =
HeaderValue::from_str(&String::from(content_type.clone()))
{
response.headers_mut().append(header, header_value);
} else {
log!(
WARN,
"Failed to create content type header value from {content_type}"
"Failed to create content type header value from {content_type:?}"
);
}
tlog!(
&instant,
"Assembled response for {content_type} {file_path}"
);
tlog!(&instant, "Assembled response for {content_type:?} {path}");
response
}
@ -147,30 +168,4 @@ mod tests {
== "application/json"
);
}
#[tokio::test]
async fn file_valid_header() {
let payload = "y1mgMhjeIMFsRNZ1tskP52DfWuvhvbRP";
let response = file("./static/graph.toml", payload).await;
assert_eq!(
response.headers().get(header::CONTENT_TYPE).unwrap(),
payload
);
}
#[tokio::test]
async fn file_invalid_header() {
let response = file("./static/graph.toml", "\n").await;
println!("{response:#?}");
assert!(response.headers().get(header::CONTENT_TYPE).is_none());
}
#[tokio::test]
#[should_panic(
expected = "Failed to read IvnhZhdHb1xDnUw4hYDDNIERoaOojkiu \
contents: No such file or directory (os error 2)"
)]
async fn file_invalid_path() {
drop(file("IvnhZhdHb1xDnUw4hYDDNIERoaOojkiu", "text/plain").await);
}
}

View file

@ -0,0 +1,94 @@
#[derive(Debug, Clone)]
pub enum Mime {
Txt,
Csv,
Css,
Ttf,
Otf,
Woff,
Woff2,
Svg,
Ico,
Jpeg,
Png,
Apng,
Gif,
Webp,
Avif,
Toml,
Xml,
Json,
Js,
Pdf,
Epub,
Unknown,
}
impl From<&str> for Mime {
fn from(extension: &str) -> Mime {
match extension {
"txt" => Mime::Txt,
"csv" => Mime::Csv,
"css" => Mime::Css,
"ttf" => Mime::Ttf,
"otf" => Mime::Otf,
"woff" => Mime::Woff,
"woff2" => Mime::Woff2,
"svg" => Mime::Svg,
"ico" => Mime::Ico,
"jpeg" => Mime::Jpeg,
"png" => Mime::Png,
"apng" => Mime::Apng,
"gif" => Mime::Gif,
"webp" => Mime::Webp,
"avif" => Mime::Avif,
"toml" => Mime::Toml,
"xml" => Mime::Xml,
"json" => Mime::Json,
"js" => Mime::Js,
"pdf" => Mime::Pdf,
"epub" => Mime::Epub,
_ => Mime::Unknown,
}
}
}
impl From<Mime> for String {
fn from(mime: Mime) -> String {
let s = match mime {
Mime::Txt => "text/plain",
Mime::Csv => "text/csv",
Mime::Css => "text/css",
Mime::Ttf => "font/ttf",
Mime::Otf => "font/otf",
Mime::Woff => "font/woff",
Mime::Woff2 => "font/woff2",
Mime::Svg => "image/svg+xml",
Mime::Ico => "image/x-icon",
Mime::Jpeg => "image/jpeg",
Mime::Png => "image/png",
Mime::Apng => "image/apng",
Mime::Gif => "image/gif",
Mime::Webp => "image/webp",
Mime::Avif => "image/avif",
Mime::Toml => "application/toml",
Mime::Xml => "application/xml",
Mime::Json => "application/json",
Mime::Js => "text/javascript",
Mime::Pdf => "application/pdf",
Mime::Epub => "application/epub+zip",
Mime::Unknown => "application/octet-stream",
};
String::from(s)
}
}
impl Mime {
pub fn guess(path: &str) -> Mime {
if let Some(pair) = path.rsplit_once('.') {
Mime::from(pair.1)
} else {
Mime::Unknown
}
}
}