torrust_index_backend/
config.rs

1//! Configuration for the application.
2use std::path::Path;
3use std::{env, fs};
4
5use config::{Config, ConfigError, File, FileFormat};
6use log::warn;
7use serde::{Deserialize, Serialize};
8use tokio::sync::RwLock;
9
10/// Information displayed to the user in the website.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Website {
13    /// The name of the website.
14    pub name: String,
15}
16
17impl Default for Website {
18    fn default() -> Self {
19        Self {
20            name: "Torrust".to_string(),
21        }
22    }
23}
24
25/// See `TrackerMode` in [`torrust-tracker-primitives`](https://docs.rs/torrust-tracker-primitives)
26/// crate for more information.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum TrackerMode {
29    // todo: use https://crates.io/crates/torrust-tracker-primitives
30    /// Will track every new info hash and serve every peer.
31    Public,
32    /// Will only serve authenticated peers.
33    Private,
34    /// Will only track whitelisted info hashes.
35    Whitelisted,
36    /// Will only track whitelisted info hashes and serve authenticated peers.
37    PrivateWhitelisted,
38}
39
40impl Default for TrackerMode {
41    fn default() -> Self {
42        Self::Public
43    }
44}
45
46/// Configuration for the associated tracker.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Tracker {
49    /// Connection string for the tracker. For example: `udp://TRACKER_IP:6969`.
50    pub url: String,
51    /// The mode of the tracker. For example: `Public`.
52    /// See `TrackerMode` in [`torrust-tracker-primitives`](https://docs.rs/torrust-tracker-primitives)
53    /// crate for more information.
54    pub mode: TrackerMode,
55    /// The url of the tracker API. For example: `http://localhost:1212`.
56    pub api_url: String,
57    /// The token used to authenticate with the tracker API.
58    pub token: String,
59    /// The amount of seconds the token is valid.
60    pub token_valid_seconds: u64,
61}
62
63impl Default for Tracker {
64    fn default() -> Self {
65        Self {
66            url: "udp://localhost:6969".to_string(),
67            mode: TrackerMode::default(),
68            api_url: "http://localhost:1212".to_string(),
69            token: "MyAccessToken".to_string(),
70            token_valid_seconds: 7_257_600,
71        }
72    }
73}
74
75/// Port number representing that the OS will choose one randomly from the available ports.
76///
77/// It's the port number `0`
78pub const FREE_PORT: u16 = 0;
79
80/// The the base URL for the API.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Network {
83    /// The port to listen on. Default to `3001`.
84    pub port: u16,
85    /// The base URL for the API. For example: `http://localhost`.
86    /// If not set, the base URL will be inferred from the request.
87    pub base_url: Option<String>,
88}
89
90impl Default for Network {
91    fn default() -> Self {
92        Self {
93            port: 3001,
94            base_url: None,
95        }
96    }
97}
98
99/// Whether the email is required on signup or not.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum EmailOnSignup {
102    /// The email is required on signup.
103    Required,
104    /// The email is optional on signup.
105    Optional,
106    /// The email is not allowed on signup. It will only be ignored if provided.
107    None, // code-review: rename to `Ignored`?
108}
109
110impl Default for EmailOnSignup {
111    fn default() -> Self {
112        Self::Optional
113    }
114}
115
116/// Authentication options.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Auth {
119    /// Whether or not to require an email on signup.
120    pub email_on_signup: EmailOnSignup,
121    /// The minimum password length.
122    pub min_password_length: usize,
123    /// The maximum password length.
124    pub max_password_length: usize,
125    /// The secret key used to sign JWT tokens.
126    pub secret_key: String,
127}
128
129impl Default for Auth {
130    fn default() -> Self {
131        Self {
132            email_on_signup: EmailOnSignup::default(),
133            min_password_length: 6,
134            max_password_length: 64,
135            secret_key: "MaxVerstappenWC2021".to_string(),
136        }
137    }
138}
139
140/// Database configuration.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct Database {
143    /// The connection string for the database. For example: `sqlite://data.db?mode=rwc`.
144    pub connect_url: String,
145}
146
147impl Default for Database {
148    fn default() -> Self {
149        Self {
150            connect_url: "sqlite://data.db?mode=rwc".to_string(),
151        }
152    }
153}
154
155/// SMTP configuration.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Mail {
158    /// Whether or not to enable email verification on signup.
159    pub email_verification_enabled: bool,
160    /// The email address to send emails from.
161    pub from: String,
162    /// The email address to reply to.
163    pub reply_to: String,
164    /// The username to use for SMTP authentication.
165    pub username: String,
166    /// The password to use for SMTP authentication.
167    pub password: String,
168    /// The SMTP server to use.
169    pub server: String,
170    /// The SMTP port to use.
171    pub port: u16,
172}
173
174impl Default for Mail {
175    fn default() -> Self {
176        Self {
177            email_verification_enabled: false,
178            from: "example@email.com".to_string(),
179            reply_to: "noreply@email.com".to_string(),
180            username: String::default(),
181            password: String::default(),
182            server: String::default(),
183            port: 25,
184        }
185    }
186}
187
188/// Configuration for the image proxy cache.
189///
190/// Users have a cache quota per period. For example: 100MB per day.
191/// When users are navigating the site, they will be downloading images that are
192/// embedded in the torrent description. These images will be cached in the
193/// proxy. The proxy will not download new images if the user has reached the
194/// quota.
195#[allow(clippy::module_name_repetitions)]
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ImageCache {
198    /// Maximum time in seconds to wait for downloading the image form the original source.
199    pub max_request_timeout_ms: u64,
200    /// Cache size in bytes.
201    pub capacity: usize,
202    /// Maximum size in bytes for a single image.
203    pub entry_size_limit: usize,
204    /// Users have a cache quota per period. For example: 100MB per day.
205    /// This is the period in seconds (1 day in seconds).
206    pub user_quota_period_seconds: u64,
207    /// Users have a cache quota per period. For example: 100MB per day.
208    /// This is the maximum size in bytes (100MB in bytes).    
209    pub user_quota_bytes: usize,
210}
211
212/// Core configuration for the API
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct Api {
215    /// The default page size for torrent lists.
216    pub default_torrent_page_size: u8,
217    /// The maximum page size for torrent lists.
218    pub max_torrent_page_size: u8,
219}
220
221impl Default for Api {
222    fn default() -> Self {
223        Self {
224            default_torrent_page_size: 10,
225            max_torrent_page_size: 30,
226        }
227    }
228}
229
230/// Configuration for the tracker statistics importer.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct TrackerStatisticsImporter {
233    /// The interval in seconds to get statistics from the tracker.
234    pub torrent_info_update_interval: u64,
235}
236
237impl Default for TrackerStatisticsImporter {
238    fn default() -> Self {
239        Self {
240            torrent_info_update_interval: 3600,
241        }
242    }
243}
244
245impl Default for ImageCache {
246    fn default() -> Self {
247        Self {
248            max_request_timeout_ms: 1000,
249            capacity: 128_000_000,
250            entry_size_limit: 4_000_000,
251            user_quota_period_seconds: 3600,
252            user_quota_bytes: 64_000_000,
253        }
254    }
255}
256
257/// The whole configuration for the backend.
258#[derive(Debug, Default, Clone, Serialize, Deserialize)]
259pub struct TorrustBackend {
260    /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`,
261    /// `Debug` and `Trace`. Default is `Info`.
262    pub log_level: Option<String>,
263    /// The website customizable values.
264    pub website: Website,
265    /// The tracker configuration.
266    pub tracker: Tracker,
267    /// The network configuration.
268    pub net: Network,
269    /// The authentication configuration.
270    pub auth: Auth,
271    /// The database configuration.
272    pub database: Database,
273    /// The SMTP configuration.
274    pub mail: Mail,
275    /// The image proxy cache configuration.
276    pub image_cache: ImageCache,
277    /// The API configuration.
278    pub api: Api,
279    /// The tracker statistics importer job configuration.
280    pub tracker_statistics_importer: TrackerStatisticsImporter,
281}
282
283/// The configuration service.
284#[derive(Debug)]
285pub struct Configuration {
286    /// The state of the configuration.
287    pub settings: RwLock<TorrustBackend>,
288    /// The path to the configuration file. This is `None` if the configuration
289    /// was loaded from the environment.
290    pub config_path: Option<String>,
291}
292
293impl Default for Configuration {
294    fn default() -> Configuration {
295        Configuration {
296            settings: RwLock::new(TorrustBackend::default()),
297            config_path: None,
298        }
299    }
300}
301
302impl Configuration {
303    /// Loads the configuration from the configuration file.
304    ///
305    /// # Errors
306    ///
307    /// This function will return an error no configuration in the `CONFIG_PATH` exists, and a new file is is created.
308    /// This function will return an error if the `config` is not a valid `TorrustConfig` document.
309    pub async fn load_from_file(config_path: &str) -> Result<Configuration, ConfigError> {
310        let config_builder = Config::builder();
311
312        #[allow(unused_assignments)]
313        let mut config = Config::default();
314
315        if Path::new(config_path).exists() {
316            config = config_builder.add_source(File::with_name(config_path)).build()?;
317        } else {
318            warn!("No config file found. Creating default config file ...");
319
320            let config = Configuration::default();
321            let _ = config.save_to_file(config_path).await;
322
323            return Err(ConfigError::Message(format!(
324                "No config file found. Created default config file in {config_path}. Edit the file and start the application."
325            )));
326        }
327
328        let torrust_config: TorrustBackend = match config.try_deserialize() {
329            Ok(data) => Ok(data),
330            Err(e) => Err(ConfigError::Message(format!("Errors while processing config: {e}."))),
331        }?;
332
333        Ok(Configuration {
334            settings: RwLock::new(torrust_config),
335            config_path: Some(config_path.to_string()),
336        })
337    }
338
339    /// Loads the configuration from the environment variable. The whole
340    /// configuration must be in the environment variable. It contains the same
341    /// configuration as the configuration file with the same format.
342    ///
343    /// # Errors
344    ///
345    /// Will return `Err` if the environment variable does not exist or has a bad configuration.
346    pub fn load_from_env_var(config_env_var_name: &str) -> Result<Configuration, ConfigError> {
347        match env::var(config_env_var_name) {
348            Ok(config_toml) => {
349                let config_builder = Config::builder()
350                    .add_source(File::from_str(&config_toml, FileFormat::Toml))
351                    .build()?;
352                let torrust_config: TorrustBackend = config_builder.try_deserialize()?;
353                Ok(Configuration {
354                    settings: RwLock::new(torrust_config),
355                    config_path: None,
356                })
357            }
358            Err(_) => Err(ConfigError::Message(
359                "Unable to load configuration from the configuration environment variable.".to_string(),
360            )),
361        }
362    }
363
364    /// Returns the save to file of this [`Configuration`].
365    ///
366    /// # Panics
367    ///
368    /// This function will panic if it can't write to the file.
369    pub async fn save_to_file(&self, config_path: &str) {
370        let settings = self.settings.read().await;
371
372        let toml_string = toml::to_string(&*settings).expect("Could not encode TOML value");
373
374        drop(settings);
375
376        fs::write(config_path, toml_string).expect("Could not write to file!");
377    }
378
379    pub async fn get_all(&self) -> TorrustBackend {
380        let settings_lock = self.settings.read().await;
381
382        settings_lock.clone()
383    }
384
385    pub async fn get_public(&self) -> ConfigurationPublic {
386        let settings_lock = self.settings.read().await;
387
388        ConfigurationPublic {
389            website_name: settings_lock.website.name.clone(),
390            tracker_url: settings_lock.tracker.url.clone(),
391            tracker_mode: settings_lock.tracker.mode.clone(),
392            email_on_signup: settings_lock.auth.email_on_signup.clone(),
393        }
394    }
395
396    pub async fn get_site_name(&self) -> String {
397        let settings_lock = self.settings.read().await;
398
399        settings_lock.website.name.clone()
400    }
401
402    pub async fn get_api_base_url(&self) -> Option<String> {
403        let settings_lock = self.settings.read().await;
404
405        settings_lock.net.base_url.clone()
406    }
407}
408
409/// The public backend configuration.
410/// There is an endpoint to get this configuration.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ConfigurationPublic {
413    website_name: String,
414    tracker_url: String,
415    tracker_mode: TrackerMode,
416    email_on_signup: EmailOnSignup,
417}