blargh
This commit is contained in:
parent
8efba600f5
commit
74753ad4b4
13 changed files with 67 additions and 92 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2624,7 +2624,6 @@ dependencies = [
|
||||||
"cached",
|
"cached",
|
||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"glob",
|
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"opentelemetry-otlp",
|
"opentelemetry-otlp",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
|
|
|
@ -11,7 +11,6 @@ axum = { version = "0.7.5", features = ["http2", "original-uri", "tracing"] }
|
||||||
cached = "0.49.3"
|
cached = "0.49.3"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
glob = "0.3.0"
|
|
||||||
opentelemetry = { version = "0.22.0", features = ["trace", "metrics"] }
|
opentelemetry = { version = "0.22.0", features = ["trace", "metrics"] }
|
||||||
opentelemetry-otlp = { version = "0.15.0", features = ["trace", "metrics", "logs"] }
|
opentelemetry-otlp = { version = "0.15.0", features = ["trace", "metrics", "logs"] }
|
||||||
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio", "trace", "metrics"] }
|
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio", "trace", "metrics"] }
|
||||||
|
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 343 KiB |
|
@ -28,15 +28,12 @@ async fn record_hit(method: String, path: String) {
|
||||||
counter.add(1, &[KeyValue::new("path", format!("{method} {path}"))]);
|
counter.add(1, &[KeyValue::new("path", format!("{method} {path}"))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
pub fn routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.merge(pages::router())
|
.merge(pages::router())
|
||||||
.merge(tags::router())
|
.merge(tags::router())
|
||||||
// .merge(pages::pages_router(state.pages.values()))
|
|
||||||
.merge(pages::alias_router(state.pages.values()))
|
|
||||||
.route("/healthcheck", get(healthcheck))
|
.route("/healthcheck", get(healthcheck))
|
||||||
.route_service("/posts/:slug/*path", ServeDir::new("./"))
|
|
||||||
.route_service("/static/*path", ServeDir::new("./"))
|
.route_service("/static/*path", ServeDir::new("./"))
|
||||||
.layer(axum::middleware::from_fn(metrics_middleware))
|
.layer(axum::middleware::from_fn(metrics_middleware))
|
||||||
}
|
}
|
||||||
|
@ -146,13 +143,13 @@ mod tests {
|
||||||
};
|
};
|
||||||
// Load the actual posts, just to make this test fail if
|
// Load the actual posts, just to make this test fail if
|
||||||
// aliases overlap with themselves or other routes
|
// aliases overlap with themselves or other routes
|
||||||
let posts = crate::page::load_all(&state, "posts/".into())
|
let posts = crate::page::load_all(&state, "pages/".into())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
state.tags = crate::tag::get_tags(posts.values());
|
state.tags = crate::tag::get_tags(posts.values());
|
||||||
state.pages = posts;
|
state.pages = posts;
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
||||||
super::routes(&state).with_state(state).into_make_service();
|
super::routes().with_state(state).into_make_service();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{OriginalUri, Path, Request, State},
|
||||||
http::{self, header, HeaderMap, StatusCode},
|
http::{self, header, request, HeaderMap, StatusCode},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
use tracing::instrument;
|
use tower::{Service, ServiceExt};
|
||||||
|
use tower_http::{follow_redirect::policy::PolicyExt, services::ServeDir};
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
page::{render_page, Page},
|
page::{render_page, Page},
|
||||||
|
@ -20,59 +22,10 @@ use super::should_return_304;
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/posts", get(|| async { Redirect::permanent("/") }))
|
|
||||||
.route("/atom.xml", get(feed))
|
.route("/atom.xml", get(feed))
|
||||||
.route("/posts/", get(index))
|
.route("/posts/", get(index))
|
||||||
.route("/posts/:slug", get(redirect))
|
.route("/posts", get(|| async { Redirect::permanent("/") }))
|
||||||
.route("/posts/:slug/", get(view))
|
.route("/*path", get(view))
|
||||||
.route("/posts/:slug/index.md", get(super::not_found))
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn pages_router<'a>(pages: impl IntoIterator<Item = &'a Page>) -> Router<Arc<AppState>> {
|
|
||||||
// let mut router = Router::new();
|
|
||||||
|
|
||||||
// for post in pages {
|
|
||||||
// let slug = post.slug.clone();
|
|
||||||
// router = router.route(
|
|
||||||
// &post.absolute_path,
|
|
||||||
// get(
|
|
||||||
// move |state: State<Arc<AppState>>,
|
|
||||||
// method: http::method::Method,
|
|
||||||
// headers: HeaderMap| async {
|
|
||||||
// view(Path(slug), state, method, headers).await
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// for alias in &post.aliases {
|
|
||||||
// let path = post.absolute_path.clone();
|
|
||||||
// router = router.route(
|
|
||||||
// alias,
|
|
||||||
// get(move || async {
|
|
||||||
// let p = path;
|
|
||||||
// Redirect::permanent(&p)
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// router
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn alias_router<'a>(pages: impl IntoIterator<Item = &'a Page>) -> Router<Arc<AppState>> {
|
|
||||||
let mut router = Router::new();
|
|
||||||
|
|
||||||
for post in pages {
|
|
||||||
for alias in &post.aliases {
|
|
||||||
let path = post.absolute_path.clone();
|
|
||||||
router = router.route(
|
|
||||||
alias,
|
|
||||||
get(move || async {
|
|
||||||
let p = path;
|
|
||||||
Redirect::permanent(&p)
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
router
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
|
@ -80,7 +33,7 @@ struct PageContext<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state, headers))]
|
||||||
async fn index(
|
async fn index(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
@ -125,19 +78,42 @@ async fn index(
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state, uri, method, headers, request))]
|
||||||
async fn view(
|
async fn view(
|
||||||
Path(slug): Path<String>,
|
uri: OriginalUri,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
method: http::method::Method,
|
method: http::method::Method,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<axum::response::Response, WebsiteError> {
|
request: Request,
|
||||||
let post = state.pages.get(&slug).ok_or(WebsiteError::NotFound)?;
|
) -> Result<Response, WebsiteError> {
|
||||||
|
// 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 there's an easy redirect
|
||||||
|
if !uri.path().ends_with('/') {
|
||||||
|
let p = format!("{}/", uri.path());
|
||||||
|
if state.pages.contains_key(&p) {
|
||||||
|
return Ok(Redirect::permanent(&p).into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: I don't like how we create a new oneshot for every 404 request, but I don't know if there's a better way
|
||||||
|
return Ok(ServeDir::new("pages")
|
||||||
|
.oneshot(request)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_response());
|
||||||
|
};
|
||||||
|
|
||||||
if !post.is_published() {
|
if !post.is_published() {
|
||||||
return Err(WebsiteError::NotFound);
|
return Err(WebsiteError::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if they have the current page cached
|
||||||
if let Some(etag) = headers.get(header::IF_NONE_MATCH) {
|
if let Some(etag) = headers.get(header::IF_NONE_MATCH) {
|
||||||
if let Ok(etag) = etag.to_str() {
|
if let Ok(etag) = etag.to_str() {
|
||||||
if etag == post.etag {
|
if etag == post.etag {
|
||||||
|
@ -207,11 +183,13 @@ pub async fn feed(
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn redirect(
|
pub async fn redirect(
|
||||||
Path(slug): Path<String>,
|
uri: OriginalUri,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Redirect, WebsiteError> {
|
) -> Result<Redirect, WebsiteError> {
|
||||||
if state.pages.contains_key(&slug) {
|
let path = uri.path();
|
||||||
Ok(Redirect::permanent(&format!("/posts/{slug}/")))
|
let p = format!("{path}/");
|
||||||
|
if state.pages.contains_key(&p) {
|
||||||
|
Ok(Redirect::permanent(&format!("{path}/")))
|
||||||
} else {
|
} else {
|
||||||
Err(WebsiteError::NotFound)
|
Err(WebsiteError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -31,6 +31,7 @@ pub struct AppState {
|
||||||
startup_time: DateTime<chrono::offset::Utc>,
|
startup_time: DateTime<chrono::offset::Utc>,
|
||||||
base_url: Uri,
|
base_url: Uri,
|
||||||
pages: HashMap<String, Page>,
|
pages: HashMap<String, Page>,
|
||||||
|
aliases: HashMap<String, String>,
|
||||||
tags: HashMap<String, Tag>,
|
tags: HashMap<String, Tag>,
|
||||||
tera: Tera,
|
tera: Tera,
|
||||||
}
|
}
|
||||||
|
@ -66,19 +67,27 @@ async fn init_app(cfg: &Settings) -> Result<axum::routing::Router> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = page::load_all(&state, "posts/".into()).await?;
|
let pages = page::load_all(&state, "pages/".into()).await?;
|
||||||
info!("{} pages loaded", pages.len());
|
info!("{} pages loaded", pages.len());
|
||||||
for page in pages.values() {
|
for page in pages.values() {
|
||||||
debug!("slug: {}, path: {}", page.slug, page.absolute_path);
|
debug!("slug: {}, path: {}", page.slug, page.absolute_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags = tag::get_tags(pages.values());
|
let tags = tag::get_tags(pages.values());
|
||||||
|
state.aliases = pages
|
||||||
|
.values()
|
||||||
|
.flat_map(|p| {
|
||||||
|
p.aliases
|
||||||
|
.iter()
|
||||||
|
.map(|a| (a.clone(), p.absolute_path.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
state.pages = pages;
|
state.pages = pages;
|
||||||
state.tags = tags;
|
state.tags = tags;
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
||||||
info!("Listening at {}", state.base_url);
|
info!("Listening at {}", state.base_url);
|
||||||
Ok(handlers::routes(&state)
|
Ok(handlers::routes()
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.layer(CompressionLayer::new())
|
.layer(CompressionLayer::new())
|
||||||
.layer(
|
.layer(
|
||||||
|
|
33
src/page.rs
33
src/page.rs
|
@ -47,7 +47,7 @@ impl Page {
|
||||||
let etag = format!("W/\"{:x}\"", hasher.finish());
|
let etag = format!("W/\"{:x}\"", hasher.finish());
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
absolute_path: format!("/posts/{slug}/"),
|
absolute_path: format!("/{slug}/"),
|
||||||
slug,
|
slug,
|
||||||
etag,
|
etag,
|
||||||
content,
|
content,
|
||||||
|
@ -80,10 +80,10 @@ impl Page {
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn load_all(state: &AppState, folder: PathBuf) -> Result<HashMap<String, Page>> {
|
pub async fn load_all(state: &AppState, folder: PathBuf) -> Result<HashMap<String, Page>> {
|
||||||
let mut pages = HashMap::<String, Page>::new();
|
let mut pages = HashMap::<String, Page>::new();
|
||||||
let mut dirs: Vec<PathBuf> = vec![folder];
|
let mut dirs: Vec<PathBuf> = vec![folder.clone()];
|
||||||
|
|
||||||
while let Some(dir) = dirs.pop() {
|
while let Some(dir) = dirs.pop() {
|
||||||
let mut read_dir = fs::read_dir(dbg!(dir)).await?;
|
let mut read_dir = fs::read_dir(&dir).await?;
|
||||||
|
|
||||||
while let Some(entry) = read_dir.next_entry().await? {
|
while let Some(entry) = read_dir.next_entry().await? {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
@ -92,34 +92,26 @@ pub async fn load_all(state: &AppState, folder: PathBuf) -> Result<HashMap<Strin
|
||||||
} else if let Some(ext) = path.extension() {
|
} else if let Some(ext) = path.extension() {
|
||||||
if ext == "md" {
|
if ext == "md" {
|
||||||
// it's a page to load
|
// it's a page to load
|
||||||
let page = load_page(state, &path).await?;
|
let page = load_page(state, &path, &folder).await?;
|
||||||
pages.insert(page.slug.clone(), page);
|
pages.insert(page.absolute_path.clone(), page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for path in --- {
|
|
||||||
// let path = path.unwrap();
|
|
||||||
// debug!("found page: {}", path.display());
|
|
||||||
|
|
||||||
// let post = load_post(state, &path).await?;
|
|
||||||
|
|
||||||
// res.insert(post.slug.clone(), post);
|
|
||||||
// }
|
|
||||||
Ok(pages)
|
Ok(pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn load_page(state: &AppState, path: &Path) -> Result<Page> {
|
pub async fn load_page(state: &AppState, path: &Path, root_folder: &Path) -> Result<Page> {
|
||||||
debug!("loading page: {path:?}");
|
debug!("loading page: {path:?}");
|
||||||
|
|
||||||
let content = fs::read_to_string(path).await?;
|
let content = fs::read_to_string(path).await?;
|
||||||
|
|
||||||
let path_str = path.to_string_lossy().replace('\\', "/");
|
let path_str = path.to_string_lossy().replace('\\', "/");
|
||||||
|
let root = root_folder.to_string_lossy();
|
||||||
|
|
||||||
let slug = path_str
|
let slug = path_str[root.len()..]
|
||||||
.trim_start_matches("posts")
|
|
||||||
.trim_start_matches('/')
|
.trim_start_matches('/')
|
||||||
.trim_end_matches(".html")
|
.trim_end_matches(".html")
|
||||||
.trim_end_matches(".md")
|
.trim_end_matches(".md")
|
||||||
|
@ -127,10 +119,11 @@ pub async fn load_page(state: &AppState, path: &Path) -> Result<Page> {
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
|
|
||||||
let base_path = if let Some(i) = path_str.rfind('/') {
|
let base_path = if let Some(i) = path_str.rfind('/') {
|
||||||
&path_str[..=i]
|
&path_str[root.len()..=i]
|
||||||
} else {
|
} else {
|
||||||
&path_str
|
&path_str[root.len()..]
|
||||||
};
|
}
|
||||||
|
.trim_start_matches("pages/");
|
||||||
|
|
||||||
let base_uri = helpers::uri_with_path(&state.base_url, base_path);
|
let base_uri = helpers::uri_with_path(&state.base_url, base_path);
|
||||||
|
|
||||||
|
@ -169,7 +162,7 @@ mod tests {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
state.pages = super::load_all(&state, "posts/".into()).await.unwrap();
|
state.pages = super::load_all(&state, "pages/".into()).await.unwrap();
|
||||||
for post in state.pages.values() {
|
for post in state.pages.values() {
|
||||||
super::render_page(&state, post).await.unwrap();
|
super::render_page(&state, post).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue