ckb-dev 0.1.1

A simple tool for CKB DevOps.
Documentation
use std::{net::IpAddr, path::PathBuf, str::FromStr as _};

use ini::Ini;
use url::Url;

use crate::{
    ckb_config::AppConfig as CkbConfig,
    error::{Error, Result},
};

const NORMAL_CONFIG_FILE: &str = "/etc/ckbdev.conf";
const SECRET_CONFIG_FILE: &str = "/etc/ckbdev.secret.conf";

pub struct Config {
    pub(crate) normal: NormalConfig,
    pub(crate) secret: SecretConfig,
}

pub(crate) struct NormalConfig {
    pub(crate) host: HostSection,
    pub(crate) ckb: CkbSection,
}

pub(crate) struct SecretConfig {
    pub(crate) qiniu: QiniuSection,
}

pub(crate) struct HostSection {
    #[allow(dead_code)]
    pub(crate) ip: IpAddr,
    pub(crate) name: String,
}

pub(crate) struct CkbSection {
    pub(crate) service_name: String,
    pub(crate) bin_path: PathBuf,
    pub(crate) root_dir: PathBuf,
    pub(crate) data_dir: PathBuf,
    pub(crate) rpc_url: Url,
}

pub(crate) struct QiniuSection {
    pub(crate) access_key: String,
    pub(crate) secret_key: String,
    pub(crate) bucket: String,
    pub(crate) domain: Url,
    pub(crate) path_prefix: String,
}

impl Config {
    pub fn load_from_files() -> Result<Self> {
        let normal = NormalConfig::load_from_file(NORMAL_CONFIG_FILE)?;
        let secret = SecretConfig::load_from_file(SECRET_CONFIG_FILE)?;
        Ok(Self { normal, secret })
    }
}

impl NormalConfig {
    pub(crate) fn load_from_file(path: &str) -> Result<Self> {
        let ini = Ini::load_from_file(path)
            .map_err(|err| Error::Cfg(format!("failed to load \"{}\" since {}", path, err)))?;
        let host = {
            let prop = ini
                .section(Some("host"))
                .ok_or_else(|| Error::config_not_found("host"))?;
            let ip = prop
                .get("ip")
                .ok_or_else(|| Error::config_not_found("host.ip"))
                .and_then(|s| {
                    IpAddr::from_str(s).map_err(|err| {
                        Error::Cfg(format!("failed to parse [host.ip] since {}", err))
                    })
                })?;
            let name = prop
                .get("name")
                .ok_or_else(|| Error::config_not_found("host.name"))?
                .to_owned();
            HostSection { ip, name }
        };
        let ckb = {
            let prop = ini
                .section(Some("ckb"))
                .ok_or_else(|| Error::config_not_found("ckb"))?;
            let service_name = prop
                .get("service_name")
                .ok_or_else(|| Error::config_not_found("ckb.service_name"))?
                .to_owned();
            let bin_path = prop
                .get("bin_path")
                .ok_or_else(|| Error::config_not_found("ckb.bin_path"))
                .map(PathBuf::from)?;
            let root_dir = prop
                .get("root_dir")
                .ok_or_else(|| Error::config_not_found("ckb.root_dir"))
                .map(PathBuf::from)?;
            let ckb_cfg = CkbConfig::load_from_workdir(&root_dir)?;
            let data_dir = root_dir.join(&ckb_cfg.data_dir);
            let rpc_url = {
                let url = format!("http://{}", ckb_cfg.rpc.listen_address);
                Url::parse(&url).map_err(|err| {
                    Error::Cfg(format!(
                        "failed to parse CKB RPC URL [{}] since {}",
                        url, err
                    ))
                })
            }?;
            CkbSection {
                service_name,
                bin_path,
                root_dir,
                data_dir,
                rpc_url,
            }
        };
        Ok(Self { host, ckb })
    }
}

impl SecretConfig {
    pub(crate) fn load_from_file(path: &str) -> Result<Self> {
        let ini = Ini::load_from_file(path)
            .map_err(|err| Error::Cfg(format!("failed to load \"{}\" since {}", path, err)))?;
        let qiniu = {
            let prop = ini
                .section(Some("qiniu"))
                .ok_or_else(|| Error::config_not_found("qiniu"))?;
            let access_key = prop
                .get("access_key")
                .ok_or_else(|| Error::config_not_found("qiniu.access_key"))?
                .to_owned();
            let secret_key = prop
                .get("secret_key")
                .ok_or_else(|| Error::config_not_found("qiniu.secret_key"))?
                .to_owned();
            let bucket = prop
                .get("bucket")
                .ok_or_else(|| Error::config_not_found("qiniu.bucket"))?
                .to_owned();
            let domain = prop
                .get("domain")
                .ok_or_else(|| Error::config_not_found("qiniu.domain"))
                .and_then(|s| {
                    let u = Url::parse(s).map_err(|err| {
                        Error::Cfg(format!("failed to parse [qiniu.domain] since {}", err))
                    })?;
                    if u.scheme() != "http" && u.scheme() != "https" {
                        let msg = "invalid [qiniu.domain], scheme should be \"http\" or \"https\"";
                        let err = Error::Cfg(msg.to_owned());
                        Err(err)
                    } else {
                        Ok(u)
                    }
                })?;
            let path_prefix = prop
                .get("path_prefix")
                .ok_or_else(|| Error::config_not_found("qiniu.path_prefix"))?
                .to_owned();
            QiniuSection {
                access_key,
                secret_key,
                bucket,
                domain,
                path_prefix,
            }
        };
        Ok(Self { qiniu })
    }
}