diff --git a/Cargo.lock b/Cargo.lock index 87b6a39..037fef2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2624,7 +2624,6 @@ dependencies = [ "cached", "chrono", "config", - "glob", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", diff --git a/Cargo.toml b/Cargo.toml index 5adec3a..9340524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ axum = { version = "0.7.5", features = ["http2", "original-uri", "tracing"] } cached = "0.49.3" chrono = { version = "0.4.31", features = ["serde"] } config = "0.14.0" -glob = "0.3.0" opentelemetry = { version = "0.22.0", features = ["trace", "metrics"] } opentelemetry-otlp = { version = "0.15.0", features = ["trace", "metrics", "logs"] } opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio", "trace", "metrics"] } diff --git a/posts/draft-test.md b/pages/posts/draft-test.md similarity index 100% rename from posts/draft-test.md rename to pages/posts/draft-test.md diff --git a/posts/dungeon/index.md b/pages/posts/dungeon/index.md similarity index 100% rename from posts/dungeon/index.md rename to pages/posts/dungeon/index.md diff --git a/posts/dungeon/screenshot.png b/pages/posts/dungeon/screenshot.png similarity index 100% rename from posts/dungeon/screenshot.png rename to pages/posts/dungeon/screenshot.png diff --git a/posts/foldertest/FbHSmoeUUAA2x-m.png b/pages/posts/foldertest/FbHSmoeUUAA2x-m.png similarity index 100% rename from posts/foldertest/FbHSmoeUUAA2x-m.png rename to pages/posts/foldertest/FbHSmoeUUAA2x-m.png diff --git a/posts/foldertest/index.md b/pages/posts/foldertest/index.md similarity index 100% rename from posts/foldertest/index.md rename to pages/posts/foldertest/index.md diff --git a/posts/hello-world.md b/pages/posts/hello-world.md similarity index 100% rename from posts/hello-world.md rename to pages/posts/hello-world.md diff --git a/posts/status-update-2020-05-16/index.md b/pages/posts/status-update-2020-05-16/index.md similarity index 100% rename from posts/status-update-2020-05-16/index.md rename to pages/posts/status-update-2020-05-16/index.md diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 7c96c3f..adf804f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -28,15 +28,12 @@ async fn record_hit(method: String, path: String) { counter.add(1, &[KeyValue::new("path", format!("{method} {path}"))]); } -pub fn routes(state: &Arc) -> Router> { +pub fn routes() -> Router> { Router::new() .route("/", get(index)) .merge(pages::router()) .merge(tags::router()) - // .merge(pages::pages_router(state.pages.values())) - .merge(pages::alias_router(state.pages.values())) .route("/healthcheck", get(healthcheck)) - .route_service("/posts/:slug/*path", ServeDir::new("./")) .route_service("/static/*path", ServeDir::new("./")) .layer(axum::middleware::from_fn(metrics_middleware)) } @@ -146,13 +143,13 @@ mod tests { }; // Load the actual posts, just to make this test fail if // 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 .unwrap(); state.tags = crate::tag::get_tags(posts.values()); state.pages = posts; let state = Arc::new(state); - super::routes(&state).with_state(state).into_make_service(); + super::routes().with_state(state).into_make_service(); } } diff --git a/src/handlers/pages.rs b/src/handlers/pages.rs index 8ecf0bf..4256eb7 100644 --- a/src/handlers/pages.rs +++ b/src/handlers/pages.rs @@ -1,15 +1,17 @@ use std::sync::Arc; use axum::{ - extract::{Path, State}, - http::{self, header, HeaderMap, StatusCode}, + extract::{OriginalUri, Path, Request, State}, + http::{self, header, request, HeaderMap, StatusCode}, response::{Html, IntoResponse, Redirect, Response}, routing::get, Router, }; 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::{ page::{render_page, Page}, @@ -20,59 +22,10 @@ use super::should_return_304; pub fn router() -> Router> { Router::new() - .route("/posts", get(|| async { Redirect::permanent("/") })) .route("/atom.xml", get(feed)) .route("/posts/", get(index)) - .route("/posts/:slug", get(redirect)) - .route("/posts/:slug/", get(view)) - .route("/posts/:slug/index.md", get(super::not_found)) -} - -// pub fn pages_router<'a>(pages: impl IntoIterator) -> Router> { -// let mut router = Router::new(); - -// for post in pages { -// let slug = post.slug.clone(); -// router = router.route( -// &post.absolute_path, -// get( -// move |state: State>, -// 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) -> Router> { - 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 + .route("/posts", get(|| async { Redirect::permanent("/") })) + .route("/*path", get(view)) } #[derive(Serialize, Debug)] @@ -80,7 +33,7 @@ struct PageContext<'a> { title: &'a str, } -#[instrument(skip(state))] +#[instrument(skip(state, headers))] async fn index( State(state): State>, headers: HeaderMap, @@ -125,19 +78,42 @@ async fn index( .into_response()) } -#[instrument(skip(state))] +#[instrument(skip(state, uri, method, headers, request))] async fn view( - Path(slug): Path, + uri: OriginalUri, State(state): State>, method: http::method::Method, headers: HeaderMap, -) -> Result { - let post = state.pages.get(&slug).ok_or(WebsiteError::NotFound)?; + request: Request, +) -> Result { + // 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() { return Err(WebsiteError::NotFound); } + // Check if they have the current page cached if let Some(etag) = headers.get(header::IF_NONE_MATCH) { if let Ok(etag) = etag.to_str() { if etag == post.etag { @@ -207,11 +183,13 @@ pub async fn feed( #[instrument(skip(state))] pub async fn redirect( - Path(slug): Path, + uri: OriginalUri, State(state): State>, ) -> Result { - if state.pages.contains_key(&slug) { - Ok(Redirect::permanent(&format!("/posts/{slug}/"))) + let path = uri.path(); + let p = format!("{path}/"); + if state.pages.contains_key(&p) { + Ok(Redirect::permanent(&format!("{path}/"))) } else { Err(WebsiteError::NotFound) } diff --git a/src/main.rs b/src/main.rs index e123a58..1cfd7c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ pub struct AppState { startup_time: DateTime, base_url: Uri, pages: HashMap, + aliases: HashMap, tags: HashMap, tera: Tera, } @@ -66,19 +67,27 @@ async fn init_app(cfg: &Settings) -> Result { ..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()); for page in pages.values() { debug!("slug: {}, path: {}", page.slug, page.absolute_path); } 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.tags = tags; let state = Arc::new(state); info!("Listening at {}", state.base_url); - Ok(handlers::routes(&state) + Ok(handlers::routes() .layer(CorsLayer::permissive()) .layer(CompressionLayer::new()) .layer( diff --git a/src/page.rs b/src/page.rs index 46ab34a..8e40c9d 100644 --- a/src/page.rs +++ b/src/page.rs @@ -47,7 +47,7 @@ impl Page { let etag = format!("W/\"{:x}\"", hasher.finish()); Page { - absolute_path: format!("/posts/{slug}/"), + absolute_path: format!("/{slug}/"), slug, etag, content, @@ -80,10 +80,10 @@ impl Page { #[instrument(skip(state))] pub async fn load_all(state: &AppState, folder: PathBuf) -> Result> { let mut pages = HashMap::::new(); - let mut dirs: Vec = vec![folder]; + let mut dirs: Vec = vec![folder.clone()]; 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? { let path = entry.path(); @@ -92,34 +92,26 @@ pub async fn load_all(state: &AppState, folder: PathBuf) -> Result Result { +pub async fn load_page(state: &AppState, path: &Path, root_folder: &Path) -> Result { debug!("loading page: {path:?}"); let content = fs::read_to_string(path).await?; let path_str = path.to_string_lossy().replace('\\', "/"); + let root = root_folder.to_string_lossy(); - let slug = path_str - .trim_start_matches("posts") + let slug = path_str[root.len()..] .trim_start_matches('/') .trim_end_matches(".html") .trim_end_matches(".md") @@ -127,10 +119,11 @@ pub async fn load_page(state: &AppState, path: &Path) -> Result { .trim_end_matches('/'); let base_path = if let Some(i) = path_str.rfind('/') { - &path_str[..=i] + &path_str[root.len()..=i] } else { - &path_str - }; + &path_str[root.len()..] + } + .trim_start_matches("pages/"); let base_uri = helpers::uri_with_path(&state.base_url, base_path); @@ -169,7 +162,7 @@ mod tests { ..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() { super::render_page(&state, post).await.unwrap(); }