use crate::{sessions::Session, Client, Error, DEFAULT_NETWORK_CONTACTS_FILE_NAME};
use qp2p::Config as Qp2pConfig;
use sn_dbc::Owner;
use sn_interface::{network_knowledge::SectionTree, types::Keypair};
use std::{
net::{Ipv4Addr, SocketAddr},
path::PathBuf,
str::FromStr,
sync::Arc,
time::Duration,
};
use tokio::sync::RwLock;
pub const ENV_QUERY_TIMEOUT: &str = "SN_QUERY_TIMEOUT";
pub const ENV_MAX_BACKOFF_INTERVAL: &str = "SN_MAX_BACKOFF_INTERVAL";
pub const ENV_CMD_TIMEOUT: &str = "SN_CMD_TIMEOUT";
pub const DEFAULT_LOCAL_ADDR: (Ipv4Addr, u16) = (Ipv4Addr::UNSPECIFIED, 0);
pub const DEFAULT_QUERY_CMD_TIMEOUT: Duration = Duration::from_secs(90);
pub const DEFAULT_MAX_QUERY_CMD_BACKOFF_INTERVAL: Duration = Duration::from_secs(3);
#[derive(Debug, Default)]
pub struct ClientBuilder {
keypair: Option<Keypair>,
dbc_owner: Option<Owner>,
local_addr: Option<SocketAddr>,
qp2p: Option<Qp2pConfig>,
query_timeout: Option<Duration>,
max_backoff_interval: Option<Duration>,
cmd_timeout: Option<Duration>,
network_contacts: Option<SectionTree>,
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn keypair(mut self, kp: impl Into<Option<Keypair>>) -> Self {
self.keypair = kp.into();
self
}
pub fn dbc_owner(mut self, owner: impl Into<Option<Owner>>) -> Self {
self.dbc_owner = owner.into();
self
}
pub fn local_addr(mut self, addr: impl Into<Option<SocketAddr>>) -> Self {
self.local_addr = addr.into();
self
}
pub fn qp2p(mut self, cfg: impl Into<Option<Qp2pConfig>>) -> Self {
self.qp2p = cfg.into();
self
}
pub fn query_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
self.query_timeout = timeout.into();
self
}
pub fn max_backoff_interval(
mut self,
max_backoff_interval: impl Into<Option<Duration>>,
) -> Self {
self.max_backoff_interval = max_backoff_interval.into();
self
}
pub fn cmd_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
self.cmd_timeout = timeout.into();
self
}
pub fn network_contacts(mut self, pm: impl Into<Option<SectionTree>>) -> Self {
self.network_contacts = pm.into();
self
}
pub fn from_env(mut self) -> Self {
if let Ok(Some(v)) = env_parse(ENV_QUERY_TIMEOUT) {
self.query_timeout = Some(Duration::from_secs(v));
}
if let Ok(Some(v)) = env_parse(ENV_MAX_BACKOFF_INTERVAL) {
self.max_backoff_interval = Some(Duration::from_secs(v));
}
if let Ok(Some(v)) = env_parse(ENV_CMD_TIMEOUT) {
self.cmd_timeout = Some(Duration::from_secs(v));
}
self
}
pub async fn build(self) -> Result<Client, Error> {
let max_backoff_interval = self
.max_backoff_interval
.unwrap_or(DEFAULT_MAX_QUERY_CMD_BACKOFF_INTERVAL);
let query_timeout = self.query_timeout.unwrap_or(DEFAULT_QUERY_CMD_TIMEOUT);
let cmd_timeout = self.cmd_timeout.unwrap_or(DEFAULT_QUERY_CMD_TIMEOUT);
let network_contacts = match self.network_contacts {
Some(pm) => pm,
None => {
let network_contacts_dir = default_network_contacts_path()?;
SectionTree::from_disk(&network_contacts_dir)
.await
.map_err(|err| Error::NetworkContacts(err.to_string()))?
}
};
let mut qp2p = self.qp2p.unwrap_or_default();
if qp2p.idle_timeout.is_none() {
let idle_timeout = if query_timeout > cmd_timeout {
query_timeout
} else {
cmd_timeout
};
qp2p.idle_timeout = Some(idle_timeout);
}
debug!("Session config: {:?}", qp2p);
let session = Session::new(
qp2p,
self.local_addr
.unwrap_or_else(|| SocketAddr::from(DEFAULT_LOCAL_ADDR)),
network_contacts,
)?;
let keypair = self.keypair.unwrap_or_else(Keypair::new_ed25519);
let dbc_owner = self
.dbc_owner
.unwrap_or_else(|| Owner::from_random_secret_key(&mut rand::thread_rng()));
let client = Client {
keypair,
dbc_owner,
session,
query_timeout,
max_backoff_interval,
cmd_timeout,
chunks_cache: Arc::new(RwLock::new(Default::default())),
};
client.connect().await?;
Ok(client)
}
}
fn env_parse<F: FromStr>(s: &str) -> Result<Option<F>, F::Err> {
match std::env::var(s) {
Ok(v) => F::from_str(&v).map(|v| Some(v)),
Err(_) => Ok(None),
}
}
fn default_network_contacts_path() -> Result<PathBuf, Error> {
let path = dirs_next::home_dir()
.ok_or_else(|| {
crate::Error::NetworkContacts("Could not read user's home directory".to_string())
})?
.join(".safe")
.join("network_contacts")
.join(DEFAULT_NETWORK_CONTACTS_FILE_NAME);
Ok(path)
}
#[cfg(test)]
mod tests {}