1
0
Fork 0
website/src/handlers/pages.rs
2025-03-23 20:43:38 +01:00

138 lines
3.7 KiB
Rust

use std::sync::Arc;
use anyhow::anyhow;
use axum::{
Router,
body::Body,
extract::{OriginalUri, Request, State},
http::{self, HeaderMap, StatusCode, Uri, header},
response::{Html, IntoResponse, Redirect, Response},
routing::get,
};
use time::format_description::well_known::Rfc3339;
use tokio::sync::RwLock;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use tracing::instrument;
use crate::{
AppState, WebsiteError,
page::{Page, render_page},
};
use super::should_return_304;
pub fn router() -> Router<Arc<RwLock<AppState>>> {
Router::new()
.route("/atom.xml", get(feed))
.route("/", get(view))
.route("/{*path}", get(view))
}
#[instrument(skip(state, uri, method, headers))]
async fn view(
OriginalUri(uri): OriginalUri,
State(state): State<Arc<RwLock<AppState>>>,
method: http::method::Method,
headers: HeaderMap,
) -> Result<Response, WebsiteError> {
let state = state.read().await;
// Fetch post
let Some(post) = state.pages.get(uri.path()) else {
// Invalid path for a post, check aliases
if let Some(p) = state.aliases.get(uri.path()) {
return Ok(Redirect::permanent(p).into_response());
}
// No alias, check if we can do a simple redirect
if !uri.path().ends_with('/') {
let p = format!("{}/", uri.path());
if state.pages.contains_key(&p) {
return Ok(Redirect::permanent(&p).into_response());
}
}
// no page, check if there's a static file to serve from here
return get_static_file("pages", uri).await;
};
if !post.is_published() {
return Err(WebsiteError::NotFound);
}
// Check if they have the current page cached
if let Some(etag) = headers.get(header::IF_NONE_MATCH) {
if etag.to_str().ok() == Some(post.etag.as_str()) {
return Ok(StatusCode::NOT_MODIFIED.into_response());
}
}
if method == http::method::Method::HEAD {
return Ok((
StatusCode::OK,
[
(header::ETAG, post.etag.as_str()),
(header::CACHE_CONTROL, "no-cache"),
],
)
.into_response());
}
let res = render_page(&state, post).await?;
Ok((
StatusCode::OK,
[
(header::ETAG, post.etag.as_str()),
(header::CACHE_CONTROL, "no-cache"),
],
Html(res),
)
.into_response())
}
#[instrument(skip(state))]
pub async fn feed(
State(state): State<Arc<RwLock<AppState>>>,
headers: HeaderMap,
) -> Result<Response, WebsiteError> {
let state = state.read().await;
let mut posts: Vec<&Page> = state.pages.values().filter(|p| p.is_published()).collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
if let Some(res) = should_return_304(&headers, last_changed) {
return Ok(res);
}
posts.sort_by_key(|p| &p.date);
posts.reverse();
posts.truncate(10);
let last_modified = last_changed.map_or_else(
|| state.startup_time.format(&Rfc3339).unwrap(),
|d| d.format(&Rfc3339).unwrap(),
);
Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, "application/atom+xml"),
(header::LAST_MODIFIED, &last_modified),
],
crate::feed::render_atom_feed(&state)?,
)
.into_response())
}
#[instrument]
async fn get_static_file(base_dir: &str, uri: Uri) -> Result<Response, WebsiteError> {
let req = Request::builder().uri(uri).body(Body::empty()).unwrap();
ServeDir::new(base_dir)
.oneshot(req)
.await
.map(IntoResponse::into_response)
.map_err(|e| anyhow!(e).into())
}