use std::str::FromStr; use ::mime::Mime; use axum::{ http::StatusCode, response::{self, IntoResponse}, }; use axum_extra::TypedHeader; use headers::{ContentType, ETag, IfNoneMatch}; use rust_embed::EmbeddedFile; use super::{error::NotFound, mime}; use crate::error::{ Internal, failed::{Failed, ResultExt}, }; #[derive(rust_embed::Embed)] #[folder = "$OUT_DIR/ui"] pub struct Assets; impl Assets { pub fn load(path: impl AsRef) -> Result { let path = path.as_ref(); let mime = mime::from_path(path).fail("Failed to determine MIME type from asset path")?; let file = Self::get(path).ok_or(Error::NotFound(path.into()))?; let asset = Asset::new(mime, file)?; Ok(asset) } pub fn index() -> Result { // "not found" in this case really is an internal error, as it should // never happen. `index.html` is a known-valid path with a known-valid // file extension. Ok(Self::load("index.html")?) } } pub struct Asset { mime: Mime, file: EmbeddedFile, etag: ETag, } impl Asset { fn new(mime: Mime, file: EmbeddedFile) -> Result { let etag = Self::etag_from(&file).fail("Failed to compute ETag")?; Ok(Self { mime, file, etag }) } fn etag_from(file: &EmbeddedFile) -> Result::Err> { let digest = file.metadata.sha256_hash(); let digest = hex::encode(digest); let tag = format!("\"{digest}\""); let tag = ETag::from_str(&tag)?; Ok(tag) } pub fn differs_from(&self, if_none_match: &IfNoneMatch) -> bool { if_none_match.precondition_passes(&self.etag) } } impl IntoResponse for Asset { fn into_response(self) -> response::Response { let Self { mime, file, etag } = self; ( StatusCode::OK, TypedHeader(ContentType::from(mime)), TypedHeader(etag), file.data, ) .into_response() } } // Clippy warns us here because the `Asset` variant is the size of, well, the Asset struct - itself // quite large due to the embedded file metadata. There isn't a better alternative that I can see, // and it's not causing any performance or stack exhaustion problems in practice. #[expect(clippy::large_enum_variant)] pub enum Response { NotModified, Asset(Asset), } impl Response { pub fn load(path: impl AsRef, if_none_match: &IfNoneMatch) -> Result { let asset = Assets::load(path)?; let response = Self::respond_with(asset, if_none_match); Ok(response) } pub fn index(if_none_match: &IfNoneMatch) -> Result { let asset = Assets::index()?; let response = Self::respond_with(asset, if_none_match); Ok(response) } fn respond_with(asset: Asset, if_none_match: &IfNoneMatch) -> Self { if asset.differs_from(if_none_match) { Self::Asset(asset) } else { Self::NotModified } } } impl IntoResponse for Response { fn into_response(self) -> response::Response { match self { Self::NotModified => StatusCode::NOT_MODIFIED.into_response(), Self::Asset(asset) => asset.into_response(), } } } #[derive(Debug, thiserror::Error)] pub enum Error { #[error("not found: {0}")] NotFound(String), #[error(transparent)] Failed(#[from] Failed), } impl IntoResponse for Error { fn into_response(self) -> response::Response { match self { Self::NotFound(_) => NotFound(self.to_string()).into_response(), Self::Failed(_) => Internal::from(self).into_response(), } } }