use std::{
env, fs, io,
net::IpAddr,
path::{Path, PathBuf},
};
use bitview::Config as RunnerConfig;
use bitview_server::{
CdnCacheMode, DEFAULT_BIND, DEFAULT_MAX_UTXOS, DEFAULT_MAX_WEIGHT, ServerConfig, Website,
};
use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use brk_types::Port;
use lexopt::{
Arg::{Long, Short},
ValueExt,
};
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};
use crate::paths::{default_bitview_dir, fix_user_path};
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
bitviewdir: Option<String>,
#[serde(default)]
serverbind: Option<IpAddr>,
#[serde(default)]
serverport: Option<Port>,
#[serde(default)]
website: Option<Website>,
#[serde(default)]
cdn: Option<bool>,
#[serde(default)]
maxweight: Option<usize>,
#[serde(default)]
maxutxos: Option<usize>,
#[serde(default)]
bitcoindir: Option<String>,
#[serde(default)]
blocksdir: Option<String>,
#[serde(default)]
rpcconnect: Option<String>,
#[serde(default)]
rpcport: Option<u16>,
#[serde(default)]
rpccookiefile: Option<String>,
#[serde(default)]
rpcuser: Option<String>,
#[serde(default)]
rpcpassword: Option<String>,
}
impl Config {
pub fn import() -> Result<RunnerConfig> {
let config_args = Self::parse_args();
let config_dir = default_bitview_dir();
fs::create_dir_all(&config_dir)?;
let path = config_dir.join("config.toml");
let mut config = Self::read(&path);
if let Some(v) = config_args.bitviewdir {
config.bitviewdir = Some(v);
}
if let Some(v) = config_args.serverbind {
config.serverbind = Some(v);
}
if let Some(v) = config_args.serverport {
config.serverport = Some(v);
}
if let Some(v) = config_args.website {
config.website = Some(v);
}
if let Some(v) = config_args.cdn {
config.cdn = Some(v);
}
if let Some(v) = config_args.maxweight {
config.maxweight = Some(v);
}
if let Some(v) = config_args.maxutxos {
config.maxutxos = Some(v);
}
if let Some(v) = config_args.bitcoindir {
config.bitcoindir = Some(v);
}
if let Some(v) = config_args.blocksdir {
config.blocksdir = Some(v);
}
if let Some(v) = config_args.rpcconnect {
config.rpcconnect = Some(v);
}
if let Some(v) = config_args.rpcport {
config.rpcport = Some(v);
}
if let Some(v) = config_args.rpccookiefile {
config.rpccookiefile = Some(v);
}
if let Some(v) = config_args.rpcuser {
config.rpcuser = Some(v);
}
if let Some(v) = config_args.rpcpassword {
config.rpcpassword = Some(v);
}
let data_path = config.bitviewdir();
config.check();
fs::create_dir_all(&data_path)?;
config.runner(data_path)
}
fn parse_args() -> Self {
let mut config = Self::default();
let mut parser = lexopt::Parser::from_env();
let command = Self::command_name();
while let Some(arg) = parser.next().unwrap() {
match arg {
Short('h') | Long("help") => {
Self::print_help(&command);
std::process::exit(0);
}
Short('V') | Long("version") => {
println!("{command} {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
Long("bitviewdir") => {
config.bitviewdir = Some(parser.value().unwrap().parse().unwrap())
}
Long("serverbind") => {
config.serverbind = Some(parser.value().unwrap().parse().unwrap())
}
Long("serverport") => {
config.serverport = Some(parser.value().unwrap().parse().unwrap())
}
Long("website") => config.website = Some(parser.value().unwrap().parse().unwrap()),
Long("cdn") => config.cdn = Some(parser.value().unwrap().parse().unwrap()),
Long("maxweight") => {
config.maxweight = Some(parser.value().unwrap().parse().unwrap())
}
Long("maxutxos") => {
config.maxutxos = Some(parser.value().unwrap().parse().unwrap())
}
Long("bitcoindir") => {
config.bitcoindir = Some(parser.value().unwrap().parse().unwrap())
}
Long("blocksdir") => {
config.blocksdir = Some(parser.value().unwrap().parse().unwrap())
}
Long("rpcconnect") => {
config.rpcconnect = Some(parser.value().unwrap().parse().unwrap())
}
Long("rpcport") => config.rpcport = Some(parser.value().unwrap().parse().unwrap()),
Long("rpccookiefile") => {
config.rpccookiefile = Some(parser.value().unwrap().parse().unwrap())
}
Long("rpcuser") => config.rpcuser = Some(parser.value().unwrap().parse().unwrap()),
Long("rpcpassword") => {
config.rpcpassword = Some(parser.value().unwrap().parse().unwrap())
}
_ => {
eprintln!("{}", arg.unexpected());
std::process::exit(1);
}
}
}
config
}
fn command_name() -> String {
env::args_os()
.next()
.and_then(|path| {
Path::new(&path)
.file_name()
.and_then(|name| name.to_str())
.map(str::to_owned)
})
.unwrap_or_else(|| "bitviewd".to_owned())
}
fn print_help(command: &str) {
let v = env!("CARGO_PKG_VERSION");
println!("{} {}", command.bold(), v.bright_black());
println!("Self-hosted Bitcoin data platform built on BRK");
println!();
println!("{}", "USAGE:".bold());
println!(
" {} {command} {}",
"[ENV]".bright_black(),
"[OPTIONS]".bright_black()
);
println!();
println!("{}", "OPTIONS:".bold());
println!(" -h, --help Print help");
println!(" -V, --version Print version");
println!();
println!(
" --bitviewdir {} Output directory {}",
"<PATH>".bright_black(),
"[~/.bitview]".bright_black()
);
println!(
" --serverbind {} Server bind address {}",
"<IP>".bright_black(),
format!("[{DEFAULT_BIND}]").bright_black()
);
println!(
" --serverport {} Server port {}",
"<PORT>".bright_black(),
format!("[{}]", Port::DEFAULT).bright_black()
);
println!(
" --website {} Website {}",
"<BOOL|PATH>".bright_black(),
"[true]".bright_black()
);
println!(
" --cdn {} Aggressive CDN cache, requires purge on deploy {}",
"<BOOL>".bright_black(),
"[false]".bright_black()
);
println!(
" --maxweight {} Server cap on series response weight in bytes; rejects /api/{{series,metric}}/... over the limit {}",
"<BYTES>".bright_black(),
format!("[{}]", DEFAULT_MAX_WEIGHT).bright_black()
);
println!(
" --maxutxos {} Server cap on UTXOs per address; /api/address/{{addr}}/utxo errors past the limit {}",
"<COUNT>".bright_black(),
format!("[{}]", DEFAULT_MAX_UTXOS).bright_black()
);
println!();
println!(
" --bitcoindir {} Bitcoin directory {}",
"<PATH>".bright_black(),
"[OS default]".bright_black()
);
println!(
" --blocksdir {} Blocks directory {}",
"<PATH>".bright_black(),
"[<bitcoindir>/blocks]".bright_black()
);
println!();
println!(
" --rpcconnect {} RPC host {}",
"<IP>".bright_black(),
"[localhost]".bright_black()
);
println!(
" --rpcport {} RPC port {}",
"<PORT>".bright_black(),
"[8332]".bright_black()
);
println!(
" --rpccookiefile {} RPC cookie file {}",
"<PATH>".bright_black(),
"[<bitcoindir>/.cookie]".bright_black()
);
println!(
" --rpcuser {} RPC username",
"<USERNAME>".bright_black()
);
println!(
" --rpcpassword {} RPC password",
"<PASSWORD>".bright_black()
);
println!();
println!("{}", "ENVIRONMENT:".bold());
println!(
" LOG={} Log level {}",
"<LEVEL>".bright_black(),
"[info]".bright_black()
);
println!(
" RUST_LOG={} Full log filter",
"<RULES>".bright_black()
);
println!();
println!("{}", "CONFIG:".bold());
println!(
" Edit {} to persist settings:",
"~/.bitview/config.toml".bright_black()
);
println!(" {}", "bitviewdir = \"/path/to/data\"".bright_black());
println!(
" {}",
"bitcoindir = \"/path/to/.bitcoin\"".bright_black()
);
}
fn check(&self) {
if !self.bitcoindir().is_dir() {
println!("{:?} isn't a valid directory", self.bitcoindir());
println!("Please use the --bitcoindir parameter to set a valid path.");
println!("Run the program with '-h' for help.");
std::process::exit(1);
}
if !self.blocksdir().is_dir() {
println!("{:?} isn't a valid directory", self.blocksdir());
println!("Please use the --blocksdir parameter to set a valid path.");
println!("Run the program with '-h' for help.");
std::process::exit(1);
}
if self.rpc_auth().is_err() {
println!(
"Unsuccessful authentication with the RPC client.
First make sure that `bitcoind` is running. If it is then please either set --rpccookiefile or --rpcuser and --rpcpassword as the default values seemed to have failed.
Finally, you can run the program with '-h' for help."
);
std::process::exit(1);
}
}
fn read(path: &Path) -> Self {
let contents = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Config::default(),
Err(e) => {
eprintln!("Cannot read {}: {e}", path.display());
std::process::exit(1);
}
};
toml::from_str(&contents).unwrap_or_else(|e| {
eprintln!("Invalid {}:\n{e}", path.display());
std::process::exit(1);
})
}
fn rpc(&self) -> Result<Client> {
Client::new(
&format!(
"http://{}:{}",
self.rpcconnect().unwrap_or(&"localhost".to_string()),
self.rpcport().unwrap_or(8332)
),
self.rpc_auth()?,
)
}
fn rpc_auth(&self) -> Result<Auth> {
let cookie = self.path_cookiefile();
if cookie.is_file() {
Ok(Auth::CookieFile(cookie))
} else if self.rpcuser.is_some() && self.rpcpassword.is_some() {
Ok(Auth::UserPass(
self.rpcuser.clone().unwrap(),
self.rpcpassword.clone().unwrap(),
))
} else {
Err(Error::AuthFailed)
}
}
fn rpcconnect(&self) -> Option<&String> {
self.rpcconnect.as_ref()
}
fn rpcport(&self) -> Option<u16> {
self.rpcport
}
fn bitcoindir(&self) -> PathBuf {
self.bitcoindir
.as_ref()
.map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref()))
}
fn blocksdir(&self) -> PathBuf {
self.blocksdir.as_ref().map_or_else(
|| self.bitcoindir().join("blocks"),
|blocksdir| fix_user_path(blocksdir.as_str()),
)
}
fn bitviewdir(&self) -> PathBuf {
self.bitviewdir
.as_ref()
.map_or_else(default_bitview_dir, |s| fix_user_path(s.as_ref()))
}
fn path_cookiefile(&self) -> PathBuf {
self.rpccookiefile.as_ref().map_or_else(
|| self.bitcoindir().join(".cookie"),
|p| fix_user_path(p.as_str()),
)
}
fn website(&self) -> Website {
self.website.clone().unwrap_or_default()
}
fn cdn_cache_mode(&self) -> CdnCacheMode {
if self.cdn.unwrap_or(false) {
CdnCacheMode::Aggressive
} else {
CdnCacheMode::Live
}
}
fn max_weight(&self) -> usize {
self.maxweight.unwrap_or(DEFAULT_MAX_WEIGHT)
}
fn max_utxos(&self) -> usize {
self.maxutxos.unwrap_or(DEFAULT_MAX_UTXOS)
}
fn serverbind(&self) -> IpAddr {
self.serverbind.unwrap_or(DEFAULT_BIND)
}
fn serverport(&self) -> Port {
self.serverport.unwrap_or_default()
}
fn runner(&self, data_path: PathBuf) -> Result<RunnerConfig> {
Ok(RunnerConfig {
client: self.rpc()?,
blocks_path: self.blocksdir(),
server: ServerConfig {
bind: self.serverbind(),
port: self.serverport(),
data_path,
website: self.website(),
cdn_cache_mode: self.cdn_cache_mode(),
max_weight: self.max_weight(),
max_utxos: self.max_utxos(),
},
})
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
#[test]
fn server_listener_is_typed_and_defaults_are_centralized() {
let defaults = Config::default();
assert_eq!(defaults.serverbind(), DEFAULT_BIND);
assert_eq!(defaults.serverport(), Port::DEFAULT);
let config: Config = toml::from_str(
r#"
serverbind = "127.0.0.1"
serverport = 3111
"#,
)
.unwrap();
assert_eq!(config.serverbind(), IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(config.serverport(), Port::new(3111));
}
}