iota/iota-storage/src/util/config_util.rs
2026-07-25 18:23:36 +02:00

230 lines
6.5 KiB
Rust

use arc_swap::ArcSwap;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::OnceLock;
pub static CONFIG: Lazy<ArcSwap<IotaConfig>> =
Lazy::new(|| ArcSwap::new(Arc::new(IotaConfig::default())));
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IotaConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub iota_id: Option<u64>,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub web: WebSettings,
#[serde(skip_serializing_if = "Option::is_none")]
pub omikron_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub omikron_port: Option<u16>,
#[serde(skip_serializing)]
pub keyring: Option<String>,
#[serde(skip_serializing)]
pub public_key: Option<String>,
#[serde(skip_serializing)]
pub private_key: Option<String>,
#[serde(default = "default_read_receipts_enabled")]
pub read_receipts_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebMode {
Disabled,
Loopback,
Network,
}
impl Default for WebMode {
fn default() -> Self {
Self::Disabled
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSettings {
#[serde(default)]
pub mode: WebMode,
#[serde(default = "default_web_bind")]
pub bind: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default = "default_web_asset_dir")]
pub asset_dir: String,
pub certificate: Option<String>,
pub key: Option<String>,
#[serde(default)]
pub required: bool,
}
fn default_web_bind() -> String {
"127.0.0.1".into()
}
fn default_web_asset_dir() -> String {
String::new()
}
impl Default for WebSettings {
fn default() -> Self {
Self {
mode: WebMode::default(),
bind: default_web_bind(),
port: default_port(),
asset_dir: default_web_asset_dir(),
certificate: None,
key: None,
required: false,
}
}
}
const fn default_port() -> u16 {
1984
}
const fn default_read_receipts_enabled() -> bool {
true
}
impl Default for IotaConfig {
fn default() -> Self {
Self {
iota_id: None,
port: default_port(),
web: WebSettings::default(),
omikron_host: None,
omikron_port: None,
keyring: None,
public_key: None,
private_key: None,
read_receipts_enabled: default_read_receipts_enabled(),
}
}
}
pub fn load_config() {
load_config_from(&default_config_path());
}
/// Loading is intentionally side-effect free: a missing configuration means
/// documented defaults, not a newly-created file.
pub fn load_config_from(path: &Path) {
let s = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(error) => {
eprintln!("Failed to read {}: {error}", path.display());
return;
}
};
match serde_yaml::from_str::<IotaConfig>(&s) {
Ok(parsed) => {
CONFIG.store(Arc::new(parsed));
}
Err(e) => {
eprintln!("Failed to parse {}: {e}", path.display());
}
}
}
pub fn clear_config() {
CONFIG.store(Arc::new(IotaConfig::default()));
save_config();
}
pub fn save_config() {
save_config_to(&default_config_path());
}
pub fn save_config_to(path: &Path) {
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
if let Some(parent) = path.parent() {
if let Err(error) = fs::create_dir_all(parent) {
eprintln!(
"Cannot create configuration directory {}: {error}",
parent.display()
);
return;
}
}
let temporary = path.with_extension("yaml.tmp");
if let Err(error) = fs::write(&temporary, yaml).and_then(|_| fs::rename(&temporary, path)) {
eprintln!("Cannot save {}: {error}", path.display());
let _ = fs::remove_file(temporary);
}
}
}
fn default_config_path() -> PathBuf {
if let Some(path) = CONFIG_PATH.get() {
return path.clone();
}
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.config_file
}
pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
let mut cfg = IotaConfig::clone(&**CONFIG.load());
f(&mut cfg);
CONFIG.store(Arc::new(cfg));
save_config();
}
pub fn modify_config_value(key: &str, value: &str) -> Result<(), &'static str> {
match key {
"iota_id" => {
let parsed: u64 = value.parse().map_err(|_| "invalid iota_id")?;
modify_config(|cfg| cfg.iota_id = Some(parsed));
Ok(())
}
"port" => {
let parsed: u16 = value.parse().map_err(|_| "invalid port")?;
modify_config(|cfg| cfg.port = parsed);
Ok(())
}
"omikron_host" => {
let host = value.to_string();
modify_config(|cfg| cfg.omikron_host = Some(host));
Ok(())
}
"omikron_port" => {
let parsed: u16 = value.parse().map_err(|_| "invalid omikron_port")?;
modify_config(|cfg| cfg.omikron_port = Some(parsed));
Ok(())
}
"read_receipts_enabled" => {
let parsed: bool = value.parse().map_err(|_| "invalid boolean")?;
modify_config(|cfg| cfg.read_receipts_enabled = parsed);
Ok(())
}
"web.mode" => {
let mode = match value {
"disabled" => WebMode::Disabled,
"loopback" => WebMode::Loopback,
"network" => WebMode::Network,
_ => return Err("invalid web.mode; use disabled, loopback, or network"),
};
modify_config(|cfg| cfg.web.mode = mode);
Ok(())
}
"web.port" => {
let parsed: u16 = value.parse().map_err(|_| "invalid web.port")?;
modify_config(|cfg| cfg.web.port = parsed);
Ok(())
}
"web.bind" => {
let bind = value.to_string();
modify_config(|cfg| cfg.web.bind = bind);
Ok(())
}
_ => Err("unknown config key"),
}
}
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
pub fn configure_config_path(path: PathBuf) {
let _ = CONFIG_PATH.set(path);
}