#![allow(trivial_numeric_casts)]
use crate::Result;
use log::{debug, Level};
use serde::{Deserialize, Serialize};
use sn_routing::TransportConfig as NetworkConfig;
use std::{
fs::{self, File},
io::{self, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
};
use structopt::StructOpt;
use unwrap::unwrap;
const CONFIG_FILE: &str = "node.config";
const CONNECTION_INFO_FILE: &str = "node_connection_info.config";
const DEFAULT_ROOT_DIR_NAME: &str = "root_dir";
const DEFAULT_MAX_CAPACITY: u64 = 2 * 1024 * 1024 * 1024;
const ARGS: [&str; 17] = [
"wallet-id",
"max-capacity",
"root-dir",
"verbose",
"hard-coded-contacts",
"port",
"ip",
"max-msg-size-allowed",
"idle-timeout-msec",
"keep-alive-interval-msec",
"first",
"completions",
"log-dir",
"update",
"update-only",
"upnp-lease-duration",
"local",
];
#[derive(Default, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, StructOpt)]
#[structopt(rename_all = "kebab-case", bin_name = "sn_node")]
#[structopt(global_settings = &[structopt::clap::AppSettings::ColoredHelp])]
pub struct Config {
#[structopt(short, long, parse(try_from_str))]
wallet_id: Option<String>,
#[structopt(short, long)]
max_capacity: Option<u64>,
#[structopt(short, long, parse(from_os_str))]
root_dir: Option<PathBuf>,
#[structopt(short, long, parse(from_occurrences))]
verbose: u64,
#[structopt(short, long)]
local: bool,
#[structopt(short, long)]
first: bool,
#[structopt(flatten)]
#[allow(missing_docs)]
network_config: NetworkConfig,
#[structopt(long)]
completions: Option<String>,
#[structopt(long)]
log_dir: Option<PathBuf>,
#[structopt(long)]
update: bool,
#[structopt(long, name = "update-only")]
update_only: bool,
}
impl Config {
pub fn new() -> Self {
let mut config = match Self::read_from_file() {
Ok(Some(config)) => config,
Ok(None) | Err(_) => Default::default(),
};
let command_line_args = Config::clap().get_matches();
for arg in &ARGS {
let occurrences = command_line_args.occurrences_of(arg);
if occurrences != 0 {
if let Some(cla) = command_line_args.value_of(arg) {
config.set_value(arg, cla);
} else {
config.set_flag(arg, occurrences);
}
}
}
config
}
pub fn wallet_id(&self) -> Option<&String> {
self.wallet_id.as_ref()
}
pub fn is_first(&self) -> bool {
self.first
}
pub fn is_local(&self) -> bool {
self.local
}
pub fn max_capacity(&self) -> u64 {
self.max_capacity.unwrap_or(DEFAULT_MAX_CAPACITY)
}
pub fn root_dir(&self) -> Result<PathBuf> {
Ok(match &self.root_dir {
Some(root_dir) => root_dir.clone(),
None => project_dirs()?.join(DEFAULT_ROOT_DIR_NAME),
})
}
pub fn set_root_dir<P: Into<PathBuf>>(&mut self, path: P) {
self.root_dir = Some(path.into())
}
pub fn set_log_dir<P: Into<PathBuf>>(&mut self, path: P) {
self.log_dir = Some(path.into())
}
pub fn verbose(&self) -> Level {
match self.verbose {
0 => Level::Error,
1 => Level::Warn,
2 => Level::Info,
3 => Level::Debug,
_ => Level::Trace,
}
}
pub fn network_config(&self) -> &NetworkConfig {
&self.network_config
}
pub fn set_network_config(&mut self, config: NetworkConfig) {
self.network_config = config;
}
pub fn completions(&self) -> &Option<String> {
&self.completions
}
pub fn log_dir(&self) -> &Option<PathBuf> {
&self.log_dir
}
pub fn update(&self) -> bool {
self.update
}
pub fn update_only(&self) -> bool {
self.update_only
}
pub fn listen_on_loopback(&mut self) {
self.network_config.ip = Some(IpAddr::V4(Ipv4Addr::LOCALHOST));
}
pub(crate) fn set_value(&mut self, arg: &str, value: &str) {
if arg == ARGS[0] {
self.wallet_id = Some(value.parse().unwrap());
} else if arg == ARGS[1] {
self.max_capacity = Some(value.parse().unwrap());
} else if arg == ARGS[2] {
self.root_dir = Some(value.parse().unwrap());
} else if arg == ARGS[3] {
self.verbose = value.parse().unwrap();
} else if arg == ARGS[4] {
self.network_config.hard_coded_contacts = unwrap!(serde_json::from_str(value));
} else if arg == ARGS[5] {
self.network_config.port = Some(value.parse().unwrap());
} else if arg == ARGS[6] {
self.network_config.ip = Some(value.parse().unwrap());
} else if arg == ARGS[11] {
self.completions = Some(value.parse().unwrap());
} else if arg == ARGS[12] {
self.log_dir = Some(value.parse().unwrap());
} else if arg == ARGS[7] {
self.network_config.max_msg_size_allowed = Some(value.parse().unwrap());
} else if arg == ARGS[8] {
self.network_config.idle_timeout_msec = Some(value.parse().unwrap());
} else if arg == ARGS[9] {
self.network_config.keep_alive_interval_msec = Some(value.parse().unwrap());
} else if arg == ARGS[15] {
self.network_config.upnp_lease_duration = Some(value.parse().unwrap());
} else {
println!("ERROR");
}
}
pub(crate) fn set_flag(&mut self, arg: &str, occurrences: u64) {
if arg == ARGS[3] {
self.verbose = occurrences;
} else if arg == ARGS[10] {
self.first = occurrences >= 1;
} else if arg == ARGS[13] {
self.update = occurrences >= 1;
} else if arg == ARGS[14] {
self.update_only = occurrences >= 1;
} else if arg == ARGS[16] {
self.local = occurrences >= 1;
} else {
println!("ERROR");
}
}
fn read_from_file() -> Result<Option<Config>> {
let path = project_dirs()?.join(CONFIG_FILE);
match File::open(&path) {
Ok(file) => {
debug!("Reading settings from {}", path.display());
let reader = BufReader::new(file);
let config = serde_json::from_reader(reader)?;
Ok(config)
}
Err(error) => {
if error.kind() == std::io::ErrorKind::NotFound {
debug!("No config file available at {}", path.display());
Ok(None)
} else {
Err(error.into())
}
}
}
}
#[cfg(test)]
#[allow(dead_code)]
pub fn write_config_file(&self) -> Result<PathBuf> {
write_file(CONFIG_FILE, self)
}
}
pub fn write_connection_info(peer_addr: &SocketAddr) -> Result<PathBuf> {
write_file(CONNECTION_INFO_FILE, peer_addr)
}
fn write_file<T: ?Sized>(file: &str, config: &T) -> Result<PathBuf>
where
T: Serialize,
{
let project_dirs = project_dirs()?;
fs::create_dir_all(project_dirs.clone())?;
let path = project_dirs.join(file);
let mut file = File::create(&path)?;
serde_json::to_writer_pretty(&mut file, config)?;
file.sync_all()?;
Ok(path)
}
fn project_dirs() -> Result<PathBuf> {
let mut home_dir = dirs_next::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?;
home_dir.push(".safe");
home_dir.push("node");
Ok(home_dir)
}
#[cfg(test)]
mod test {
use super::Config;
use super::ARGS;
use std::{fs::File, io::Read, path::Path};
use structopt::StructOpt;
use unwrap::unwrap;
#[test]
fn smoke() {
let app_name = Config::clap().get_name().to_string();
let test_values = [
["wallet-id", "86a23e052dd07f3043f5b98e3add38764d7384f105a25eddbce62f3e02ac13467ff4565ff31bd3f1801d86e2ef79c103"],
["max-capacity", "1"],
["root-dir", "dir"],
["verbose", "None"],
["hard-coded-contacts", "[\"127.0.0.1:33292\"]"],
["port", "1"],
["ip", "127.0.0.1"],
["max-msg-size-allowed", "1"],
["idle-timeout-msec", "1"],
["keep-alive-interval-msec", "1"],
["first", "None"],
["completions", "bash"],
["log-dir", "log-dir-path"],
["update", "None"],
["update-only", "None"],
["local", "None"],
["upnp-lease-duration", "180"],
];
for arg in &ARGS {
let user_arg = format!("--{}", arg);
let value = test_values
.iter()
.find(|elt| &elt[0] == arg)
.unwrap_or_else(|| panic!("Missing arg: {:?}", &arg))[1];
let matches = if value == "None" {
Config::clap().get_matches_from(&[app_name.as_str(), user_arg.as_str()])
} else {
Config::clap().get_matches_from(&[app_name.as_str(), user_arg.as_str(), value])
};
let occurrences = matches.occurrences_of(arg);
assert_eq!(1, occurrences);
let mut config = Config {
local: false,
wallet_id: None,
max_capacity: None,
root_dir: None,
verbose: 0,
network_config: Default::default(),
first: false,
completions: None,
log_dir: None,
update: false,
update_only: false,
};
let empty_config = config.clone();
if let Some(val) = matches.value_of(arg) {
config.set_value(arg, val);
} else {
config.set_flag(arg, occurrences);
}
assert_ne!(empty_config, config, "Failed to set_value() for {}", arg);
}
}
#[ignore]
#[test]
fn parse_sample_config_file() {
let path = Path::new("installer/common/sample.node.config").to_path_buf();
let mut file = unwrap!(File::open(&path), "Error opening {}:", path.display());
let mut encoded_contents = String::new();
let _ = unwrap!(
file.read_to_string(&mut encoded_contents),
"Error reading {}:",
path.display()
);
let config: Config = unwrap!(
serde_json::from_str(&encoded_contents),
"Error parsing {} as JSON:",
path.display()
);
assert!(
config.wallet_id.is_some(),
"{} is missing `wallet_id` field.",
path.display()
);
assert!(
config.max_capacity.is_some(),
"{} is missing `max_capacity` field.",
path.display()
);
assert!(
config.root_dir.is_some(),
"{} is missing `root_dir` field.",
path.display()
);
}
}