autopulse_service/settings/
mod.rs

1use app::App;
2use auth::Auth;
3use config::Config;
4use opts::Opts;
5use serde::{Deserialize, Serialize};
6use std::env;
7use std::{collections::HashMap, path::PathBuf};
8use targets::Target;
9use triggers::manual::Manual;
10use triggers::Trigger;
11use webhooks::Webhook;
12
13/// App-specific settings
14///
15/// Example:
16///
17/// ```yml
18/// app:
19///   hostname: 0.0.0.0
20///   port: 1234
21///   database_url: sqlite://autopulse.db
22///   log_level: debug
23/// ```
24pub mod app;
25
26/// Authentication settings
27///
28/// Example:
29///
30/// ```yml
31/// auth:
32///   username: terry
33///   password: yogurt
34/// ```
35pub mod auth;
36
37/// Global settings
38///
39/// Example:
40///
41/// ```yml
42/// opts:
43///   check_path: true
44///   max_retries: 10
45///   default_timer_wait: 300
46///   cleanup_days: 7
47/// ```
48pub mod opts;
49
50/// Rewrite structure for triggers
51///
52/// Example:
53///
54/// ```yml
55/// triggers:
56///   sonarr:
57///     type: sonarr
58///     rewrite:
59///       from: /tv
60///       to: /media/tv
61pub use autopulse_utils::rewrite;
62
63/// Timer structure for triggers
64///
65/// Example:
66///
67/// ```yml
68/// triggers:
69///  sonarr:
70///   type: sonarr
71///   timer:
72///    wait: 300 # wait 5 minutes before processing
73/// ```
74pub mod timer;
75
76/// Trigger structure
77///
78/// [Triggers](triggers) for all triggers
79pub mod triggers;
80
81/// Target structure
82///
83/// [Targets](targets) for all targets
84pub mod targets;
85
86/// Webhook structure
87///
88/// [Webhooks](webhooks) for all webhooks
89pub mod webhooks;
90
91#[doc(hidden)]
92pub fn default_triggers() -> HashMap<String, Trigger> {
93    let mut triggers = HashMap::new();
94
95    triggers.insert(
96        "manual".to_string(),
97        Trigger::Manual(Manual {
98            rewrite: None,
99            timer: None,
100            excludes: vec![],
101        }),
102    );
103
104    triggers
105}
106
107#[derive(Serialize, Deserialize, Clone)]
108pub struct Settings {
109    #[serde(default)]
110    pub app: App,
111
112    #[serde(default)]
113    pub auth: Auth,
114
115    #[serde(default)]
116    pub opts: Opts,
117
118    #[serde(default = "default_triggers")]
119    pub triggers: HashMap<String, Trigger>,
120    #[serde(default)]
121    pub targets: HashMap<String, Target>,
122
123    #[serde(default)]
124    pub webhooks: HashMap<String, Webhook>,
125
126    /// List of paths to anchor the service to
127    ///
128    /// This is useful to prevent the service notifying a target when the drive is not mounted or visible
129    /// The contents of the file/directory are not tampered with, only the presence of the file/directory is checked
130    ///
131    /// Example:
132    /// ```yml
133    /// anchors:
134    ///  - /mnt/media/tv # Directory
135    ///  - /mnt/media/anchor # File
136    /// ```
137    #[serde(default)]
138    pub anchors: Vec<PathBuf>,
139}
140
141impl Default for Settings {
142    fn default() -> Self {
143        Self {
144            app: App::default(),
145            auth: Auth::default(),
146            opts: Opts::default(),
147            triggers: default_triggers(),
148            targets: HashMap::new(),
149            webhooks: HashMap::new(),
150            anchors: vec![],
151        }
152    }
153}
154
155impl Settings {
156    fn resolve_env() -> HashMap<String, String> {
157        let mut out = HashMap::new();
158
159        for (key, value) in env::vars() {
160            if let Some(base) = key.strip_suffix("__FILE") {
161                if let Ok(file_value) = std::fs::read_to_string(&value) {
162                    out.insert(base.to_string(), file_value.trim().to_string());
163                    continue;
164                }
165            }
166
167            out.entry(key).or_insert(value);
168        }
169
170        out
171    }
172
173    pub fn get_settings(optional_config_file: Option<String>) -> anyhow::Result<Self> {
174        let mut settings = Config::builder()
175            .add_source(config::File::with_name("config").required(false))
176            .add_source(
177                config::Environment::with_prefix("AUTOPULSE")
178                    .separator("__")
179                    .source(Some(Self::resolve_env())),
180            );
181
182        if let Some(file_loc) = optional_config_file {
183            settings = settings.add_source(config::File::with_name(&file_loc));
184        }
185
186        let settings = settings.build()?;
187
188        let mut settings = settings
189            .try_deserialize::<Self>()
190            .map_err(|e| anyhow::anyhow!(e))?;
191
192        settings.add_default_manual_trigger()?;
193
194        Ok(settings)
195    }
196
197    pub fn add_default_manual_trigger(&mut self) -> anyhow::Result<()> {
198        if !self.triggers.contains_key("manual") {
199            self.triggers.insert(
200                "manual".to_string(),
201                Trigger::Manual(Manual {
202                    rewrite: None,
203                    timer: None,
204                    excludes: vec![],
205                }),
206            );
207        }
208
209        Ok(())
210    }
211}