Skip to main content

dial9_viewer/server/
mod.rs

1use crate::storage::StorageBackend;
2use axum::Router;
3use axum::body::Body;
4use axum::http::{HeaderValue, StatusCode, header};
5use axum::response::{IntoResponse, Response};
6use rust_embed::Embed;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10mod config;
11mod prefixes;
12mod search;
13mod trace;
14
15#[derive(Embed)]
16#[folder = "ui/"]
17struct UiAssets;
18
19#[derive(Clone)]
20#[non_exhaustive]
21pub struct AppState {
22    pub backend: Arc<dyn StorageBackend>,
23    pub default_bucket: Option<String>,
24    pub default_prefix: Option<String>,
25    /// When set, serve UI files from disk instead of embedded assets.
26    pub dev_ui_dir: Option<PathBuf>,
27}
28
29impl AppState {
30    pub fn new(
31        backend: Arc<dyn StorageBackend>,
32        default_bucket: Option<String>,
33        default_prefix: Option<String>,
34    ) -> Self {
35        Self {
36            backend,
37            default_bucket,
38            default_prefix,
39            dev_ui_dir: None,
40        }
41    }
42
43    pub fn with_dev_ui_dir(mut self, dir: PathBuf) -> Self {
44        self.dev_ui_dir = Some(dir);
45        self
46    }
47}
48
49pub fn router(state: AppState) -> Router {
50    if let Some(dir) = state.dev_ui_dir.clone() {
51        tracing::info!(path = %dir.display(), "serving UI from disk (dev mode)");
52        Router::new()
53            .nest("/api", api_router(state))
54            .fallback_service(tower_http::services::ServeDir::new(dir))
55    } else {
56        Router::new()
57            .nest("/api", api_router(state))
58            .fallback(serve_embedded)
59    }
60}
61
62async fn serve_embedded(uri: axum::http::Uri) -> Response {
63    let path = uri.path().trim_start_matches('/');
64    let path = if path.is_empty() { "index.html" } else { path };
65
66    match UiAssets::get(path) {
67        Some(file) => {
68            let mime = mime_guess::from_path(path).first_or_octet_stream();
69            Response::builder()
70                .header(
71                    header::CONTENT_TYPE,
72                    HeaderValue::from_str(mime.as_ref()).unwrap(),
73                )
74                .body(Body::from(file.data.to_vec()))
75                .unwrap()
76                .into_response()
77        }
78        None => (StatusCode::NOT_FOUND, "not found").into_response(),
79    }
80}
81
82fn api_router(state: AppState) -> Router {
83    Router::new()
84        .route("/config", axum::routing::get(config::get_config))
85        .route("/prefixes", axum::routing::get(prefixes::list_prefixes))
86        .route("/search", axum::routing::get(search::search))
87        .route("/trace", axum::routing::get(trace::get_trace))
88        .with_state(state)
89}