//! Route handlers //! //! All route handlers live here. //! //! | Handler | Route | //! | - | - | //! | [`index`] | GET `/` | //! | [`static_path`] | GET `/{*path}` | //! | | | //! | [`api`] | Nested under `/api` | //! | [`quark`] | Nested under `/quark` | use std::sync::Arc; use axum::{ Extension, RequestExt, body::Body, extract::{Path, Request, State}, http::StatusCode, middleware::Next, response::Response, }; use sessionhash::RequestContext; pub mod api; pub mod me; pub mod quark; mod staticfile; mod templated; use crate::{RuntimeError, config::Settings, fs::RequestPath, state::SplormState}; /// `Result` /// /// Convenience type for use in routes return types. /// See [Axum's `Response`][response-axum], [`RuntimeError`] /// /// [response-axum]: https://docs.rs/axum/latest/axum/response/type.Response.html type FallibleResponse = Result; /// log runtime errors using a middleware layer. /// /// Very much based on the [axum error_handling example][axum_errorhandling] /// /// [axum_errorhandling]: https://github.com/tokio-rs/axum/blob/3696bfe5f1c30ee57f39dfb992b01bbf57bf2c1f/examples/error-handling/src/main.rs pub async fn log_errors(mut request: Request, next: Next) -> Response { let ctx = request.extract_parts::().await; let response = next.run(request).await; if let Some(runtime_error) = response.extensions().get::>() { match ctx { Ok(ctx) => tracing::error!("{} {runtime_error}", ctx.identifying_string()), Err(_) => tracing::error!("{runtime_error}"), } } response } /// GET / /// /// Assumes the index is a template called "home.html" in the webroot. /// Internally uses [`templated::send_file()`] pub async fn index( State(state): State, Extension(settings): Extension, ctx: RequestContext, ) -> FallibleResponse { tracing::info!("{ctx} GET /"); let rqp = RequestPath::verify_safe(settings.root(), "home.html")?; templated::send_file(rqp, state, &settings).await } /// GET /{*path} /// /// Routes not caught by handlers. Tries to read the path from the filesystem, /// using the [`Mime`] to determine how to treat the file. /// /// If the path, after made-safe with [`RequestPath::verify_safe()`] points /// to a directory, but does not end in a slash, sends a permanent redirect /// to `{path}/` /// /// **[`Mime`] of `"text/html"`** /// Assumes the path is a template, and tries to send it with [`templated::send_file()`] /// /// **Otherwise** /// Attempts to send the file using [`staticfile::send_or_stream_file()`] pub async fn static_path( State(state): State, Extension(settings): Extension, Path(path): Path, ctx: RequestContext, ) -> Result { tracing::trace!("static_path with {path}"); let rqp = RequestPath::verify_safe(settings.root(), &path)?; if rqp.is_dirfile() && !rqp.web_path().ends_with('/') { return permanent_redirect(format!("{}/", rqp.web_path())).await; } if rqp.guess_mime().as_ref() == "text/html" { tracing::info!("{ctx} GET /{path}"); templated::send_file(rqp, state, &settings).await } else { staticfile::send_or_stream_file(rqp).await } } /// Creates a response for 308 Permanent Redirect async fn permanent_redirect(path: String) -> Result { Response::builder() .status(StatusCode::PERMANENT_REDIRECT) .header("Location", path) .body(Body::from("redirecting...")) .map_err(|_| RuntimeError::Unknown) } /// Creates a response for 307 Temporary Redirect async fn temporary_redirect(path: String) -> Result { Response::builder() .status(StatusCode::TEMPORARY_REDIRECT) .header("Location", path) .body(Body::from("redirecting...")) .map_err(|_| RuntimeError::Unknown) } /// Creates a response for 303 See Other async fn see_other(path: String) -> Result { Response::builder() .status(StatusCode::SEE_OTHER) .header("Location", path) .body(Body::from("redirecting...")) .map_err(|_| RuntimeError::Unknown) }