1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
//! 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<AxumResponse, RuntimeError>`
///
/// 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<axum::response::Response, crate::RuntimeError>;
/// 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::<RequestContext>().await;
let response = next.run(request).await;
if let Some(runtime_error) = response.extensions().get::<Arc<RuntimeError>>() {
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<SplormState>,
Extension(settings): Extension<Settings>,
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<SplormState>,
Extension(settings): Extension<Settings>,
Path(path): Path<String>,
ctx: RequestContext,
) -> Result<Response, RuntimeError> {
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, RuntimeError> {
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, RuntimeError> {
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, RuntimeError> {
Response::builder()
.status(StatusCode::SEE_OTHER)
.header("Location", path)
.body(Body::from("redirecting..."))
.map_err(|_| RuntimeError::Unknown)
}
|