1use crate::{Mode, MyLanguageTag};
4use base64::{prelude::BASE64_STANDARD, Engine};
5use chrono::TimeDelta;
6use dotenvy::var;
7use std::{
8 num::NonZeroUsize,
9 path::{self, Path, PathBuf},
10 str::FromStr,
11 sync::OnceLock,
12 time::Duration,
13};
14use tracing::{info, warn};
15
16const DEFAULT_TTL_BATCH_LEN: &str = "50";
19const DEFAULT_TTL_SECS: &str = "30";
20const DEFAULT_TTL_INTERVAL_SECS: &str = "60";
21
22const DEFAULT_MFC_INTERVAL_SECS: &str = "10";
23
24const DEPRECATION_MSG1: &str =
25 "LRS_AUTHORITY_IFI is now deprecated and will be removed in future release.\nUse LRS_ROOT_EMAIL instead.";
26
27static CONFIG: OnceLock<Config> = OnceLock::new();
28pub fn config() -> &'static Config {
30 CONFIG.get_or_init(Config::default)
31}
32
33#[derive(Debug)]
35pub struct Config {
36 pub(crate) db_server_url: String,
37 pub(crate) db_name: String,
38 pub(crate) db_max_connections: u32,
39 pub(crate) db_min_connections: u32,
40 pub(crate) db_acquire_timeout: Duration,
41 pub(crate) db_idle_timeout: Duration,
42 pub(crate) db_max_lifetime: Duration,
43 pub(crate) db_statements_page_len: i32,
44
45 pub external_url: String,
47 pub(crate) static_dir: PathBuf,
48 pub mode: Mode,
51 pub(crate) root_email: String,
52 pub(crate) root_credentials: Option<u32>,
53 pub(crate) user_cache_len: NonZeroUsize,
54
55 pub(crate) ttl_batch_len: i32,
56 pub(crate) ttl: TimeDelta,
57 pub(crate) ttl_interval: u64,
58
59 pub(crate) mfc_interval: u64,
60
61 pub(crate) default_language: String,
62
63 pub jws_strict: bool,
78}
79
80impl Default for Config {
81 fn default() -> Self {
82 let db_server_url = var("DB_SERVER_URL").expect("Missing DB_SERVERL_URL");
83 let db_name = var("DB_NAME").expect("Missing DB_NAME");
84
85 let db_max_connections: u32 = var("DB_MAX_CONNECTIONS")
86 .unwrap_or("8".to_string())
87 .parse()
88 .expect("Failed parsing DB_MAX_CONNECTIONS");
89 let db_min_connections: u32 = var("DB_MIN_CONNECTIONS")
90 .unwrap_or("4".to_string())
91 .parse()
92 .expect("Failed parsing DB_MIN_CONNECTIONS");
93 let db_acquire_timeout = Duration::from_secs(
94 var("DB_ACQUIRE_TIMEOUT_SECS")
95 .unwrap_or("8".to_string())
96 .parse()
97 .expect("Failed parsing DB_ACQUIRE_TIMEOUT_SECS"),
98 );
99 let db_idle_timeout = Duration::from_secs(
100 var("DB_IDLE_TIMEOUT_SECS")
101 .unwrap_or("8".to_string())
102 .parse()
103 .expect("Failed parsing DB_IDLE_TIMEOUT_SECS"),
104 );
105 let db_max_lifetime = Duration::from_secs(
106 var("DB_MAX_LIFETIME_SECS")
107 .unwrap_or("8".to_string())
108 .parse()
109 .expect("Failed parsing DB_MAX_LIFETIME_SECS"),
110 );
111
112 let db_statements_page_len: i32 = var("DB_STATEMENTS_PAGE_LEN")
113 .unwrap_or("20".to_string())
114 .parse()
115 .expect("Failed parsing DB_STATEMENTS_PAGE_LEN");
116 assert!(
118 db_statements_page_len > 0,
119 "DB_STATEMENTS_PAGE_LEN must be greater than 0"
120 );
121
122 let mut external_url = var("LRS_EXTERNAL_URL").expect("Missing LRS_EXTERNAL_URL");
123 if external_url.ends_with(path::MAIN_SEPARATOR) {
124 external_url.pop();
125 }
126 let home_dir = my_home_dir();
127 let static_dir = Path::new(&home_dir).join("static").to_owned();
128
129 let mode: Mode = var("LRS_MODE")
130 .unwrap_or("legacy".to_owned())
131 .as_str()
132 .try_into()
133 .unwrap();
134 info!("*** LaRS will be running in {:?} mode", mode);
135 let root_email = match var("LRS_ROOT_EMAIL") {
136 Ok(x) => x,
137 Err(_) => match var("LRS_AUTHORITY_IFI") {
138 Ok(x) => {
139 warn!("{}", DEPRECATION_MSG1);
140 x
141 }
142 Err(_) => panic!(
143 "Both LRS_ROOT_EMAIL and LRS_AUTHORITY_IFI are missing or contain invalid Unicode characters"
144 ),
145 },
146 };
147 let root_credentials = match var("LRS_ROOT_PASSWORD") {
153 Ok(x) => {
154 let token = format!("{}:{}", root_email.as_str(), &x);
155 let encoded = BASE64_STANDARD.encode(token);
156 let hashed = fxhash::hash32(&encoded);
157 Some(hashed)
158 }
159 Err(_) => {
160 info!("Missing LRS_ROOT_PASSWORD. Will only operate in Legacy mode");
161 None
162 }
163 };
164 let user_cache_len = NonZeroUsize::new(
165 var("LRS_USER_CACHE_LEN")
166 .unwrap_or("100".to_string())
167 .parse()
168 .expect("Failed parsing LRS_USER_CACHE_LEN"),
169 )
170 .expect("Failed converting LRS_USER_CACHE_LEN to unsigned integer");
171 if let Ok(x) = var("LRS_AUTHORITY_IFI") {
173 if x != root_email {
174 warn!("LRS_AUTHORITY_IFI is different than LRS_ROOT_EMAIL. Ignore + continue");
175 }
176 warn!("{}", DEPRECATION_MSG1);
177 }
178
179 let ttl_batch_len = i32::try_from(
181 var("TTL_BATCH_LEN")
182 .unwrap_or(DEFAULT_TTL_BATCH_LEN.to_string())
183 .parse::<u32>()
184 .expect("Failed parsing TTL_BATCH_LEN"),
185 )
186 .expect("Failed converting TTL_BATCH_LEN to i32");
187
188 let ttl_secs: usize = var("TTL_SECS")
189 .unwrap_or(DEFAULT_TTL_SECS.to_string())
190 .parse()
191 .expect("Failed parsing TTL_SECS");
192 let ttl = TimeDelta::new(
193 i64::try_from(ttl_secs).expect("Failed converting TTL_SECS to i64"),
194 0,
195 )
196 .expect("Failed converting TTL_SECS to TimeDelta");
197
198 let ttl_interval: u64 = var("TTL_INTERVAL_SECS")
199 .unwrap_or(DEFAULT_TTL_INTERVAL_SECS.to_string())
200 .parse()
201 .expect("Failed parsing TTL_INTERVAL_SECS");
202
203 let mfc_interval: u64 = var("MFC_INTERVAL_SECS")
204 .unwrap_or(DEFAULT_MFC_INTERVAL_SECS.to_string())
205 .parse()
206 .expect("Failed parsing MFC_INTERVAL_SECS");
207
208 let default_language = var("EXT_DEFAULT_LANGUAGE").expect("Missing EXT_DEFAULT_LANGUAGE");
209 let _ = MyLanguageTag::from_str(&default_language).expect("Invalid default language tag");
211
212 let jws_strict: bool = var("JWS_STRICT")
213 .unwrap_or("false".to_owned())
214 .parse()
215 .expect("Failed parsing JWS_STRICT");
216
217 Self {
218 db_server_url,
219 db_name,
220 db_max_connections,
221 db_min_connections,
222 db_acquire_timeout,
223 db_idle_timeout,
224 db_max_lifetime,
225 db_statements_page_len,
226 external_url,
227 static_dir,
228 mode,
229 root_email,
230 root_credentials,
231 user_cache_len,
232 ttl_batch_len,
233 ttl,
234 ttl_interval,
235 mfc_interval,
236 default_language,
237 jws_strict,
238 }
239 }
240}
241
242impl Config {
243 pub fn to_external_url(&self, partial: &str) -> String {
245 let mut url = self.external_url.clone();
246 if !partial.starts_with(path::MAIN_SEPARATOR) {
247 url.push(path::MAIN_SEPARATOR);
248 }
249 url.push_str(partial);
250 url
251 }
252
253 pub fn is_legacy(&self) -> bool {
255 matches!(self.mode, Mode::Legacy)
256 }
257}
258
259fn my_home_dir() -> String {
260 let mut result = var("CARGO_MANIFEST_DIR").expect("Failed accessing Cargo vars...");
261 if result.ends_with(path::MAIN_SEPARATOR) {
262 result.pop();
263 }
264 result
265}