// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Static-asset, theme, and operational endpoints (`/assets`, `/theme`, //! `/healthz`, `/version`). use std::process::Command; use axum::Json; use axum::extract::{Path, State}; use axum::http::{StatusCode, header}; use axum::response::{IntoResponse, Response}; use crate::AppState; /// Serve an embedded or disk-overridden asset by path. pub async fn assets(State(state): State, Path(path): Path) -> Response { match state.assets().asset(&path) { Some((bytes, content_type)) => ( [ (header::CONTENT_TYPE, content_type), (header::CACHE_CONTROL, "public, max-age=3600"), ], bytes, ) .into_response(), None => StatusCode::NOT_FOUND.into_response(), } } /// Serve a named theme stylesheet (`/theme/dark.css`). pub async fn theme_named(State(state): State, Path(file): Path) -> Response { let name = file.strip_suffix(".css").unwrap_or(&file); theme_response(&state, name) } /// Serve the instance default theme (`/theme.css`), used by self-contained error /// pages that do not know the visitor's chosen theme. pub async fn theme_default(State(state): State) -> Response { let name = state.config.ui.theme.clone(); theme_response(&state, &name) } /// Build a CSS response for theme `name`, or 404. fn theme_response(state: &AppState, name: &str) -> Response { match state.assets().theme_css(name) { Some(bytes) => ( [ (header::CONTENT_TYPE, "text/css; charset=utf-8"), (header::CACHE_CONTROL, "public, max-age=3600"), ], bytes, ) .into_response(), None => StatusCode::NOT_FOUND.into_response(), } } /// `/healthz` — liveness plus the git plumbing and database status. pub async fn healthz(State(state): State) -> Response { let git = git_version(&state.config.git.binary); let db_ok = state.store.migrate().await.is_ok(); let healthy = git.is_some() && db_ok; let storage = match state.config.storage.backend { config::StorageBackend::Local => "local".to_string(), config::StorageBackend::S3 => state .config .storage .s3 .as_ref() .map_or_else(|| "s3".to_string(), |s3| format!("s3:{}", s3.bucket)), }; let body = serde_json::json!({ "status": if healthy { "ok" } else { "degraded" }, "git": git, "git_binary": state.config.git.binary, "database": if db_ok { "ok" } else { "error" }, "storage": storage, }); let status = if healthy { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE }; (status, Json(body)).into_response() } /// `/version` — the instance name and build version. pub async fn version(State(state): State) -> Json { Json(serde_json::json!({ "name": state.config.instance.name, "version": env!("CARGO_PKG_VERSION"), })) } /// Return the `git --version` string, or `None` if it cannot be run. fn git_version(binary: &str) -> Option { let output = Command::new(binary).arg("--version").output().ok()?; output .status .success() .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) }