fabrica

hanna/fabrica

3600 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Static-asset, theme, and operational endpoints (`/assets`, `/theme`,
6//! `/healthz`, `/version`).
7
8use std::process::Command;
9
10use axum::Json;
11use axum::extract::{Path, State};
12use axum::http::{StatusCode, header};
13use axum::response::{IntoResponse, Response};
14
15use crate::AppState;
16
17/// Serve an embedded or disk-overridden asset by path.
18pub async fn assets(State(state): State<AppState>, Path(path): Path<String>) -> Response {
19 match state.assets().asset(&path) {
20 Some((bytes, content_type)) => (
21 [
22 (header::CONTENT_TYPE, content_type),
23 (header::CACHE_CONTROL, "public, max-age=3600"),
24 ],
25 bytes,
26 )
27 .into_response(),
28 None => StatusCode::NOT_FOUND.into_response(),
29 }
30}
31
32/// Serve a named theme stylesheet (`/theme/dark.css`).
33pub async fn theme_named(State(state): State<AppState>, Path(file): Path<String>) -> Response {
34 let name = file.strip_suffix(".css").unwrap_or(&file);
35 theme_response(&state, name)
36}
37
38/// Serve the instance default theme (`/theme.css`), used by self-contained error
39/// pages that do not know the visitor's chosen theme.
40pub async fn theme_default(State(state): State<AppState>) -> Response {
41 let name = state.config.ui.theme.clone();
42 theme_response(&state, &name)
43}
44
45/// Build a CSS response for theme `name`, or 404.
46fn theme_response(state: &AppState, name: &str) -> Response {
47 match state.assets().theme_css(name) {
48 Some(bytes) => (
49 [
50 (header::CONTENT_TYPE, "text/css; charset=utf-8"),
51 (header::CACHE_CONTROL, "public, max-age=3600"),
52 ],
53 bytes,
54 )
55 .into_response(),
56 None => StatusCode::NOT_FOUND.into_response(),
57 }
58}
59
60/// `/healthz` — liveness plus the git plumbing and database status.
61pub async fn healthz(State(state): State<AppState>) -> Response {
62 let git = git_version(&state.config.git.binary);
63 let db_ok = state.store.migrate().await.is_ok();
64 let healthy = git.is_some() && db_ok;
65 let storage = match state.config.storage.backend {
66 config::StorageBackend::Local => "local".to_string(),
67 config::StorageBackend::S3 => state
68 .config
69 .storage
70 .s3
71 .as_ref()
72 .map_or_else(|| "s3".to_string(), |s3| format!("s3:{}", s3.bucket)),
73 };
74 let body = serde_json::json!({
75 "status": if healthy { "ok" } else { "degraded" },
76 "git": git,
77 "git_binary": state.config.git.binary,
78 "database": if db_ok { "ok" } else { "error" },
79 "storage": storage,
80 });
81 let status = if healthy {
82 StatusCode::OK
83 } else {
84 StatusCode::SERVICE_UNAVAILABLE
85 };
86 (status, Json(body)).into_response()
87}
88
89/// `/version` — the instance name and build version.
90pub async fn version(State(state): State<AppState>) -> Json<serde_json::Value> {
91 Json(serde_json::json!({
92 "name": state.config.instance.name,
93 "version": env!("CARGO_PKG_VERSION"),
94 }))
95}
96
97/// Return the `git --version` string, or `None` if it cannot be run.
98fn git_version(binary: &str) -> Option<String> {
99 let output = Command::new(binary).arg("--version").output().ok()?;
100 output
101 .status
102 .success()
103 .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string())
104}