use std::{
collections::BTreeMap,
path::{Path, PathBuf},
time::Duration,
};
use serde::{Deserialize, Serialize};
use crate::error::{Result, SyncwebError};
use crate::net::RelayConfig;
use crate::schedule::{ScheduleConfig, TimeWindow, parse_rate};
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Config {
#[serde(default)]
pub bep: BepConfig,
#[serde(default)]
pub schedule: ScheduleConfig,
#[serde(default)]
pub bandwidth: BandwidthConfig,
#[serde(default)]
pub parallel: ParallelConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub advanced: AdvancedConfig,
#[serde(default)]
pub discovery: DiscoveryConfig,
#[serde(default)]
pub default_path: Option<PathBuf>,
#[serde(default = "default_sync_mode")]
pub default_sync_mode: String,
#[serde(default)]
pub subscribe: SubscribeConfig,
}
impl Config {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path_ref = path.as_ref();
if !path_ref.exists() {
return Ok(Self::default());
}
let contents = std::fs::read_to_string(path_ref)
.map_err(|error| SyncwebError::operation("failed to read config", error))?;
toml::from_str(&contents).map_err(|error| SyncwebError::operation("failed to parse config", error))
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let path_ref = path.as_ref();
let contents = toml::to_string_pretty(self)
.map_err(|error| SyncwebError::operation("failed to serialize config", error))?;
crate::fs::atomic::atomic_write(path_ref, contents.as_bytes())
}
#[must_use]
pub fn relay_config(&self) -> RelayConfig {
self.bep.relay_config()
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"bep.enabled" => self.bep.enabled = parse_bool(key, value)?,
"bep.relay_urls" => self.bep.relay_urls = parse_string_list(key, value)?,
"bep.relay_timeout" => {
self.bep.relay_timeout = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
})?;
}
"bep.auto_fallback" => self.bep.auto_fallback = parse_bool(key, value)?,
"schedule.active_hours" => {
TimeWindow::parse(value)?;
value.clone_into(&mut self.schedule.active_hours);
}
"bandwidth.max_upload" => {
parse_rate(value)?;
value.clone_into(&mut self.bandwidth.max_upload);
}
"bandwidth.max_download" => {
parse_rate(value)?;
value.clone_into(&mut self.bandwidth.max_download);
}
"parallel.threads" => {
self.parallel.threads = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
})?;
}
"cache.max_cache_size" => {
self.cache.max_cache_size = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
})?;
}
"advanced.blob_cache_size_gb" => {
self.advanced.blob_cache_size_gb = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
})?;
}
"discovery.mdns" => self.discovery.mdns = parse_bool(key, value)?,
"discovery.beacon" => self.discovery.beacon = parse_bool(key, value)?,
"discovery.beacon_base_port" => {
self.discovery.beacon_base_port = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a valid port number: {error}"))
})?;
}
"discovery.beacon_interval_ms" => {
self.discovery.beacon_interval_ms = value.parse().map_err(|error| {
SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
})?;
}
"discovery.interface" => self.discovery.interface = Some(value.to_owned()),
"default_path" => self.default_path = Some(PathBuf::from(value)),
"default_sync_mode" => {
value.parse::<crate::folder::SyncMode>()?;
value.clone_into(&mut self.default_sync_mode);
}
_ if key.ends_with(".subscribe") => {
if let Some(namespace) = key.strip_suffix(".subscribe") {
let namespace_id = namespace.parse::<iroh_docs::NamespaceId>().map_err(|error| {
SyncwebError::InvalidConfig(format!("invalid namespace {namespace:?} in {key}: {error}"))
})?;
let enabled = parse_bool(key, value)?;
if enabled {
let entry = self.subscribe.folders.entry(namespace_id.to_string()).or_default();
entry.enabled = true;
} else {
self.subscribe.folders.remove(namespace_id.to_string().as_str());
}
}
}
_ => {
return Err(SyncwebError::InvalidConfig(format!(
"unsupported config key {key:?}; supported keys: \
bep.enabled, bep.relay_urls, bep.relay_timeout, bep.auto_fallback, schedule.active_hours, \
bandwidth.max_upload, bandwidth.max_download, parallel.threads, cache.max_cache_size, \
advanced.blob_cache_size_gb, discovery.mdns, discovery.beacon, discovery.beacon_base_port, \
discovery.beacon_interval_ms, discovery.interface, default_path, default_sync_mode, \
<namespace>.subscribe"
)));
}
}
Ok(())
}
pub fn set_subscribe(&mut self, namespace: &str, enabled: bool, filters: &SubscribeFilters) {
let entry = self.subscribe.folders.entry(namespace.to_owned()).or_default();
entry.enabled = enabled;
if !filters.is_default() {
entry.filters = filters.clone();
}
}
pub fn remove_subscribe(&mut self, namespace: &str) -> bool {
self.subscribe.folders.remove(namespace).is_some()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct SubscribeConfig {
#[serde(default)]
pub folders: BTreeMap<String, SubscribeFolderConfig>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct SubscribeFolderConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub filters: SubscribeFilters,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct SubscribeFilters {
#[serde(default)]
pub ingest_only: bool,
#[serde(default)]
pub ignore_self: bool,
#[serde(default)]
pub sync_prefix: Option<PathBuf>,
#[serde(default)]
pub glob: Option<String>,
#[serde(default)]
pub max_count: Option<u64>,
#[serde(default)]
pub max_size: Option<u64>,
}
impl SubscribeFilters {
#[must_use]
pub const fn new(
ingest_only: bool,
ignore_self: bool,
sync_prefix: Option<PathBuf>,
glob: Option<String>,
max_count: Option<u64>,
max_size: Option<u64>,
) -> Self {
Self {
ingest_only,
ignore_self,
sync_prefix,
glob,
max_count,
max_size,
}
}
#[must_use]
pub fn is_default(&self) -> bool {
*self == Self::default()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct BandwidthConfig {
#[serde(default)]
pub max_upload: String,
#[serde(default)]
pub max_download: String,
}
impl Default for BandwidthConfig {
fn default() -> Self {
Self {
max_upload: "0".to_owned(),
max_download: "0".to_owned(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[derive(Default)]
pub struct ParallelConfig {
#[serde(default)]
pub threads: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct CacheConfig {
#[serde(default = "default_cache_size")]
pub max_cache_size: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_cache_size: default_cache_size(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct AdvancedConfig {
#[serde(default = "default_blob_cache_size")]
pub blob_cache_size_gb: u64,
}
impl Default for AdvancedConfig {
fn default() -> Self {
Self {
blob_cache_size_gb: default_blob_cache_size(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct DiscoveryConfig {
#[serde(default = "default_discovery_enabled")]
pub mdns: bool,
#[serde(default = "default_discovery_enabled")]
pub beacon: bool,
#[serde(default = "default_beacon_base_port")]
pub beacon_base_port: u16,
#[serde(default = "default_beacon_interval_ms")]
pub beacon_interval_ms: u64,
#[serde(default)]
pub interface: Option<String>,
}
impl Default for DiscoveryConfig {
fn default() -> Self {
Self {
mdns: default_discovery_enabled(),
beacon: default_discovery_enabled(),
beacon_base_port: default_beacon_base_port(),
beacon_interval_ms: default_beacon_interval_ms(),
interface: None,
}
}
}
const fn default_discovery_enabled() -> bool {
true
}
const fn default_beacon_base_port() -> u16 {
crate::node::beacon_lookup::DEFAULT_BEACON_PORT
}
const fn default_beacon_interval_ms() -> u64 {
1_000
}
fn default_sync_mode() -> String {
"sendreceive".to_owned()
}
const fn default_cache_size() -> usize {
5_000
}
const fn default_blob_cache_size() -> u64 {
2
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct BepConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub relay_urls: Vec<String>,
#[serde(default = "default_relay_timeout")]
pub relay_timeout: u64,
#[serde(default = "default_auto_fallback")]
pub auto_fallback: bool,
}
impl Default for BepConfig {
fn default() -> Self {
Self {
enabled: false,
relay_urls: Vec::new(),
relay_timeout: default_relay_timeout(),
auto_fallback: default_auto_fallback(),
}
}
}
impl BepConfig {
#[must_use]
pub fn relay_config(&self) -> RelayConfig {
RelayConfig {
relay_urls: self.relay_urls.clone(),
timeout: Duration::from_secs(self.relay_timeout),
auto_fallback: self.enabled && self.auto_fallback,
}
}
}
const fn default_relay_timeout() -> u64 {
10
}
const fn default_auto_fallback() -> bool {
true
}
fn parse_bool(key: &str, value: &str) -> Result<bool> {
value
.parse()
.map_err(|error| SyncwebError::InvalidConfig(format!("{key} must be true or false: {error}")))
}
fn parse_string_list(key: &str, value: &str) -> Result<Vec<String>> {
let parsed: toml::Value = toml::from_str(&format!("value = {value}")).map_err(|error| {
SyncwebError::InvalidConfig(format!(
"{key} must be a TOML string array, for example [\"tcp://relay:22270\"]: {error}"
))
})?;
parsed
.get("value")
.and_then(toml::Value::as_array)
.filter(|values| values.iter().all(toml::Value::is_str))
.map(|values| {
values
.iter()
.map(|v| v.as_str().unwrap_or_default().to_owned())
.collect()
})
.ok_or_else(|| {
SyncwebError::InvalidConfig(format!(
"{key} must be a TOML string array, for example [\"tcp://relay:22270\"]"
))
})
}