138 lines
3.7 KiB
Rust
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())
|
|
}
|