mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 09:30:00 -07:00
global: snapshot
This commit is contained in:
@@ -1,29 +1,30 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_interface::{Format, Output, Params};
|
||||
use brk_vecs::Stamp;
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::traits::{HeaderMapExtended, ResponseExtended};
|
||||
use crate::{HeaderMapExtended, ResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod bridge;
|
||||
|
||||
pub use bridge::*;
|
||||
|
||||
const MAX_WEIGHT: usize = 320_000;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<Params>,
|
||||
State(app_state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(headers, query, app_state) {
|
||||
match req_to_response_res(uri, headers, query, app_state) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
@@ -35,9 +36,12 @@ pub async fn handler(
|
||||
}
|
||||
|
||||
fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<Params>,
|
||||
AppState { interface, .. }: AppState,
|
||||
AppState {
|
||||
interface, cache, ..
|
||||
}: AppState,
|
||||
) -> Result<Response> {
|
||||
let vecs = interface.search(¶ms);
|
||||
|
||||
@@ -67,7 +71,7 @@ fn req_to_response_res(
|
||||
.first()
|
||||
.unwrap()
|
||||
.1
|
||||
.etag(Stamp::from(u64::from(interface.get_height())), to);
|
||||
.etag(Stamp::from(interface.get_height()), to);
|
||||
|
||||
if headers
|
||||
.get_if_none_match()
|
||||
@@ -76,17 +80,51 @@ fn req_to_response_res(
|
||||
return Ok(Response::new_not_modified());
|
||||
}
|
||||
|
||||
let output = interface.format(vecs, ¶ms.rest)?;
|
||||
let guard_res = cache.get_value_or_guard(
|
||||
&format!("{}{}{etag}", uri.path(), uri.query().unwrap_or("")),
|
||||
Some(Duration::from_millis(500)),
|
||||
);
|
||||
|
||||
let mut response = match output {
|
||||
Output::CSV(s) => s.into_response(),
|
||||
Output::TSV(s) => s.into_response(),
|
||||
Output::Json(v) => match v {
|
||||
brk_interface::Value::Single(v) => Json(v).into_response(),
|
||||
brk_interface::Value::List(v) => Json(v).into_response(),
|
||||
brk_interface::Value::Matrix(v) => Json(v).into_response(),
|
||||
},
|
||||
Output::MD(s) => s.into_response(),
|
||||
let mut response = if let GuardResult::Value(v) = guard_res {
|
||||
Response::new(Body::from(v))
|
||||
} else {
|
||||
match interface.format(vecs, ¶ms.rest)? {
|
||||
Output::CSV(s) => {
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
g.insert(s.clone().into())
|
||||
.map_err(|_| Error::QuickCacheError)?;
|
||||
}
|
||||
s.into_response()
|
||||
}
|
||||
Output::TSV(s) => {
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
g.insert(s.clone().into())
|
||||
.map_err(|_| Error::QuickCacheError)?;
|
||||
}
|
||||
s.into_response()
|
||||
}
|
||||
Output::MD(s) => {
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
g.insert(s.clone().into())
|
||||
.map_err(|_| Error::QuickCacheError)?;
|
||||
}
|
||||
s.into_response()
|
||||
}
|
||||
Output::Json(v) => {
|
||||
let json = match v {
|
||||
brk_interface::Value::Single(v) => serde_json::to_vec(&v)?,
|
||||
brk_interface::Value::List(v) => serde_json::to_vec(&v)?,
|
||||
brk_interface::Value::Matrix(v) => serde_json::to_vec(&v)?,
|
||||
};
|
||||
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
g.insert(json.clone().into())
|
||||
.map_err(|_| Error::QuickCacheError)?;
|
||||
}
|
||||
|
||||
json.into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let headers = response.headers_mut();
|
||||
@@ -1,110 +0,0 @@
|
||||
use std::{fs, io, path::Path};
|
||||
|
||||
use brk_interface::{Index, Interface};
|
||||
|
||||
use crate::{VERSION, Website};
|
||||
|
||||
const SCRIPTS: &str = "scripts";
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub trait Bridge {
|
||||
fn generate_bridge_file(&self, website: Website, websites_path: &Path) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl Bridge for Interface<'static> {
|
||||
fn generate_bridge_file(&self, website: Website, websites_path: &Path) -> io::Result<()> {
|
||||
if website.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = websites_path.join(website.to_folder_name());
|
||||
|
||||
if !fs::exists(&path)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = path.join(SCRIPTS);
|
||||
|
||||
fs::create_dir_all(&path)?;
|
||||
|
||||
let path = path.join(Path::new("vecid-to-indexes.js"));
|
||||
|
||||
let indexes = Index::all();
|
||||
|
||||
let mut contents = format!(
|
||||
"//
|
||||
// File auto-generated, any modifications will be overwritten
|
||||
//
|
||||
|
||||
export const VERSION = \"v{VERSION}\";
|
||||
|
||||
"
|
||||
);
|
||||
|
||||
contents += &indexes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i_of_i, i)| {
|
||||
// let lowered = i.to_string().to_lowercase();
|
||||
format!("/** @typedef {{{i_of_i}}} {i} */",)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
contents += &format!(
|
||||
"\n\n/** @typedef {{{}}} Index */\n",
|
||||
indexes
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
);
|
||||
|
||||
contents += "
|
||||
/** @typedef {ReturnType<typeof createIndexes>} Indexes */
|
||||
|
||||
export function createIndexes() {
|
||||
return {
|
||||
";
|
||||
|
||||
contents += &indexes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i_of_i, i)| {
|
||||
let lowered = i.to_string().to_lowercase();
|
||||
format!(" {lowered}: /** @satisfies {{{i}}} */ ({i_of_i}),",)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
contents += " };\n}\n";
|
||||
|
||||
contents += "
|
||||
/** @typedef {ReturnType<typeof createVecIdToIndexes>} VecIdToIndexes
|
||||
/** @typedef {keyof VecIdToIndexes} VecId */
|
||||
|
||||
/**
|
||||
* @returns {Record<any, number[]>}
|
||||
*/
|
||||
export function createVecIdToIndexes() {
|
||||
return {
|
||||
";
|
||||
|
||||
self.id_to_index_to_vec()
|
||||
.iter()
|
||||
.for_each(|(id, index_to_vec)| {
|
||||
let indexes = index_to_vec
|
||||
.keys()
|
||||
.map(|i| (*i as u8).to_string())
|
||||
// .map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
contents += &format!(" \"{id}\": [{indexes}],\n");
|
||||
});
|
||||
|
||||
contents += " };\n}\n";
|
||||
|
||||
fs::write(path, contents)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
http::HeaderMap,
|
||||
http::{HeaderMap, Uri},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
@@ -12,8 +12,6 @@ use super::AppState;
|
||||
mod explorer;
|
||||
mod interface;
|
||||
|
||||
pub use interface::Bridge;
|
||||
|
||||
pub trait ApiRoutes {
|
||||
fn add_api_routes(self) -> Self;
|
||||
}
|
||||
@@ -87,7 +85,8 @@ impl ApiRoutes for Router<AppState> {
|
||||
.route(
|
||||
"/api/vecs/{variant}",
|
||||
get(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(variant): Path<String>,
|
||||
Query(params_opt): Query<ParamsOpt>,
|
||||
state: State<AppState>|
|
||||
@@ -100,7 +99,7 @@ impl ApiRoutes for Router<AppState> {
|
||||
(index, split.collect::<Vec<_>>().join(TO_SEPARATOR)),
|
||||
params_opt,
|
||||
));
|
||||
interface::handler(headers, Query(params), state).await
|
||||
interface::handler(uri, headers, Query(params), state).await
|
||||
} else {
|
||||
"Bad variant".into_response()
|
||||
}
|
||||
@@ -127,22 +126,3 @@ impl ApiRoutes for Router<AppState> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// pub async fn variants_handler(State(app_state): State<AppState>) -> Response {
|
||||
// Json(
|
||||
// app_state
|
||||
// .query
|
||||
// .vec_trees
|
||||
// .index_to_id_to_vec
|
||||
// .iter()
|
||||
// .flat_map(|(index, id_to_vec)| {
|
||||
// let index_ser = index.serialize_long();
|
||||
// id_to_vec
|
||||
// .keys()
|
||||
// .map(|id| format!("{}-to-{}", index_ser, id))
|
||||
// .collect::<Vec<_>>()
|
||||
// })
|
||||
// .collect::<Vec<_>>(),
|
||||
// )
|
||||
// .into_response()
|
||||
// }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{fs, path::Path, time::Duration};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -6,13 +6,11 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_error::{Error, Result};
|
||||
use log::{error, info};
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
traits::{HeaderMapExtended, ModifiedState, ResponseExtended},
|
||||
};
|
||||
use crate::{AppState, HeaderMapExtended, ModifiedState, ResponseExtended};
|
||||
|
||||
pub async fn file_handler(
|
||||
headers: HeaderMap,
|
||||
@@ -31,12 +29,12 @@ fn any_handler(
|
||||
app_state: AppState,
|
||||
path: Option<extract::Path<String>>,
|
||||
) -> Response {
|
||||
let dist_path = app_state.dist_path();
|
||||
let files_path = app_state.path.as_ref().unwrap();
|
||||
|
||||
if let Some(path) = path.as_ref() {
|
||||
let path = path.0.replace("..", "").replace("\\", "");
|
||||
|
||||
let mut path = dist_path.join(&path);
|
||||
let mut path = files_path.join(&path);
|
||||
|
||||
if !path.exists() || path.is_dir() {
|
||||
if path.extension().is_some() {
|
||||
@@ -50,18 +48,18 @@ fn any_handler(
|
||||
|
||||
return response;
|
||||
} else {
|
||||
path = dist_path.join("index.html");
|
||||
path = files_path.join("index.html");
|
||||
}
|
||||
}
|
||||
|
||||
path_to_response(&headers, &path)
|
||||
path_to_response(&headers, &app_state, &path)
|
||||
} else {
|
||||
path_to_response(&headers, &dist_path.join("index.html"))
|
||||
path_to_response(&headers, &app_state, &files_path.join("index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_response(headers: &HeaderMap, path: &Path) -> Response {
|
||||
match path_to_response_(headers, path) {
|
||||
fn path_to_response(headers: &HeaderMap, app_state: &AppState, path: &Path) -> Response {
|
||||
match path_to_response_(headers, app_state, path) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
@@ -74,32 +72,51 @@ fn path_to_response(headers: &HeaderMap, path: &Path) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_response_(headers: &HeaderMap, path: &Path) -> Result<Response> {
|
||||
fn path_to_response_(headers: &HeaderMap, app_state: &AppState, path: &Path) -> Result<Response> {
|
||||
let (modified, date) = headers.check_if_modified_since(path)?;
|
||||
if 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 serialized_path = path.to_str().unwrap();
|
||||
|
||||
let mut response = Response::new(content.into());
|
||||
let must_revalidate = path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "html")
|
||||
|| serialized_path.ends_with("service-worker.js");
|
||||
|
||||
let guard_res = if !must_revalidate {
|
||||
Some(app_state.cache.get_value_or_guard(
|
||||
&path.to_str().unwrap().to_owned(),
|
||||
Some(Duration::from_millis(500)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
Response::new(content.into())
|
||||
};
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_content_type(path);
|
||||
|
||||
let serialized_path = path.to_str().unwrap();
|
||||
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "html")
|
||||
|| serialized_path.ends_with("service-worker.js")
|
||||
{
|
||||
if must_revalidate {
|
||||
headers.insert_cache_control_must_revalidate();
|
||||
} else if path.extension().is_some_and(|extension| {
|
||||
extension == "jpg"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod file;
|
||||
mod website;
|
||||
|
||||
use file::{file_handler, index_handler};
|
||||
pub use website::Website;
|
||||
|
||||
pub trait FilesRoutes {
|
||||
fn add_website_routes(self, website: Website) -> Self;
|
||||
fn add_files_routes(self, path: Option<&PathBuf>) -> Self;
|
||||
}
|
||||
|
||||
impl FilesRoutes for Router<AppState> {
|
||||
fn add_website_routes(self, website: Website) -> Self {
|
||||
if website.is_some() {
|
||||
fn add_files_routes(self, path: Option<&PathBuf>) -> Self {
|
||||
if path.is_some() {
|
||||
self.route("/{*path}", get(file_handler))
|
||||
.route("/", get(index_handler))
|
||||
} else {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
use clap_derive::ValueEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, ValueEnum)]
|
||||
pub enum Website {
|
||||
None,
|
||||
Default,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl Website {
|
||||
pub fn is_none(&self) -> bool {
|
||||
self == &Self::None
|
||||
}
|
||||
|
||||
pub fn is_some(&self) -> bool {
|
||||
!self.is_none()
|
||||
}
|
||||
|
||||
pub fn to_folder_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Custom => "custom",
|
||||
Self::Default => "default",
|
||||
Self::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,124 +3,57 @@
|
||||
#![doc = include_str!("../examples/main.rs")]
|
||||
#![doc = "```"]
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use api::{ApiRoutes, Bridge};
|
||||
use api::ApiRoutes;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
body::Body,
|
||||
body::{Body, Bytes},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
middleware::Next,
|
||||
routing::get,
|
||||
serve,
|
||||
};
|
||||
use brk_bundler::bundle;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_interface::Interface;
|
||||
use brk_logger::OwoColorize;
|
||||
use brk_mcp::route::MCPRoutes;
|
||||
use files::FilesRoutes;
|
||||
use log::{error, info};
|
||||
use owo_colors::OwoColorize;
|
||||
use quick_cache::sync::Cache;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
use tracing::Span;
|
||||
|
||||
mod api;
|
||||
mod extended;
|
||||
mod files;
|
||||
mod traits;
|
||||
|
||||
pub use files::Website;
|
||||
use tracing::Span;
|
||||
use extended::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
interface: &'static Interface<'static>,
|
||||
website: Website,
|
||||
websites_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn dist_path(&self) -> PathBuf {
|
||||
self.websites_path
|
||||
.as_ref()
|
||||
.expect("Should never reach here is websites_path is None")
|
||||
.join("dist")
|
||||
}
|
||||
path: Option<PathBuf>,
|
||||
cache: Arc<Cache<String, Bytes>>,
|
||||
}
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const DEV_PATH: &str = "../..";
|
||||
const WEBSITES: &str = "websites";
|
||||
|
||||
pub struct Server(AppState);
|
||||
|
||||
impl Server {
|
||||
pub fn new(
|
||||
indexer: Indexer,
|
||||
computer: Computer,
|
||||
website: Website,
|
||||
downloads_path: &Path,
|
||||
) -> Result<Self> {
|
||||
let indexer = Box::leak(Box::new(indexer));
|
||||
let computer = Box::leak(Box::new(computer));
|
||||
let interface = Box::leak(Box::new(Interface::build(indexer, computer)));
|
||||
|
||||
let websites_path = if website.is_some() {
|
||||
let websites_dev_path = Path::new(DEV_PATH).join(WEBSITES);
|
||||
|
||||
let websites_path = if fs::exists(&websites_dev_path)? {
|
||||
websites_dev_path
|
||||
} else {
|
||||
let downloaded_websites_path =
|
||||
downloads_path.join(format!("brk-{VERSION}")).join(WEBSITES);
|
||||
|
||||
if !fs::exists(&downloaded_websites_path)? {
|
||||
info!("Downloading websites from Github...");
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/bitcoinresearchkit/brk/archive/refs/tags/v{VERSION}.zip",
|
||||
);
|
||||
|
||||
let response = minreq::get(url).send()?;
|
||||
let bytes = response.as_bytes();
|
||||
let cursor = Cursor::new(bytes);
|
||||
|
||||
let mut zip = zip::ZipArchive::new(cursor).unwrap();
|
||||
|
||||
zip.extract(downloads_path).unwrap();
|
||||
}
|
||||
|
||||
downloaded_websites_path
|
||||
};
|
||||
|
||||
interface.generate_bridge_file(website, websites_path.as_path())?;
|
||||
|
||||
Some(websites_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self(AppState {
|
||||
interface,
|
||||
website,
|
||||
websites_path,
|
||||
}))
|
||||
pub fn new(interface: Interface<'static>, files_path: Option<PathBuf>) -> Self {
|
||||
Self(AppState {
|
||||
interface: Box::leak(Box::new(interface)),
|
||||
path: files_path,
|
||||
cache: Arc::new(Cache::new(10_000)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn serve(self, watch: bool, mcp: bool) -> Result<()> {
|
||||
pub async fn serve(self, mcp: bool) -> Result<()> {
|
||||
let state = self.0;
|
||||
|
||||
if let Some(websites_path) = state.websites_path.clone() {
|
||||
bundle(&websites_path, state.website.to_folder_name(), watch).await?;
|
||||
}
|
||||
|
||||
let compression_layer = CompressionLayer::new()
|
||||
.br(true)
|
||||
.deflate(true)
|
||||
@@ -162,7 +95,7 @@ impl Server {
|
||||
|
||||
let router = Router::new()
|
||||
.add_api_routes()
|
||||
.add_website_routes(state.website)
|
||||
.add_files_routes(state.path.as_ref())
|
||||
.add_mcp_routes(state.interface, mcp)
|
||||
.route("/version", get(Json(VERSION)))
|
||||
.with_state(state)
|
||||
|
||||
Reference in New Issue
Block a user