server: snapshot

This commit is contained in:
nym21
2026-01-12 12:34:30 +01:00
parent 1b9e18f98b
commit b12a72ea1a
24 changed files with 3619 additions and 5378 deletions
@@ -2,7 +2,7 @@
use std::fmt::Write;
use crate::{Endpoint, Parameter, generators::{MANUAL_GENERIC_TYPES, write_description}, to_camel_case};
use crate::{Endpoint, Parameter, generators::{normalize_return_type, write_description}, to_camel_case};
/// Generate API methods for the BrkClient class.
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
@@ -130,15 +130,6 @@ fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
result
}
/// Replace generic types with their Any variants in return types.
fn normalize_return_type(return_type: &str) -> String {
let mut result = return_type.to_string();
for type_name in MANUAL_GENERIC_TYPES {
result = result.replace(type_name, &format!("Any{}", type_name));
}
result
}
/// Format param description with dash prefix, or empty string if no description.
fn format_param_desc(desc: Option<&str>) -> String {
match desc {
@@ -4,7 +4,7 @@ use std::fmt::Write;
use serde_json::Value;
use crate::{TypeSchemas, generators::MANUAL_GENERIC_TYPES, ref_to_type_name, to_camel_case};
use crate::{TypeSchemas, generators::{MANUAL_GENERIC_TYPES, write_description}, ref_to_type_name, to_camel_case};
/// Generate JSDoc type definitions from OpenAPI schemas.
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
@@ -26,7 +26,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
if is_primitive_alias(schema) {
if let Some(desc) = type_desc {
writeln!(output, "/**").unwrap();
write_jsdoc_description(output, desc);
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
writeln!(output, " */").unwrap();
@@ -36,7 +36,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
} else if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
writeln!(output, "/**").unwrap();
if let Some(desc) = type_desc {
write_jsdoc_description(output, desc);
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
}
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
@@ -64,7 +64,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
writeln!(output, " */").unwrap();
} else if let Some(desc) = type_desc {
writeln!(output, "/**").unwrap();
write_jsdoc_description(output, desc);
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
writeln!(output, " */").unwrap();
@@ -75,17 +75,6 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
writeln!(output).unwrap();
}
/// Write a multi-line description with proper JSDoc formatting.
fn write_jsdoc_description(output: &mut String, desc: &str) {
for line in desc.lines() {
if line.is_empty() {
writeln!(output, " *").unwrap();
} else {
writeln!(output, " * {}", line).unwrap();
}
}
}
fn is_primitive_alias(schema: &Value) -> bool {
schema.get("properties").is_none()
&& schema.get("items").is_none()
+10
View File
@@ -31,3 +31,13 @@ pub fn write_description(output: &mut String, desc: &str, prefix: &str, empty_pr
}
}
}
/// Replace generic types with their Any variants in return types.
/// Used by JS and Python generators.
pub fn normalize_return_type(return_type: &str) -> String {
let mut result = return_type.to_string();
for type_name in MANUAL_GENERIC_TYPES {
result = result.replace(type_name, &format!("Any{}", type_name));
}
result
}
@@ -2,7 +2,7 @@
use std::fmt::Write;
use crate::{Endpoint, Parameter, escape_python_keyword, generators::{MANUAL_GENERIC_TYPES, write_description}, to_snake_case};
use crate::{Endpoint, Parameter, escape_python_keyword, generators::{normalize_return_type, write_description}, to_snake_case};
use super::client::generate_class_constants;
use super::types::js_type_to_python;
@@ -187,12 +187,3 @@ fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
}
result
}
/// Replace generic types with their Any variants in return types.
fn normalize_return_type(return_type: &str) -> String {
let mut result = return_type.to_string();
for type_name in MANUAL_GENERIC_TYPES {
result = result.replace(type_name, &format!("Any{}", type_name));
}
result
}
+2 -1
View File
@@ -82,6 +82,7 @@ pub fn run() -> color_eyre::Result<()> {
let website = config.website();
let downloads_path = config.downloads_dir();
let data_path = config.brkdir();
let future = async move {
let bundle_path = if website.is_some() {
@@ -148,7 +149,7 @@ pub fn run() -> color_eyre::Result<()> {
}
}
let server = Server::new(&query, bundle_path);
let server = Server::new(&query, data_path, bundle_path);
tokio::spawn(async move {
server.serve(true).await.unwrap();
+731 -2028
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -3,6 +3,7 @@ use brk_error::Result;
use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_reader::Reader;
use brk_rpc::Client;
use tokio::task::spawn_blocking;
use crate::Query;
@@ -53,4 +54,8 @@ impl AsyncQuery {
pub fn inner(&self) -> &Query {
&self.0
}
pub fn client(&self) -> &Client {
self.0.client()
}
}
+14
View File
@@ -7,6 +7,7 @@ use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_reader::Reader;
use brk_rpc::Client;
use brk_types::Height;
use vecdb::AnyStoredVec;
@@ -32,6 +33,7 @@ pub use vecs::Vecs;
pub struct Query(Arc<QueryInner<'static>>);
struct QueryInner<'a> {
vecs: &'a Vecs<'a>,
client: Client,
reader: Reader,
indexer: &'a Indexer,
computer: &'a Computer,
@@ -45,6 +47,7 @@ impl Query {
computer: &Computer,
mempool: Option<Mempool>,
) -> Self {
let client = reader.client().clone();
let reader = reader.clone();
let indexer = Box::leak(Box::new(indexer.clone()));
let computer = Box::leak(Box::new(computer.clone()));
@@ -52,6 +55,7 @@ impl Query {
Self(Arc::new(QueryInner {
vecs,
client,
reader,
indexer,
computer,
@@ -69,6 +73,16 @@ impl Query {
&self.0.reader
}
#[inline]
pub fn client(&self) -> &Client {
&self.0.client
}
#[inline]
pub fn blocks_dir(&self) -> &std::path::Path {
self.0.reader.blocks_dir()
}
#[inline]
pub fn indexer(&self) -> &Indexer {
self.0.indexer
+5 -1
View File
@@ -6,7 +6,7 @@ use std::{
io::{Read, Seek, SeekFrom},
mem,
ops::ControlFlow,
path::PathBuf,
path::{Path, PathBuf},
sync::Arc,
thread,
time::Duration,
@@ -75,6 +75,10 @@ impl ReaderInner {
&self.client
}
pub fn blocks_dir(&self) -> &Path {
&self.blocks_dir
}
pub fn blk_index_to_blk_path(&self) -> RwLockReadGuard<'_, BlkIndexToBlkPath> {
self.blk_index_to_blk_path.read()
}
+1 -1
View File
@@ -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, None);
let server = Server::new(&query, outputs_dir, None);
let handle = tokio::spawn(async move { server.serve(true).await });
+9 -47
View File
@@ -1,25 +1,24 @@
use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;
use aide::{
axum::{ApiRouter, routing::get_with},
axum::ApiRouter,
openapi::OpenApi,
};
use axum::{
Extension, Json,
extract::State,
Extension,
http::HeaderMap,
response::{Html, Redirect, Response},
routing::get,
};
use brk_types::Health;
use crate::{
CacheStrategy, VERSION,
VERSION,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, transactions::TxRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
transactions::TxRoutes,
},
extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended},
extended::{HeaderMapExtended, ResponseExtended},
};
use super::AppState;
@@ -30,6 +29,7 @@ mod mempool;
mod metrics;
mod mining;
mod openapi;
mod server;
mod transactions;
pub use openapi::*;
@@ -46,46 +46,8 @@ impl ApiRoutes for ApiRouter<AppState> {
.add_mining_routes()
.add_tx_routes()
.add_metrics_routes()
.add_server_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route(
"/version",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, |_| {
Ok(env!("CARGO_PKG_VERSION"))
})
.await
},
|op| {
op.id("get_version")
.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)
.api_route(
"/health",
get_with(
async || -> Json<Health> {
Json(Health {
status: Cow::Borrowed("healthy"),
service: Cow::Borrowed("brk"),
timestamp: jiff::Timestamp::now().to_string(),
})
},
|op| {
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server")
.ok_response::<Health>()
},
),
)
.route(
"/api.json",
get(
+174
View File
@@ -0,0 +1,174 @@
use std::{borrow::Cow, fs, path};
use aide::axum::{ApiRouter, routing::get_with};
use axum::{extract::State, http::HeaderMap};
use brk_types::{DiskUsage, Health, Height, SyncStatus};
use vecdb::GenericStoredVec;
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
pub trait ServerRoutes {
fn add_server_routes(self) -> Self;
}
impl ServerRoutes for ApiRouter<AppState> {
fn add_server_routes(self) -> Self {
self.api_route(
"/api/server/sync",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let tip_height = state.client.get_last_height();
state
.cached_json(&headers, CacheStrategy::Height, move |q| {
let indexed_height = q.height();
let tip_height = tip_height?;
let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height));
let last_indexed_at_unix = q
.indexer()
.vecs
.blocks
.timestamp
.read_once(indexed_height)?;
Ok(SyncStatus {
indexed_height,
tip_height,
blocks_behind,
last_indexed_at: last_indexed_at_unix.to_iso8601(),
last_indexed_at_unix,
})
})
.await
},
|op| {
op.id("get_sync_status")
.server_tag()
.summary("Sync status")
.description(
"Returns the sync status of the indexer, including indexed height, \
tip height, blocks behind, and last indexed timestamp.",
)
.ok_response::<SyncStatus>()
.not_modified()
},
),
)
.api_route(
"/api/server/disk",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let brk_path = state.data_path.clone();
state
.cached_json(&headers, CacheStrategy::Height, move |q| {
let brk_bytes = dir_size(&brk_path)?;
let bitcoin_bytes = dir_size(q.blocks_dir())?;
Ok(DiskUsage::new(brk_bytes, bitcoin_bytes))
})
.await
},
|op| {
op.id("get_disk_usage")
.server_tag()
.summary("Disk usage")
.description(
"Returns the disk space used by the indexed data.",
)
.ok_response::<DiskUsage>()
.not_modified()
},
),
)
.api_route(
"/health",
get_with(
async |State(state): State<AppState>| -> axum::Json<Health> {
let uptime = state.started_instant.elapsed();
axum::Json(Health {
status: Cow::Borrowed("healthy"),
service: Cow::Borrowed("brk"),
timestamp: jiff::Timestamp::now().to_string(),
started_at: state.started_at.to_string(),
uptime_seconds: uptime.as_secs(),
})
},
|op| {
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server, including uptime information.")
.ok_response::<Health>()
},
),
)
.api_route(
"/version",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, |_| {
Ok(env!("CARGO_PKG_VERSION"))
})
.await
},
|op| {
op.id("get_version")
.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)
}
}
#[cfg(unix)]
fn dir_size(path: &path::Path) -> brk_error::Result<u64> {
use std::os::unix::fs::MetadataExt;
let mut total = 0u64;
if path.is_file() {
// blocks * 512 = actual disk usage (accounts for sparse files)
return Ok(fs::metadata(path)?.blocks() * 512);
}
let entries = fs::read_dir(path)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
total += dir_size(&path)?;
} else {
total += fs::metadata(&path)?.blocks() * 512;
}
}
Ok(total)
}
#[cfg(not(unix))]
fn dir_size(path: &path::Path) -> brk_error::Result<u64> {
let mut total = 0u64;
if path.is_file() {
return Ok(fs::metadata(path)?.len());
}
let entries = fs::read_dir(path)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
total += dir_size(&path)?;
} else {
total += fs::metadata(&path)?.len();
}
}
Ok(total)
}
+1 -1
View File
@@ -33,7 +33,7 @@ fn any_handler(
state: AppState,
path: Option<extract::Path<String>>,
) -> Response {
let files_path = state.path.as_ref().unwrap();
let files_path = state.files_path.as_ref().unwrap();
if let Some(path) = path.as_ref() {
// Sanitize path components to prevent traversal attacks
+8 -4
View File
@@ -1,6 +1,6 @@
#![doc = include_str!("../README.md")]
use std::{panic, path::PathBuf, sync::Arc, time::Duration};
use std::{panic, path::PathBuf, sync::Arc, time::{Duration, Instant}};
use aide::axum::ApiRouter;
use axum::{
@@ -37,11 +37,15 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
impl Server {
pub fn new(query: &AsyncQuery, files_path: Option<PathBuf>) -> Self {
pub fn new(query: &AsyncQuery, data_path: PathBuf, files_path: Option<PathBuf>) -> Self {
Self(AppState {
client: query.client().clone(),
query: query.clone(),
path: files_path,
data_path,
files_path,
cache: Arc::new(Cache::new(5_000)),
started_at: jiff::Timestamp::now(),
started_instant: Instant::now(),
})
}
@@ -83,7 +87,7 @@ impl Server {
let vecs = state.query.inner().vecs();
let router = ApiRouter::new()
.add_api_routes()
.add_files_routes(state.path.as_ref())
.add_files_routes(state.files_path.as_ref())
.route(
"/discord",
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
+8 -2
View File
@@ -1,4 +1,4 @@
use std::{path::PathBuf, sync::Arc};
use std::{path::PathBuf, sync::Arc, time::Instant};
use derive_more::Deref;
@@ -7,6 +7,8 @@ use axum::{
http::{HeaderMap, Response},
};
use brk_query::AsyncQuery;
use brk_rpc::Client;
use jiff::Timestamp;
use quick_cache::sync::Cache;
use serde::Serialize;
@@ -19,8 +21,12 @@ use crate::{
pub struct AppState {
#[deref]
pub query: AsyncQuery,
pub path: Option<PathBuf>,
pub data_path: PathBuf,
pub files_path: Option<PathBuf>,
pub cache: Arc<Cache<String, Bytes>>,
pub client: Client,
pub started_at: Timestamp,
pub started_instant: Instant,
}
impl AppState {
+53
View File
@@ -0,0 +1,53 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Disk usage of the indexed data
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiskUsage {
/// Human-readable brk data size (e.g., "48.8 GiB")
pub brk: String,
/// brk data size in bytes
pub brk_bytes: u64,
/// Human-readable Bitcoin blocks directory size
pub bitcoin: String,
/// Bitcoin blocks directory size in bytes
pub bitcoin_bytes: u64,
/// brk as percentage of Bitcoin data
pub ratio: f64,
}
impl DiskUsage {
pub fn new(brk_bytes: u64, bitcoin_bytes: u64) -> Self {
let ratio = if bitcoin_bytes > 0 {
brk_bytes as f64 / bitcoin_bytes as f64
} else {
0.0
};
Self {
brk: format_bytes(brk_bytes),
brk_bytes,
bitcoin: format_bytes(bitcoin_bytes),
bitcoin_bytes,
ratio,
}
}
}
fn format_bytes(bytes: u64) -> String {
const KIB: u64 = 1024;
const MIB: u64 = KIB * 1024;
const GIB: u64 = MIB * 1024;
const TIB: u64 = GIB * 1024;
if bytes >= TIB {
format!("{:.2} TiB", bytes as f64 / TIB as f64)
} else if bytes >= GIB {
format!("{:.2} GiB", bytes as f64 / GIB as f64)
} else if bytes >= MIB {
format!("{:.2} MiB", bytes as f64 / MIB as f64)
} else if bytes >= KIB {
format!("{:.2} KiB", bytes as f64 / KIB as f64)
} else {
format!("{} B", bytes)
}
}
+4
View File
@@ -9,4 +9,8 @@ pub struct Health {
pub status: Cow<'static, str>,
pub service: Cow<'static, str>,
pub timestamp: String,
/// Server start time (ISO 8601)
pub started_at: String,
/// Uptime in seconds
pub uptime_seconds: u64,
}
+4
View File
@@ -44,6 +44,7 @@ mod dateindex;
mod decadeindex;
mod deser;
mod difficultyadjustment;
mod diskusage;
mod difficultyadjustmententry;
mod difficultyentry;
mod difficultyepoch;
@@ -131,6 +132,7 @@ mod stored_u32;
mod stored_u64;
mod stored_u8;
mod supply_state;
mod syncstatus;
mod timeperiod;
mod timeperiodparam;
mod term;
@@ -206,6 +208,7 @@ pub use dateindex::*;
pub use decadeindex::*;
pub use deser::*;
pub use difficultyadjustment::*;
pub use diskusage::*;
pub use difficultyadjustmententry::*;
pub use difficultyentry::*;
pub use difficultyepoch::*;
@@ -293,6 +296,7 @@ pub use stored_u16::*;
pub use stored_u32::*;
pub use stored_u64::*;
pub use supply_state::*;
pub use syncstatus::*;
pub use term::*;
pub use timeperiod::*;
pub use timeperiodparam::*;
+19
View File
@@ -0,0 +1,19 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{Height, Timestamp};
/// Sync status of the indexer
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SyncStatus {
/// Height of the last indexed block
pub indexed_height: Height,
/// Height of the chain tip (from Bitcoin node)
pub tip_height: Height,
/// Number of blocks behind the tip
pub blocks_behind: Height,
/// Human-readable timestamp of the last indexed block (ISO 8601)
pub last_indexed_at: String,
/// Unix timestamp of the last indexed block
pub last_indexed_at_unix: Timestamp,
}
+5
View File
@@ -79,6 +79,11 @@ impl Timestamp {
pub fn now() -> Self {
Self::from(jiff::Timestamp::now())
}
/// Returns an ISO 8601 formatted string
pub fn to_iso8601(self) -> String {
jiff::Timestamp::from(self).to_string()
}
}
impl From<u32> for Timestamp {