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 #[serde(default)]
38 pub channels: BTreeMap<String, ChannelConfig>,
39}
40
41impl Config {
42 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
46 let path_ref = path.as_ref();
47 if !path_ref.exists() {
48 return Ok(Self::default());
49 }
50
51 let contents = std::fs::read_to_string(path_ref)
52 .map_err(|error| SyncwebError::operation("failed to read config", error))?;
53 toml::from_str(&contents).map_err(|error| SyncwebError::operation("failed to parse config", error))
54 }
55
56 pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
60 let path_ref = path.as_ref();
61 let contents = toml::to_string_pretty(self)
62 .map_err(|error| SyncwebError::operation("failed to serialize config", error))?;
63 crate::fs::atomic::atomic_write(path_ref, contents.as_bytes())
64 }
65
66 #[must_use]
67 pub fn relay_config(&self) -> RelayConfig {
68 self.bep.relay_config()
69 }
70
71 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
75 match key {
76 "bep.enabled" => self.bep.enabled = parse_bool(key, value)?,
77 "bep.relay_urls" => self.bep.relay_urls = parse_string_list(key, value)?,
78 "bep.relay_timeout" => {
79 self.bep.relay_timeout = value.parse().map_err(|error| {
80 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
81 })?;
82 }
83 "bep.auto_fallback" => self.bep.auto_fallback = parse_bool(key, value)?,
84 "schedule.active_hours" => {
85 TimeWindow::parse(value)?;
86 value.clone_into(&mut self.schedule.active_hours);
87 }
88 "bandwidth.max_upload" => {
89 parse_rate(value)?;
90 value.clone_into(&mut self.bandwidth.max_upload);
91 }
92 "bandwidth.max_download" => {
93 parse_rate(value)?;
94 value.clone_into(&mut self.bandwidth.max_download);
95 }
96 "parallel.threads" => {
97 self.parallel.threads = value.parse().map_err(|error| {
98 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
99 })?;
100 }
101 "cache.max_cache_size" => {
102 self.cache.max_cache_size = value.parse().map_err(|error| {
103 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
104 })?;
105 }
106 "advanced.blob_cache_size_gb" => {
107 self.advanced.blob_cache_size_gb = value.parse().map_err(|error| {
108 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
109 })?;
110 }
111 "discovery.mdns" => self.discovery.mdns = parse_bool(key, value)?,
112 "discovery.beacon" => self.discovery.beacon = parse_bool(key, value)?,
113 "discovery.beacon_base_port" => {
114 self.discovery.beacon_base_port = value.parse().map_err(|error| {
115 SyncwebError::InvalidConfig(format!("{key} must be a valid port number: {error}"))
116 })?;
117 }
118 "discovery.beacon_interval_ms" => {
119 self.discovery.beacon_interval_ms = value.parse().map_err(|error| {
120 SyncwebError::InvalidConfig(format!("{key} must be a non-negative integer: {error}"))
121 })?;
122 }
123 "discovery.interface" => self.discovery.interface = Some(value.to_owned()),
124 "default_path" => self.default_path = Some(PathBuf::from(value)),
125 "default_sync_mode" => {
126 value.parse::<crate::folder::SyncMode>()?;
127 value.clone_into(&mut self.default_sync_mode);
128 }
129 _ if key.ends_with(".subscribe") => {
130 if let Some(namespace) = key.strip_suffix(".subscribe") {
131 let namespace_id = namespace.parse::<iroh_docs::NamespaceId>().map_err(|error| {
132 SyncwebError::InvalidConfig(format!("invalid namespace {namespace:?} in {key}: {error}"))
133 })?;
134 let enabled = parse_bool(key, value)?;
135 if enabled {
136 let entry = self.subscribe.folders.entry(namespace_id.to_string()).or_default();
137 entry.enabled = true;
138 } else {
139 self.subscribe.folders.remove(namespace_id.to_string().as_str());
140 }
141 }
142 }
143 _ if key.starts_with("channels.") => {
144 let rest = &key["channels.".len()..];
145 if let Some(field) = rest.strip_suffix(".ticket") {
146 let channel_name = field.to_owned();
147 value.clone_into(
148 &mut self
149 .channels
150 .entry(channel_name)
151 .or_insert_with(|| ChannelConfig { ticket: String::new() })
152 .ticket,
153 );
154 } else {
155 return Err(SyncwebError::InvalidConfig(format!(
156 "unsupported channels key {key:?}; supported: channels.<name>.ticket"
157 )));
158 }
159 }
160 _ => {
161 return Err(SyncwebError::InvalidConfig(format!(
162 "unsupported config key {key:?}; supported keys: \
163 bep.enabled, bep.relay_urls, bep.relay_timeout, bep.auto_fallback, schedule.active_hours, \
164 bandwidth.max_upload, bandwidth.max_download, parallel.threads, cache.max_cache_size, \
165 advanced.blob_cache_size_gb, discovery.mdns, discovery.beacon, discovery.beacon_base_port, \
166 discovery.beacon_interval_ms, discovery.interface, default_path, default_sync_mode, \
167 <namespace>.subscribe, channels.<name>.ticket"
168 )));
169 }
170 }
171 Ok(())
172 }
173
174 pub fn set_subscribe(&mut self, namespace: &str, enabled: bool, filters: &SubscribeFilters) {
180 let entry = self.subscribe.folders.entry(namespace.to_owned()).or_default();
181 entry.enabled = enabled;
182 if !filters.is_default() {
183 entry.filters = filters.clone();
184 }
185 }
186
187 pub fn remove_subscribe(&mut self, namespace: &str) -> bool {
189 self.subscribe.folders.remove(namespace).is_some()
190 }
191}
192
193#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
195#[non_exhaustive]
196pub struct SubscribeConfig {
197 #[serde(default)]
199 pub folders: BTreeMap<String, SubscribeFolderConfig>,
200}
201
202#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
204#[non_exhaustive]
205pub struct SubscribeFolderConfig {
206 #[serde(default)]
208 pub enabled: bool,
209 #[serde(default)]
211 pub filters: SubscribeFilters,
212}
213
214#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
216#[non_exhaustive]
217pub struct SubscribeFilters {
218 #[serde(default)]
220 pub ingest_only: bool,
221 #[serde(default)]
223 pub ignore_self: bool,
224 #[serde(default)]
226 pub sync_prefix: Option<PathBuf>,
227 #[serde(default)]
229 pub glob: Option<String>,
230 #[serde(default)]
232 pub max_count: Option<u64>,
233 #[serde(default)]
235 pub max_size: Option<u64>,
236}
237
238impl SubscribeFilters {
239 #[must_use]
241 pub const fn new(
242 ingest_only: bool,
243 ignore_self: bool,
244 sync_prefix: Option<PathBuf>,
245 glob: Option<String>,
246 max_count: Option<u64>,
247 max_size: Option<u64>,
248 ) -> Self {
249 Self {
250 ingest_only,
251 ignore_self,
252 sync_prefix,
253 glob,
254 max_count,
255 max_size,
256 }
257 }
258
259 #[must_use]
260 pub fn is_default(&self) -> bool {
261 *self == Self::default()
262 }
263}
264
265#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
267#[non_exhaustive]
268pub struct ChannelConfig {
269 pub ticket: String,
271}
272
273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
274#[non_exhaustive]
275pub struct BandwidthConfig {
276 #[serde(default)]
277 pub max_upload: String,
278 #[serde(default)]
279 pub max_download: String,
280}
281impl Default for BandwidthConfig {
282 fn default() -> Self {
283 Self {
284 max_upload: "0".to_owned(),
285 max_download: "0".to_owned(),
286 }
287 }
288}
289
290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
291#[non_exhaustive]
292#[derive(Default)]
293pub struct ParallelConfig {
294 #[serde(default)]
297 pub threads: usize,
298}
299
300#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
304#[non_exhaustive]
305pub struct CacheConfig {
306 #[serde(default = "default_cache_size")]
308 pub max_cache_size: usize,
309}
310
311impl Default for CacheConfig {
312 fn default() -> Self {
313 Self {
314 max_cache_size: default_cache_size(),
315 }
316 }
317}
318
319#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
321#[non_exhaustive]
322pub struct AdvancedConfig {
323 #[serde(default = "default_blob_cache_size")]
326 pub blob_cache_size_gb: u64,
327}
328
329impl Default for AdvancedConfig {
330 fn default() -> Self {
331 Self {
332 blob_cache_size_gb: default_blob_cache_size(),
333 }
334 }
335}
336
337#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
340#[non_exhaustive]
341pub struct DiscoveryConfig {
342 #[serde(default = "default_discovery_enabled")]
344 pub mdns: bool,
345 #[serde(default = "default_discovery_enabled")]
347 pub beacon: bool,
348 #[serde(default = "default_beacon_base_port")]
350 pub beacon_base_port: u16,
351 #[serde(default = "default_beacon_interval_ms")]
353 pub beacon_interval_ms: u64,
354 #[serde(default)]
356 pub interface: Option<String>,
357}
358
359impl Default for DiscoveryConfig {
360 fn default() -> Self {
361 Self {
362 mdns: default_discovery_enabled(),
363 beacon: default_discovery_enabled(),
364 beacon_base_port: default_beacon_base_port(),
365 beacon_interval_ms: default_beacon_interval_ms(),
366 interface: None,
367 }
368 }
369}
370
371const fn default_discovery_enabled() -> bool {
372 true
373}
374
375const fn default_beacon_base_port() -> u16 {
376 crate::node::beacon_lookup::DEFAULT_BEACON_PORT
377}
378
379const fn default_beacon_interval_ms() -> u64 {
380 1_000
381}
382
383fn default_sync_mode() -> String {
384 "sendreceive".to_owned()
385}
386
387const fn default_cache_size() -> usize {
388 5_000
389}
390
391const fn default_blob_cache_size() -> u64 {
392 2
393}
394
395#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
396#[non_exhaustive]
397pub struct BepConfig {
398 #[serde(default)]
399 pub enabled: bool,
400 #[serde(default)]
401 pub relay_urls: Vec<String>,
402 #[serde(default = "default_relay_timeout")]
403 pub relay_timeout: u64,
404 #[serde(default = "default_auto_fallback")]
405 pub auto_fallback: bool,
406}
407
408impl Default for BepConfig {
409 fn default() -> Self {
410 Self {
411 enabled: false,
412 relay_urls: Vec::new(),
413 relay_timeout: default_relay_timeout(),
414 auto_fallback: default_auto_fallback(),
415 }
416 }
417}
418
419impl BepConfig {
420 #[must_use]
421 pub fn relay_config(&self) -> RelayConfig {
422 RelayConfig {
423 relay_urls: self.relay_urls.clone(),
424 timeout: Duration::from_secs(self.relay_timeout),
425 auto_fallback: self.enabled && self.auto_fallback,
426 }
427 }
428}
429
430const fn default_relay_timeout() -> u64 {
431 10
432}
433
434const fn default_auto_fallback() -> bool {
435 true
436}
437
438fn parse_bool(key: &str, value: &str) -> Result<bool> {
439 value
440 .parse()
441 .map_err(|error| SyncwebError::InvalidConfig(format!("{key} must be true or false: {error}")))
442}
443
444fn parse_string_list(key: &str, value: &str) -> Result<Vec<String>> {
445 let parsed: toml::Value = toml::from_str(&format!("value = {value}")).map_err(|error| {
446 SyncwebError::InvalidConfig(format!(
447 "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]: {error}"
448 ))
449 })?;
450 parsed
451 .get("value")
452 .and_then(toml::Value::as_array)
453 .filter(|values| values.iter().all(toml::Value::is_str))
454 .map(|values| {
455 values
456 .iter()
457 .map(|v| v.as_str().unwrap_or_default().to_owned())
458 .collect()
459 })
460 .ok_or_else(|| {
461 SyncwebError::InvalidConfig(format!(
462 "{key} must be a TOML string array, for example [\"tcp://relay:22270\"]"
463 ))
464 })
465}