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 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 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 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 #[serde(default)]
162 pub threads: usize,
163}
164
165#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
169#[non_exhaustive]
170pub struct CacheConfig {
171 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
186#[non_exhaustive]
187pub struct AdvancedConfig {
188 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205#[non_exhaustive]
206pub struct DiscoveryConfig {
207 #[serde(default = "default_discovery_enabled")]
209 pub mdns: bool,
210 #[serde(default = "default_discovery_enabled")]
212 pub beacon: bool,
213 #[serde(default = "default_beacon_base_port")]
215 pub beacon_base_port: u16,
216 #[serde(default = "default_beacon_interval_ms")]
218 pub beacon_interval_ms: u64,
219 #[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}