summaryrefslogtreecommitdiff
path: root/src/ui/assets.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/assets.rs')
-rw-r--r--src/ui/assets.rs91
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(),