diff options
Diffstat (limited to 'src/ui/assets.rs')
| -rw-r--r-- | src/ui/assets.rs | 91 |
1 files changed, 81 insertions, 10 deletions
diff --git a/src/ui/assets.rs b/src/ui/assets.rs index ede1f7c..b8ca00b 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -1,8 +1,12 @@ +use std::str::FromStr; + use ::mime::Mime; use axum::{ - http::{StatusCode, header}, - response::{IntoResponse, Response}, + http::StatusCode, + response::{self, IntoResponse}, }; +use axum_extra::TypedHeader; +use headers::{ContentType, ETag, IfNoneMatch}; use rust_embed::EmbeddedFile; use super::{error::NotFound, mime}; @@ -20,9 +24,10 @@ impl Assets { let path = path.as_ref(); let mime = mime::from_path(path).fail("Failed to determine MIME type from asset path")?; - Self::get(path) - .map(|file| Asset(mime, file)) - .ok_or(Error::NotFound(path.into())) + let file = Self::get(path).ok_or(Error::NotFound(path.into()))?; + let asset = Asset::new(mime, file)?; + + Ok(asset) } pub fn index() -> Result<Asset, Internal> { @@ -33,20 +38,86 @@ impl Assets { } } -pub struct Asset(Mime, EmbeddedFile); +pub struct Asset { + mime: Mime, + file: EmbeddedFile, + etag: ETag, +} + +impl Asset { + fn new(mime: Mime, file: EmbeddedFile) -> Result<Self, Failed> { + let etag = Self::etag_from(&file).fail("Failed to compute ETag")?; + Ok(Self { mime, file, etag }) + } + + fn etag_from(file: &EmbeddedFile) -> Result<ETag, <ETag as FromStr>::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 { - let Self(mime, file) = self; + fn into_response(self) -> response::Response { + let Self { mime, file, etag } = self; ( StatusCode::OK, - [(header::CONTENT_TYPE, mime.as_ref())], + 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<str>, if_none_match: &IfNoneMatch) -> Result<Self, Error> { + let asset = Assets::load(path)?; + let response = Self::respond_with(asset, if_none_match); + Ok(response) + } + + pub fn index(if_none_match: &IfNoneMatch) -> Result<Self, Internal> { + 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}")] @@ -56,7 +127,7 @@ pub enum Error { } impl IntoResponse for Error { - fn into_response(self) -> Response { + fn into_response(self) -> response::Response { match self { Self::NotFound(_) => NotFound(self.to_string()).into_response(), Self::Failed(_) => Internal::from(self).into_response(), |
