1use std::{
2 collections::BTreeMap,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::error::{Result, SyncwebError};
10use crate::net::RelayConfig;
11use crate::schedule::{ScheduleConfig, TimeWindow, parse_rate};
12
13#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
14#[non_exhaustive]
15pub struct Config {
16 #[serde(default)]
17 pub bep: BepConfig,
18 #[serde(default)]
19 pub schedule: ScheduleConfig,
20 #[serde(default)]
21 pub bandwidth: BandwidthConfig,
22 #[serde(default)]
23 pub parallel: ParallelConfig,
24 #[serde(default)]
25 pub cache: CacheConfig,
26 #[serde(default)]
27 pub advanced: AdvancedConfig,
28 #[serde(default)]
29 pub discovery: DiscoveryConfig,
30 #[serde(default)]
31 pub default_path: Option<PathBuf>,
32 #[serde(default = "default_sync_mode")]
33 pub default_sync_mode: String,
34 #[serde(default)]
35 pub subscribe: SubscribeConfig,
36}
37
38impl Config {
39 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
43 let path_ref = path.as_ref();
44 if !path_ref.exists() {
45 return Ok(Self::default());
46 }
47
48 let contents = std::fs::read_to_string(path_ref)
49 .map_err(|error| SyncwebError::operation("failed to read config", error))?;
50 toml::from_str(&contents).map_err(|error| SyncwebError::operation("failed to parse config", error))
51 }
52
53 pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
57 let path_ref = path.as_ref();
58 let contents = toml::to_string_pretty(self)
59 .map_err(|error| SyncwebError::operation("failed to serialize config", error))?;
60 crate::fs::atomic::atomic_write(path_ref, contents.as_bytes())
61 }
62
63 #[must_use]
64 pub fn relay_config(&self) -> RelayConfig {
65 self.bep.relay_config()
66 }
67
68 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
72 match key {
73 "bep.enabled" => self.bep.enabled = parse_bool(key, value)?,
74 "bep.relay_urls" => self.bep.relay_urls = parse_string_list(key, value)?,
75 "bep.relay_timeout" => {
76 self.bep.relay_timeout = value.parse().map_err(|error| {
77 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
78 })?;
79 }
80 "bep.auto_fallback" => self.bep.auto_fallback = parse_bool(key, value)?,
81 "schedule.active_hours" => {
82 TimeWindow::parse(value)?;
83 value.clone_into(&mut self.schedule.active_hours);
84 }
85 "bandwidth.max_upload" => {
86 parse_rate(value)?;
87 value.clone_into(&mut self.bandwidth.max_upload);
88 }
89 "bandwidth.max_download" => {
90 parse_rate(value)?;
91 value.clone_into(&mut self.bandwidth.max_download);
92 }
93 "parallel.threads" => {
94 self.parallel.threads = value.parse().map_err(|error| {
95 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
96 })?;
97 }
98 "cache.max_cache_size" => {
99 self.cache.max_cache_size = value.parse().map_err(|error| {
100 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
101 })?;
102 }
103 "advanced.blob_cache_size_gb" => {
104 self.advanced.blob_cache_size_gb = value.parse().map_err(|error| {
105 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
106 })?;
107 }
108 "discovery.mdns" => self.discovery.mdns = parse_bool(key, value)?,
109 "discovery.beacon" => self.discovery.beacon = parse_bool(key, value)?,
110 "discovery.beacon_base_port" => {
111 self.discovery.beacon_base_port = value.parse().map_err(|error| {
112 SyncwebError::InvalidConfig(format!("{key} must be a valid port number: {error}"))
113 })?;
114 }
115 "discovery.beacon_interval_ms" => {
116 self.discovery.beacon_interval_ms = value.parse().map_err(|error| {
117 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
118 })?;
119 }
120 "discovery.interface" => self.discovery.interface = Some(value.to_owned()),
121 "default_path" => self.default_path = Some(PathBuf::from(value)),
122 "default_sync_mode" => {
123 value.parse::<crate::folder::SyncMode>()?;
124 value.clone_into(&mut self.default_sync_mode);
125 }
126 _ if key.ends_with(".subscribe") => {
127 if let Some(namespace) = key.strip_suffix(".subscribe") {
128 let namespace_id = namespace.parse::<iroh_docs::NamespaceId>().map_err(|error| {
129 SyncwebError::InvalidConfig(format!("invalid namespace {namespace:?} in {key}: {error}"))
130 })?;
131 let enabled = parse_bool(key, value)?;
132 if enabled {
133 let entry = self.subscribe.folders.entry(namespace_id.to_string()).or_default();
134 entry.enabled = true;
135 } else {
136 self.subscribe.folders.remove(namespace_id.to_string().as_str());
137 }
138 }
139 }
140 _ => {
141 return Err(SyncwebError::InvalidConfig(format!(
142 "unsupported config key {key:?}; supported keys: \
143 bep.enabled, bep.relay_urls, bep.relay_timeout, bep.auto_fallback, schedule.active_hours, \
144 bandwidth.max_upload, bandwidth.max_download, parallel.threads, cache.max_cache_size, \
145 advanced.blob_cache_size_gb, discovery.mdns, discovery.beacon, discovery.beacon_base_port, \
146 discovery.beacon_interval_ms, discovery.interface, default_path, default_sync_mode, \
147 <namespace>.subscribe"
148 )));
149 }
150 }
151 Ok(())
152 }
153
154 pub fn set_subscribe(&mut self, namespace: &str, enabled: bool, filters: &SubscribeFilters) {
160 let entry = self.subscribe.folders.entry(namespace.to_owned()).or_default();
161 entry.enabled = enabled;
162 if !filters.is_default() {
163 entry.filters = filters.clone();
164 }
165 }
166
167 pub fn remove_subscribe(&mut self, namespace: &str) -> bool {
169 self.subscribe.folders.remove(namespace).is_some()
170 }
171}
172
173#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
175#[non_exhaustive]
176pub struct SubscribeConfig {
177 #[serde(default)]
179 pub folders: BTreeMap<String, SubscribeFolderConfig>,
180}
181
182#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
184#[non_exhaustive]
185pub struct SubscribeFolderConfig {
186 #[serde(default)]
188 pub enabled: bool,
189 #[serde(default)]
191 pub filters: SubscribeFilters,
192}
193
194#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
196#[non_exhaustive]
197pub struct SubscribeFilters {
198 #[serde(default)]
200 pub ingest_only: bool,
201 #[serde(default)]
203 pub ignore_self: bool,
204 #[serde(default)]
206 pub sync_prefix: Option<PathBuf>,
207 #[serde(default)]
209 pub glob: Option<String>,
210 #[serde(default)]
212 pub max_count: Option<u64>,
213 #[serde(default)]
215 pub max_size: Option<u64>,
216}
217
218impl SubscribeFilters {
219 #[must_use]
221 pub const fn new(
222 ingest_only: bool,
223 ignore_self: bool,
224 sync_prefix: Option<PathBuf>,
225 glob: Option<String>,
226 max_count: Option<u64>,
227 max_size: Option<u64>,
228 ) -> Self {
229 Self {
230 ingest_only,
231 ignore_self,
232 sync_prefix,
233 glob,
234 max_count,
235 max_size,
236 }
237 }
238
239 #[must_use]
240 pub fn is_default(&self) -> bool {
241 *self == Self::default()
242 }
243}
244
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246#[non_exhaustive]
247pub struct BandwidthConfig {
248 #[serde(default)]
249 pub max_upload: String,
250 #[serde(default)]
251 pub max_download: String,
252}
253impl Default for BandwidthConfig {
254 fn default() -> Self {
255 Self {
256 max_upload: "0".to_owned(),
257 max_download: "0".to_owned(),
258 }
259 }
260}
261
262#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
263#[non_exhaustive]
264#[derive(Default)]
265pub struct ParallelConfig {
266 #[serde(default)]
269 pub threads: usize,
270}
271
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
276#[non_exhaustive]
277pub struct CacheConfig {
278 #[serde(default = "default_cache_size")]
280 pub max_cache_size: usize,
281}
282
283impl Default for CacheConfig {
284 fn default() -> Self {
285 Self {
286 max_cache_size: default_cache_size(),
287 }
288 }
289}
290
291#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
293#[non_exhaustive]
294pub struct AdvancedConfig {
295 #[serde(default = "default_blob_cache_size")]
298 pub blob_cache_size_gb: u64,
299}
300
301impl Default for AdvancedConfig {
302 fn default() -> Self {
303 Self {
304 blob_cache_size_gb: default_blob_cache_size(),
305 }
306 }
307}
308
309#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
312#[non_exhaustive]
313pub struct DiscoveryConfig {
314 #[serde(default = "default_discovery_enabled")]
316 pub mdns: bool,
317 #[serde(default = "default_discovery_enabled")]
319 pub beacon: bool,
320 #[serde(default = "default_beacon_base_port")]
322 pub beacon_base_port: u16,
323 #[serde(default = "default_beacon_interval_ms")]
325 pub beacon_interval_ms: u64,
326 #[serde(default)]
328 pub interface: Option<String>,
329}
330
331impl Default for DiscoveryConfig {
332 fn default() -> Self {
333 Self {
334 mdns: default_discovery_enabled(),
335 beacon: default_discovery_enabled(),
336 beacon_base_port: default_beacon_base_port(),
337 beacon_interval_ms: default_beacon_interval_ms(),
338 interface: None,
339 }
340 }
341}
342
343const fn default_discovery_enabled() -> bool {
344 true
345}
346
347const fn default_beacon_base_port() -> u16 {
348 crate::node::beacon_lookup::DEFAULT_BEACON_PORT
349}
350
351const fn default_beacon_interval_ms() -> u64 {
352 1_000
353}
354
355fn default_sync_mode() -> String {
356 "sendreceive".to_owned()
357}
358
359const fn default_cache_size() -> usize {
360 5_000
361}
362
363const fn default_blob_cache_size() -> u64 {
364 2
365}
366
367#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
368#[non_exhaustive]
369pub struct BepConfig {
370 #[serde(default)]
371 pub enabled: bool,
372 #[serde(default)]
373 pub relay_urls: Vec<String>,
374 #[serde(default = "default_relay_timeout")]
375 pub relay_timeout: u64,
376 #[serde(default = "default_auto_fallback")]
377 pub auto_fallback: bool,
378}
379
380impl Default for BepConfig {
381 fn default() -> Self {
382 Self {
383 enabled: false,
384 relay_urls: Vec::new(),
385 relay_timeout: default_relay_timeout(),
386 auto_fallback: default_auto_fallback(),
387 }
388 }
389}
390
391impl BepConfig {
392 #[must_use]
393 pub fn relay_config(&self) -> RelayConfig {
394 RelayConfig {
395 relay_urls: self.relay_urls.clone(),
396 timeout: Duration::from_secs(self.relay_timeout),
397 auto_fallback: self.enabled && self.auto_fallback,
398 }
399 }
400}
401
402const fn default_relay_timeout() -> u64 {
403 10
404}
405
406const fn default_auto_fallback() -> bool {
407 true
408}
409
410fn parse_bool(key: &str, value: &str) -> Result<bool> {
411 value
412 .parse()
413 .map_err(|error| SyncwebError::InvalidConfig(format!("{key} must be true or false: {error}")))
414}
415
416fn parse_string_list(key: &str, value: &str) -> Result<Vec<String>> {
417 let parsed: toml::Value = toml::from_str(&format!("value = {value}")).map_err(|error| {
418 SyncwebError::InvalidConfig(format!(
419 "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]: {error}"
420 ))
421 })?;
422 parsed
423 .get("value")
424 .and_then(toml::Value::as_array)
425 .filter(|values| values.iter().all(toml::Value::is_str))
426 .map(|values| {
427 values
428 .iter()
429 .map(|v| v.as_str().unwrap_or_default().to_owned())
430 .collect()
431 })
432 .ok_or_else(|| {
433 SyncwebError::InvalidConfig(format!(
434 "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]"
435 ))
436 })
437}