dial9_viewer/server/
mod.rs1use 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 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}