global: snapshot

This commit is contained in:
nym21
2026-01-14 16:38:53 +01:00
parent ddb1db7a8e
commit d75c2a881b
226 changed files with 7776 additions and 20942 deletions
+8
View File
@@ -24,6 +24,7 @@ brk_rpc = { workspace = true }
brk_types = { workspace = true }
brk_traversable = { workspace = true }
derive_more = { workspace = true }
include_dir = "0.7"
vecdb = { workspace = true }
jiff = { workspace = true }
quick_cache = "0.6.18"
@@ -34,5 +35,12 @@ tokio = { workspace = true }
tracing = { workspace = true }
tower-http = { version = "0.6.8", features = ["compression-full", "trace"] }
[build-dependencies]
# importmap = { path = "../../../importmap" }
importmap = "0.2"
[dev-dependencies]
brk_mempool = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["importmap"]
+9 -7
View File
@@ -8,7 +8,7 @@ Serve BRK data via REST API with OpenAPI documentation, response caching, MCP en
## Key Features
- **OpenAPI spec**: Auto-generated documentation at `/api/openapi.json`
- **OpenAPI spec**: Auto-generated documentation at `/api.json` (interactive docs at `/api`)
- **Response caching**: LRU cache with 5000 entries for repeated queries
- **Compression**: Brotli, gzip, deflate, zstd support
- **Static files**: Optional web interface hosting
@@ -17,7 +17,8 @@ Serve BRK data via REST API with OpenAPI documentation, response caching, MCP en
## Core API
```rust,ignore
let server = Server::new(&async_query, Some(files_path));
let server = Server::new(&async_query, data_path, WebsiteSource::Filesystem(files_path));
// Or WebsiteSource::Embedded, or WebsiteSource::Disabled
server.serve(true).await?; // true enables MCP endpoint
```
@@ -25,17 +26,18 @@ server.serve(true).await?; // true enables MCP endpoint
| Path | Description |
|------|-------------|
| `/api/blocks/{height}` | Block info, transactions, status |
| `/api/txs/{txid}` | Transaction details, status, merkle proof |
| `/api/addresses/{addr}` | Address stats, transactions, UTXOs |
| `/api/block-height/{height}` | Block by height |
| `/api/block/{hash}` | Block info, transactions, status |
| `/api/tx/{txid}` | Transaction details, status, hex |
| `/api/address/{addr}` | Address stats, transactions, UTXOs |
| `/api/metrics` | Metric catalog and data queries |
| `/api/mining/*` | Hashrate, difficulty, pools, epochs |
| `/api/v1/mining/*` | Hashrate, difficulty, pools, rewards |
| `/api/mempool/*` | Fee estimates, projected blocks |
| `/mcp` | MCP endpoint (if enabled) |
## Caching
Uses ETag-based caching with stale-while-revalidate semantics:
Uses ETag-based caching with must-revalidate semantics:
- Height-indexed data: Cache until height changes
- Date-indexed data: Cache with longer TTL
- Mempool data: Short TTL, frequent updates
+19 -5
View File
@@ -1,8 +1,22 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
use std::{env, path::Path};
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
fn main() {
let is_dev = env::var("PROFILE").is_ok_and(|p| p == "debug");
// Generate importmap for website (updates index.html in place)
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let website_path = Path::new(&manifest_dir).join("../../website");
println!("cargo:rerun-if-changed=../../website");
if website_path.exists() {
// Skip importmap hashing in dev mode (files change often)
let map = if is_dev {
importmap::ImportMap::empty()
} else {
importmap::ImportMap::scan(&website_path, "").unwrap_or_else(|_| importmap::ImportMap::empty())
};
let _ = map.update_html_file(&website_path.join("index.html"));
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_server::Server;
use brk_server::{Server, WebsiteSource};
use tracing::info;
use vecdb::Exit;
@@ -54,7 +54,7 @@ fn run() -> Result<()> {
// Option 1: block_on to run and properly propagate errors
runtime.block_on(async move {
let server = Server::new(&query, outputs_dir, None);
let server = Server::new(&query, outputs_dir, WebsiteSource::Disabled);
let handle = tokio::spawn(async move { server.serve(true).await });
+159 -104
View File
@@ -10,121 +10,63 @@ use axum::{
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use brk_error::{Error, Result};
use brk_error::Result;
use quick_cache::sync::GuardResult;
use tracing::{error, info};
use crate::{AppState, HeaderMapExtended, ModifiedState, ResponseExtended};
use crate::{
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
};
pub async fn file_handler(
headers: HeaderMap,
State(state): State<AppState>,
path: extract::Path<String>,
) -> Response {
any_handler(headers, state, Some(path))
any_handler(headers, state, Some(path.0))
}
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
any_handler(headers, state, None)
}
fn any_handler(
headers: HeaderMap,
state: AppState,
path: Option<extract::Path<String>>,
) -> Response {
let files_path = state.files_path.as_ref().unwrap();
if let Some(path) = path.as_ref() {
// Sanitize path components to prevent traversal attacks
let sanitized: String = path
.0
.split('/')
.filter(|component| !component.is_empty() && *component != "." && *component != "..")
.collect::<Vec<_>>()
.join("/");
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
// Allow paths within files_path OR within project root (2 levels up)
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs (e.g., foo.abc12345.js -> foo.js)
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path_to_response(&headers, &state, &path)
} else {
path_to_response(&headers, &state, &files_path.join("index.html"))
}
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
match &state.website {
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
WebsiteSource::Embedded => embedded_handler(&state, path),
WebsiteSource::Filesystem(files_path) => {
filesystem_handler(headers, &state, files_path, path)
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
/// Sanitize path to prevent traversal attacks
fn sanitize_path(path: &str) -> String {
path.split('/')
.filter(|c| !c.is_empty() && *c != "." && *c != "..")
.collect::<Vec<_>>()
.join("/")
}
let serialized_path = path.to_str().unwrap();
/// Check if path requires revalidation (HTML files, service worker)
fn must_revalidate(path: &Path) -> bool {
path.extension().is_some_and(|ext| ext == "html")
|| path
.to_str()
.is_some_and(|p| p.ends_with("service-worker.js"))
}
let must_revalidate = path
.extension()
.is_some_and(|extension| extension == "html")
|| serialized_path.ends_with("service-worker.js");
/// Build response with proper headers and caching
fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &str) -> Response {
let must_revalidate = must_revalidate(path);
// Use cache for non-HTML files in release mode
let guard_res = if !cfg!(debug_assertions) && !must_revalidate {
Some(state.cache.get_value_or_guard(
&path.to_str().unwrap().to_owned(),
Some(Duration::from_millis(50)),
))
Some(
state
.cache
.get_value_or_guard(&cache_key.to_owned(), Some(Duration::from_millis(50))),
)
} else {
None
};
@@ -132,19 +74,10 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
let mut response = if let Some(GuardResult::Value(v)) = guard_res {
Response::new(Body::from(v))
} else {
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
if let Some(GuardResult::Guard(g)) = guard_res {
g.insert(content.clone().into())
.map_err(|_| Error::QuickCacheError)?;
let _ = g.insert(content.clone().into());
}
Response::new(content.into())
Response::new(Body::from(content))
};
let headers = response.headers_mut();
@@ -157,7 +90,129 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
headers.insert_cache_control_immutable();
}
headers.insert_last_modified(date);
response
}
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
let path = path.unwrap_or_else(|| "index.html".to_string());
let sanitized = sanitize_path(&path);
// Try to get file, with importmap hash stripping and SPA fallback
let file = EMBEDDED_WEBSITE
.get_file(&sanitized)
.or_else(|| {
strip_importmap_hash(Path::new(&sanitized))
.and_then(|unhashed| EMBEDDED_WEBSITE.get_file(unhashed.to_str()?))
})
.or_else(|| {
// If no extension, serve index.html (SPA routing)
if Path::new(&sanitized).extension().is_none() {
EMBEDDED_WEBSITE.get_file("index.html")
} else {
None
}
});
let Some(file) = file else {
let mut response: Response<Body> =
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
};
build_response(
state,
Path::new(file.path()),
file.contents().to_vec(),
&file.path().to_string_lossy(),
)
}
fn filesystem_handler(
headers: HeaderMap,
state: &AppState,
files_path: &Path,
path: Option<String>,
) -> Response {
let path = if let Some(path) = path {
let sanitized = sanitize_path(&path);
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
// SPA fallback
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path
} else {
files_path.join("index.html")
};
path_to_response(&headers, state, &path)
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
let cache_key = path.to_str().unwrap();
let mut response = build_response(state, path, content, cache_key);
response.headers_mut().insert_last_modified(date);
Ok(response)
}
+4 -6
View File
@@ -1,21 +1,19 @@
use std::path::PathBuf;
use aide::axum::ApiRouter;
use axum::{response::Redirect, routing::get};
use super::AppState;
use super::{AppState, WebsiteSource};
mod file;
use file::{file_handler, index_handler};
pub trait FilesRoutes {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self;
fn add_files_routes(self, website: &WebsiteSource) -> Self;
}
impl FilesRoutes for ApiRouter<AppState> {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self {
if path.is_some() {
fn add_files_routes(self, website: &WebsiteSource) -> Self {
if website.is_enabled() {
self.route("/{*path}", get(file_handler))
.route("/", get(index_handler))
} else {
+21 -3
View File
@@ -15,11 +15,29 @@ use axum::{
use brk_error::Result;
use brk_mcp::route::mcp_router;
use brk_query::AsyncQuery;
use include_dir::{include_dir, Dir};
use quick_cache::sync::Cache;
use tokio::net::TcpListener;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::{error, info};
/// Embedded website assets
pub static EMBEDDED_WEBSITE: Dir = include_dir!("$CARGO_MANIFEST_DIR/../../website");
/// Source for serving the website
#[derive(Debug, Clone)]
pub enum WebsiteSource {
Disabled,
Embedded,
Filesystem(PathBuf),
}
impl WebsiteSource {
pub fn is_enabled(&self) -> bool {
!matches!(self, Self::Disabled)
}
}
mod api;
pub mod cache;
mod extended;
@@ -37,12 +55,12 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
impl Server {
pub fn new(query: &AsyncQuery, data_path: PathBuf, files_path: Option<PathBuf>) -> Self {
pub fn new(query: &AsyncQuery, data_path: PathBuf, website: WebsiteSource) -> Self {
Self(AppState {
client: query.client().clone(),
query: query.clone(),
data_path,
files_path,
website,
cache: Arc::new(Cache::new(5_000)),
started_at: jiff::Timestamp::now(),
started_instant: Instant::now(),
@@ -87,7 +105,7 @@ impl Server {
let vecs = state.query.inner().vecs();
let router = ApiRouter::new()
.add_api_routes()
.add_files_routes(state.files_path.as_ref())
.add_files_routes(&state.website)
.route(
"/discord",
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
+2 -2
View File
@@ -13,7 +13,7 @@ use quick_cache::sync::Cache;
use serde::Serialize;
use crate::{
CacheParams, CacheStrategy,
CacheParams, CacheStrategy, WebsiteSource,
extended::{ResponseExtended, ResultExtended},
};
@@ -22,7 +22,7 @@ pub struct AppState {
#[deref]
pub query: AsyncQuery,
pub data_path: PathBuf,
pub files_path: Option<PathBuf>,
pub website: WebsiteSource,
pub cache: Arc<Cache<String, Bytes>>,
pub client: Client,
pub started_at: Timestamp,