global: snapshot

This commit is contained in:
nym21
2025-08-03 23:38:58 +02:00
parent f7aa9424db
commit a2f5704581
50 changed files with 818 additions and 704 deletions

View File

@@ -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(&params);
@@ -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, &params.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, &params.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();

View File

@@ -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)
}
}

View File

@@ -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()
// }

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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!(),
}
}
}

View File

@@ -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)