Skip to main content

syncweb_core/storage/
config.rs

1use std::{
2    path::{Path, PathBuf},
3    time::Duration,
4};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{Result, SyncwebError};
9use crate::net::RelayConfig;
10use crate::schedule::{ScheduleConfig, TimeWindow, parse_rate};
11
12#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
13#[non_exhaustive]
14pub struct Config {
15    #[serde(default)]
16    pub bep: BepConfig,
17    #[serde(default)]
18    pub schedule: ScheduleConfig,
19    #[serde(default)]
20    pub bandwidth: BandwidthConfig,
21    #[serde(default)]
22    pub parallel: ParallelConfig,
23    #[serde(default)]
24    pub cache: CacheConfig,
25    #[serde(default)]
26    pub advanced: AdvancedConfig,
27    #[serde(default)]
28    pub discovery: DiscoveryConfig,
29    #[serde(default)]
30    pub default_path: Option<PathBuf>,
31    #[serde(default = "default_sync_mode")]
32    pub default_sync_mode: String,
33}
34
35impl Config {
36    /// # Errors
37    ///
38    /// Returns an error if the config file cannot be read or parsed.
39    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
40        let path_ref = path.as_ref();
41        if !path_ref.exists() {
42            return Ok(Self::default());
43        }
44
45        let contents = std::fs::read_to_string(path_ref)
46            .map_err(|error| SyncwebError::operation("failed to read config", error))?;
47        toml::from_str(&contents).map_err(|error| SyncwebError::operation("failed to parse config", error))
48    }
49
50    /// # Errors
51    ///
52    /// Returns an error if the config cannot be serialized or written to disk.
53    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
54        let path_ref = path.as_ref();
55        let contents = toml::to_string_pretty(self)
56            .map_err(|error| SyncwebError::operation("failed to serialize config", error))?;
57        crate::fs::atomic::atomic_write(path_ref, contents.as_bytes())
58    }
59
60    #[must_use]
61    pub fn relay_config(&self) -> RelayConfig {
62        self.bep.relay_config()
63    }
64
65    /// # Errors
66    ///
67    /// Returns an error if the key is unknown or the value cannot be parsed.
68    pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
69        match key {
70            "bep.enabled" => self.bep.enabled = parse_bool(key, value)?,
71            "bep.relay_urls" => self.bep.relay_urls = parse_string_list(key, value)?,
72            "bep.relay_timeout" => {
73                self.bep.relay_timeout = value.parse().map_err(|error| {
74                    SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
75                })?;
76            }
77            "bep.auto_fallback" => self.bep.auto_fallback = parse_bool(key, value)?,
78            "schedule.active_hours" => {
79                TimeWindow::parse(value)?;
80                value.clone_into(&mut self.schedule.active_hours);
81            }
82            "bandwidth.max_upload" => {
83                parse_rate(value)?;
84                value.clone_into(&mut self.bandwidth.max_upload);
85            }
86            "bandwidth.max_download" => {
87                parse_rate(value)?;
88                value.clone_into(&mut self.bandwidth.max_download);
89            }
90            "parallel.threads" => {
91                self.parallel.threads = value.parse().map_err(|error| {
92                    SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
93                })?;
94            }
95            "cache.max_cache_size" => {
96                self.cache.max_cache_size = value.parse().map_err(|error| {
97                    SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
98                })?;
99            }
100            "advanced.blob_cache_size_gb" => {
101                self.advanced.blob_cache_size_gb = value.parse().map_err(|error| {
102                    SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
103                })?;
104            }
105            "discovery.mdns" => self.discovery.mdns = parse_bool(key, value)?,
106            "discovery.beacon" => self.discovery.beacon = parse_bool(key, value)?,
107            "discovery.beacon_base_port" => {
108                self.discovery.beacon_base_port = value.parse().map_err(|error| {
109                    SyncwebError::InvalidConfig(format!("{key} must be a valid port number: {error}"))
110                })?;
111            }
112            "discovery.beacon_interval_ms" => {
113                self.discovery.beacon_interval_ms = value.parse().map_err(|error| {
114                    SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
115                })?;
116            }
117            "discovery.interface" => self.discovery.interface = Some(value.to_owned()),
118            "default_path" => self.default_path = Some(PathBuf::from(value)),
119            "default_sync_mode" => {
120                value.parse::<crate::folder::SyncMode>()?;
121                value.clone_into(&mut self.default_sync_mode);
122            }
123            _ => {
124                return Err(SyncwebError::InvalidConfig(format!(
125                    "unsupported config key {key:?}; supported keys: \
126                     bep.enabled, bep.relay_urls, bep.relay_timeout, bep.auto_fallback, schedule.active_hours, \
127                     bandwidth.max_upload, bandwidth.max_download, parallel.threads, cache.max_cache_size, \
128                     advanced.blob_cache_size_gb, discovery.mdns, discovery.beacon, discovery.beacon_base_port, \
129                     discovery.beacon_interval_ms, discovery.interface, default_path, default_sync_mode"
130                )));
131            }
132        }
133        Ok(())
134    }
135}
136
137#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138#[non_exhaustive]
139pub struct BandwidthConfig {
140    #[serde(default)]
141    pub max_upload: String,
142    #[serde(default)]
143    pub max_download: String,
144}
145
146impl Default for BandwidthConfig {
147    fn default() -> Self {
148        Self {
149            max_upload: "0".to_owned(),
150            max_download: "0".to_owned(),
151        }
152    }
153}
154
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156#[non_exhaustive]
157#[derive(Default)]
158pub struct ParallelConfig {
159    /// Thread count for parallel operations. `0` (the default) means
160    /// auto-detect the number of available CPU cores.
161    #[serde(default)]
162    pub threads: usize,
163}
164
165/// Controls the in-memory peer availability cache used during blob resolution.
166/// This is **not** a blob data cache — it limits how many peer entries are tracked
167/// for which peers serve which blobs.
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
169#[non_exhaustive]
170pub struct CacheConfig {
171    /// Maximum number of entries in the peer availability tracker.
172    #[serde(default = "default_cache_size")]
173    pub max_cache_size: usize,
174}
175
176impl Default for CacheConfig {
177    fn default() -> Self {
178        Self {
179            max_cache_size: default_cache_size(),
180        }
181    }
182}
183
184/// Advanced / experimental configuration options.
185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
186#[non_exhaustive]
187pub struct AdvancedConfig {
188    /// Maximum Iroh blob cache size in GiB (default: 2). Controls how much
189    /// local disk space is used for caching synced blob data.
190    #[serde(default = "default_blob_cache_size")]
191    pub blob_cache_size_gb: u64,
192}
193
194impl Default for AdvancedConfig {
195    fn default() -> Self {
196        Self {
197            blob_cache_size_gb: default_blob_cache_size(),
198        }
199    }
200}
201
202/// Local discovery settings. `interface` only applies to the UDP beacon; the
203/// mDNS lookup always uses the system's default multicast interface.
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205#[non_exhaustive]
206pub struct DiscoveryConfig {
207    /// Whether the mDNS address lookup is registered (default: true).
208    #[serde(default = "default_discovery_enabled")]
209    pub mdns: bool,
210    /// Whether the UDP beacon address lookup is registered (default: true).
211    #[serde(default = "default_discovery_enabled")]
212    pub beacon: bool,
213    /// Base UDP port the beacon spreads network scopes over (default: 15200).
214    #[serde(default = "default_beacon_base_port")]
215    pub beacon_base_port: u16,
216    /// How often the beacon re-broadcasts, in milliseconds (default: 1000).
217    #[serde(default = "default_beacon_interval_ms")]
218    pub beacon_interval_ms: u64,
219    /// Restrict the beacon to a single network interface by name, if any.
220    #[serde(default)]
221    pub interface: Option<String>,
222}
223
224impl Default for DiscoveryConfig {
225    fn default() -> Self {
226        Self {
227            mdns: default_discovery_enabled(),
228            beacon: default_discovery_enabled(),
229            beacon_base_port: default_beacon_base_port(),
230            beacon_interval_ms: default_beacon_interval_ms(),
231            interface: None,
232        }
233    }
234}
235
236const fn default_discovery_enabled() -> bool {
237    true
238}
239
240const fn default_beacon_base_port() -> u16 {
241    crate::node::beacon_lookup::DEFAULT_BEACON_PORT
242}
243
244const fn default_beacon_interval_ms() -> u64 {
245    1_000
246}
247
248fn default_sync_mode() -> String {
249    "sendreceive".to_owned()
250}
251
252const fn default_cache_size() -> usize {
253    5_000
254}
255
256const fn default_blob_cache_size() -> u64 {
257    2
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
261#[non_exhaustive]
262pub struct BepConfig {
263    #[serde(default)]
264    pub enabled: bool,
265    #[serde(default)]
266    pub relay_urls: Vec<String>,
267    #[serde(default = "default_relay_timeout")]
268    pub relay_timeout: u64,
269    #[serde(default = "default_auto_fallback")]
270    pub auto_fallback: bool,
271}
272
273impl Default for BepConfig {
274    fn default() -> Self {
275        Self {
276            enabled: false,
277            relay_urls: Vec::new(),
278            relay_timeout: default_relay_timeout(),
279            auto_fallback: default_auto_fallback(),
280        }
281    }
282}
283
284impl BepConfig {
285    #[must_use]
286    pub fn relay_config(&self) -> RelayConfig {
287        RelayConfig {
288            relay_urls: self.relay_urls.clone(),
289            timeout: Duration::from_secs(self.relay_timeout),
290            auto_fallback: self.enabled && self.auto_fallback,
291        }
292    }
293}
294
295const fn default_relay_timeout() -> u64 {
296    10
297}
298
299const fn default_auto_fallback() -> bool {
300    true
301}
302
303fn parse_bool(key: &str, value: &str) -> Result<bool> {
304    value
305        .parse()
306        .map_err(|error| SyncwebError::InvalidConfig(format!("{key} must be true or false: {error}")))
307}
308
309fn parse_string_list(key: &str, value: &str) -> Result<Vec<String>> {
310    let parsed: toml::Value = toml::from_str(&format!("value = {value}")).map_err(|error| {
311        SyncwebError::InvalidConfig(format!(
312            "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]: {error}"
313        ))
314    })?;
315    parsed
316        .get("value")
317        .and_then(toml::Value::as_array)
318        .filter(|values| values.iter().all(toml::Value::is_str))
319        .map(|values| {
320            values
321                .iter()
322                .map(|v| v.as_str().unwrap_or_default().to_owned())
323                .collect()
324        })
325        .ok_or_else(|| {
326            SyncwebError::InvalidConfig(format!(
327                "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]"
328            ))
329        })
330}