Merge asset and page server

This commit is contained in:
2025-11-08 09:33:12 -08:00
parent 6cb54c2300
commit e70170ee5b
18 changed files with 279 additions and 621 deletions

View File

@@ -0,0 +1,44 @@
use axum::http::{
HeaderMap, HeaderValue, StatusCode,
header::{self},
};
use std::pin::Pin;
use toolbox::mime::MimeType;
use crate::{Rendered, RenderedBody, RequestContext, Servable};
pub struct StaticAsset {
pub bytes: &'static [u8],
pub mime: MimeType,
}
impl Servable for StaticAsset {
fn render<'a>(
&'a self,
_ctx: &'a RequestContext,
) -> Pin<Box<dyn Future<Output = crate::Rendered> + 'a + Send + Sync>> {
Box::pin(async {
let mut headers = HeaderMap::with_capacity(3);
#[expect(clippy::unwrap_used)]
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str(&self.mime.to_string()).unwrap(),
);
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_str(&format!("immutable, public, max-age={}", 60 * 60 * 24 * 30))
.unwrap(),
);
return Rendered {
code: StatusCode::OK,
headers,
body: RenderedBody::Static(self.bytes),
ttl: None,
immutable: true,
};
})
}
}

View File

@@ -1,2 +1,3 @@
pub mod asset;
pub mod page;
pub mod redirect;

View File

@@ -7,7 +7,7 @@ use maud::{Markup, Render, html};
use serde::Deserialize;
use std::pin::Pin;
use crate::{Rendered, RequestContext, Servable};
use crate::{Rendered, RenderedBody, RequestContext, Servable};
//
// MARK: metadata
@@ -67,6 +67,7 @@ impl Render for PageMetadata {
// Some HTML
pub struct Page {
pub meta: PageMetadata,
pub immutable: bool,
/// How long this page's html may be cached.
/// This controls the maximum age of a page shown to the user.
@@ -94,10 +95,11 @@ impl Default for Page {
fn default() -> Self {
Page {
meta: Default::default(),
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
//css_ttl: Duration::from_secs(60 * 24 * 30),
html_ttl: Some(TimeDelta::seconds(60 * 60 * 24 * 30)),
//css_ttl: Duration::from_secs(60 * 60 * 24 * 30),
//generate_css: None,
generate_html: Box::new(|_, _| Box::pin(async { html!() })),
immutable: true,
}
}
}
@@ -125,8 +127,9 @@ impl Servable for Page {
return Rendered {
code: StatusCode::OK,
headers,
body: html.0.into_bytes(),
body: RenderedBody::Markup(html),
ttl: self.html_ttl,
immutable: self.immutable,
};
})
}

View File

@@ -5,7 +5,7 @@ use axum::http::{
header::{self, InvalidHeaderValue},
};
use crate::{Rendered, RequestContext, Servable};
use crate::{Rendered, RenderedBody, RequestContext, Servable};
pub struct Redirect {
to: HeaderValue,
@@ -31,8 +31,9 @@ impl Servable for Redirect {
return Rendered {
code: StatusCode::PERMANENT_REDIRECT,
headers,
body: Vec::new(),
body: RenderedBody::Empty,
ttl: None,
immutable: true,
};
})
}

View File

@@ -8,6 +8,7 @@ use axum::{
use chrono::{DateTime, TimeDelta, Utc};
use libservice::ServiceConnectInfo;
use lru::LruCache;
use maud::Markup;
use parking_lot::Mutex;
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
use tower_http::compression::{CompressionLayer, DefaultPredicate};
@@ -15,13 +16,21 @@ use tracing::trace;
use crate::{ClientInfo, RequestContext};
#[derive(Clone)]
pub enum RenderedBody {
Markup(Markup),
Static(&'static [u8]),
Empty,
}
#[derive(Clone)]
pub struct Rendered {
pub code: StatusCode,
pub headers: HeaderMap,
pub body: Vec<u8>,
pub body: RenderedBody,
pub ttl: Option<TimeDelta>,
pub immutable: bool,
}
pub trait Servable: Send + Sync {
@@ -209,20 +218,31 @@ impl PageServer {
#[expect(clippy::unwrap_used)]
let (mut html, expires) = html_expires.unwrap();
let max_age = match expires {
Some(expires) => (expires - now).num_seconds().max(1),
None => 1,
};
if !html.headers.contains_key(header::CACHE_CONTROL) {
let max_age = match expires {
Some(expires) => (expires - now).num_seconds().max(1),
None => 1,
};
#[expect(clippy::unwrap_used)]
html.headers.insert(
header::CACHE_CONTROL,
// immutable; public/private
HeaderValue::from_str(&format!("immutable, public, max-age={}", max_age)).unwrap(),
);
#[expect(clippy::unwrap_used)]
let mut value = String::new();
if html.immutable {
value.push_str("immutable, ");
}
html.headers
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
value.push_str("public, ");
value.push_str(&format!("max-age={}, ", max_age));
html.headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_str(&value.trim().trim_end_matches(',')).unwrap(),
);
}
if !html.headers.contains_key("Accept-CH") {
html.headers
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
}
trace!(
message = "Served route",
@@ -233,7 +253,11 @@ impl PageServer {
time_ns = start.elapsed().as_nanos()
);
return (html.code, html.headers, html.body).into_response();
return match html.body {
RenderedBody::Markup(markup) => (html.code, html.headers, markup.0).into_response(),
RenderedBody::Static(data) => (html.code, html.headers, data).into_response(),
RenderedBody::Empty => (html.code, html.headers).into_response(),
};
}
pub fn into_router(self: Arc<Self>) -> Router<()> {