use super::{
domain_port::DomainPort, quota_config::PathLimit, storage_config::StorageConfigToml, Domain,
SignupMode,
};
use crate::{
data_directory::log_level::{LogLevel, TargetLevel},
shared::toml_merge,
};
use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
fs,
net::{IpAddr, SocketAddr},
num::NonZeroU64,
path::Path,
str::FromStr,
};
use url::Url;
pub const DEFAULT_CONFIG: &str = include_str!("config.default.toml");
pub const SAMPLE_CONFIG: &str = include_str!("../../config.sample.toml");
#[derive(Debug, thiserror::Error)]
pub enum ConfigReadError {
#[error("config file not found: {0}")]
ConfigFileNotFound(#[from] std::io::Error),
#[error("config file is not valid TOML: {0}")]
ConfigFileNotValid(#[from] toml::de::Error),
#[error("failed to merge embedded and user TOML: {0}")]
ConfigMergeError(String),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PkdnsToml {
pub public_ip: IpAddr,
pub public_pubky_tls_port: Option<u16>,
pub public_icann_http_port: Option<u16>,
pub icann_domain: Option<Domain>,
pub user_keys_republisher_interval: u64,
pub dht_bootstrap_nodes: Option<Vec<DomainPort>>,
pub dht_relay_nodes: Option<Vec<Url>>,
pub dht_request_timeout_ms: Option<NonZeroU64>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct DriveToml {
pub pubky_listen_socket: SocketAddr,
pub icann_listen_socket: SocketAddr,
pub rate_limits: Vec<PathLimit>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AdminToml {
pub listen_socket: SocketAddr,
pub admin_password: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct GeneralToml {
pub signup_mode: SignupMode,
pub lmdb_backup_interval_s: u64,
pub user_storage_quota_mb: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct LoggingToml {
pub level: LogLevel,
pub module_levels: Vec<TargetLevel>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ConfigToml {
pub general: GeneralToml,
pub drive: DriveToml,
pub storage: StorageConfigToml,
pub admin: AdminToml,
pub pkdns: PkdnsToml,
pub logging: Option<LoggingToml>,
}
impl Default for ConfigToml {
fn default() -> Self {
ConfigToml::from_str(DEFAULT_CONFIG).expect("Embedded config.default.toml must be valid")
}
}
impl Default for DriveToml {
fn default() -> Self {
ConfigToml::default().drive
}
}
impl Default for AdminToml {
fn default() -> Self {
ConfigToml::default().admin
}
}
impl Default for PkdnsToml {
fn default() -> Self {
ConfigToml::default().pkdns
}
}
impl ConfigToml {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadError> {
let raw = fs::read_to_string(path)?;
Self::from_str_with_defaults(&raw)
}
pub fn from_str_with_defaults(raw: &str) -> Result<Self, ConfigReadError> {
let default_val: toml::Value = DEFAULT_CONFIG
.parse()
.expect("embedded defaults invalid TOML");
let user_val: toml::Value = raw.parse()?;
let merged_val = toml_merge::merge_with_options(default_val, user_val, true)
.map_err(|e| ConfigReadError::ConfigMergeError(e.to_string()))?;
Ok(merged_val.try_into()?)
}
pub fn sample_string() -> String {
SAMPLE_CONFIG
.lines()
.map(|line| {
let trimmed = line.trim_start();
let is_comment = trimmed.starts_with('#');
if !is_comment && !trimmed.is_empty() {
format!("# {}", line)
} else {
line.to_string()
}
})
.collect::<Vec<String>>()
.join("\n")
}
#[cfg(any(test, feature = "testing"))]
pub fn test() -> Self {
let mut config = Self::default();
config.general.signup_mode = SignupMode::Open;
config.drive.icann_listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
config.drive.pubky_listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
config.admin.listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
config.pkdns.icann_domain =
Some(Domain::from_str("localhost").expect("localhost is a valid domain"));
config.pkdns.dht_relay_nodes = None;
config.storage = StorageConfigToml::InMemory;
config.logging = None;
config
}
}
impl FromStr for ConfigToml {
type Err = toml::de::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
toml::from_str(s)
}
}
#[cfg(test)]
mod tests {
use crate::data_directory::log_level::LogLevel;
use super::*;
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
str::FromStr,
};
#[test]
fn test_default_config() {
let c = ConfigToml::default();
assert_eq!(c.general.signup_mode, SignupMode::TokenRequired);
assert_eq!(c.general.user_storage_quota_mb, 0);
assert_eq!(c.general.lmdb_backup_interval_s, 0);
assert_eq!(
c.drive.icann_listen_socket,
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6286))
);
assert_eq!(
c.pkdns.icann_domain,
Some(Domain::from_str("localhost").unwrap())
);
assert_eq!(
c.drive.pubky_listen_socket,
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6287))
);
assert_eq!(
c.admin.listen_socket,
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6288))
);
assert_eq!(c.admin.admin_password, "admin");
assert_eq!(c.pkdns.public_ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
assert_eq!(c.pkdns.public_pubky_tls_port, None);
assert_eq!(c.pkdns.public_icann_http_port, None);
assert_eq!(c.pkdns.user_keys_republisher_interval, 14400);
assert_eq!(c.pkdns.dht_bootstrap_nodes, None);
assert_eq!(c.pkdns.dht_request_timeout_ms, None);
assert_eq!(c.drive.rate_limits, vec![]);
assert_eq!(c.storage, StorageConfigToml::FileSystem);
assert_eq!(
c.logging,
Some(LoggingToml {
level: LogLevel::from_str("info").unwrap(),
module_levels: vec![
TargetLevel::from_str("pubky_homeserver=debug").unwrap(),
TargetLevel::from_str("tower_http=debug").unwrap()
],
})
);
}
#[test]
fn test_sample_config() {
ConfigToml::from_str(SAMPLE_CONFIG).expect("Embedded config.sample.toml must be valid");
}
#[test]
fn test_sample_config_commented_out() {
let s = ConfigToml::sample_string();
let parsed: ConfigToml =
ConfigToml::from_str_with_defaults(&s).expect("Should be valid config file");
assert_eq!(parsed, ConfigToml::default());
}
#[test]
fn test_empty_config() {
let s = "[general]\nsignup_mode = \"open\"\n";
let parsed: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
assert_eq!(parsed.general.signup_mode, SignupMode::Open);
assert_eq!(parsed.admin, ConfigToml::default().admin);
assert_eq!(parsed.logging, ConfigToml::default().logging);
}
#[test]
fn test_merged_config() {
let s = "[logging]\nlevel=\"trace\"\nmodule_levels = [ ]";
let merged: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
assert_eq!(merged.drive.rate_limits, vec![]);
let expected_logging = Some(LoggingToml {
level: LogLevel::from_str("trace").unwrap(),
module_levels: vec![],
});
assert_eq!(merged.logging, expected_logging);
}
}