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