Skip to main content

server/
config.rs

1use crate::db::get_connection;
2use derive_more::From;
3use rust_i18n::t;
4use sqlx::Error::RowNotFound;
5use std::env::var;
6use std::fmt;
7use std::ops::Deref;
8use std::sync::LazyLock;
9use thiserror::Error;
10
11/// Represents an error that can occur while accessing the configuration.
12///
13/// # Variants
14///
15/// - `NoConfig(String)`: Returned when a specific configuration field is not found.
16/// - `DB`: Indicates a database access error.
17/// - `Sqlx`: An error propagated from the `SQLx` crate.
18///
19/// # Example
20///
21/// ```rust
22/// use thiserror::Error;
23///
24/// #[derive(Debug, Error)]
25/// pub enum ConfigError {
26///     #[error("No such config field: {0}")]
27///     NoConfig(String),
28///     #[error("Can't access db")]
29///     DB,
30///     #[error("Sqlx")]
31///     Sqlx(#[from] sqlx::Error),
32/// }
33/// ```
34#[derive(Debug, Error)]
35pub enum ConfigError {
36    #[error("No such config field: {0}")]
37    NoConfig(String),
38    #[error("Can't access db")]
39    DB,
40    #[error("Sqlx")]
41    Sqlx(#[from] sqlx::Error),
42}
43
44/// Represents a configuration value stored in the system.
45///
46/// # Variants
47///
48/// - `String`: Stores the configuration as a string.
49/// - `Blob`: Stores the configuration as a binary blob.
50///
51/// # Example
52///
53/// ```rust
54/// use server::config::ConfigOption;
55/// let config_str = ConfigOption::String("example".to_string());
56/// let config_blob = ConfigOption::Blob(vec![1, 2, 3, 4]);
57/// ```
58///
59/// You can also convert `String` and `Vec<u8>` directly into `ConfigOption`
60/// using the `From` trait:
61///
62/// ```rust
63/// use server::config::ConfigOption;
64/// let config: ConfigOption = "example".to_string().into();
65/// let blob: ConfigOption = vec![1, 2, 3, 4].into();
66/// ```
67#[derive(Debug, From, PartialEq, Clone)]
68pub enum ConfigOption {
69    String(#[from] String),
70    Blob(#[from] Vec<u8>),
71}
72
73impl fmt::Display for ConfigOption {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            ConfigOption::String(s) => write!(f, "{s}"),
77            ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
78        }
79    }
80}
81
82/// Convert `ConfigOption` into a `&str` reference when the variant is a `String`.
83///
84/// # Panics
85///
86/// This will panic if the `ConfigOption` is of type `Blob`, as it cannot be
87/// converted to a `&str`.
88///
89/// # Example
90///
91/// ```rust,should_panic
92/// use server::config::ConfigOption;
93/// let config = ConfigOption::Blob(vec![1, 2, 3]);
94/// let str_ref: &str = config.as_ref(); // Will panic
95/// ```
96impl AsRef<str> for ConfigOption {
97    fn as_ref(&self) -> &str {
98        match self {
99            ConfigOption::String(s) => s.as_str(),
100            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be converted to &str"),
101        }
102    }
103}
104
105/// Dereference `ConfigOption::String` to `&str`.
106///
107/// This allows you to use `ConfigOption` directly in contexts where a `&str`
108/// is expected, without having to manually call `.as_ref()`.
109///
110/// # Panics
111///
112/// This will panic if the `ConfigOption` is of type `Blob`.
113///
114/// # Example
115///
116/// ```rust,should_panic
117/// use server::config::ConfigOption;
118/// let config = ConfigOption::Blob(vec![1, 2, 3]);
119/// let str_ref: &str = &*config; // Will panic
120/// ```
121impl Deref for ConfigOption {
122    type Target = str;
123
124    fn deref(&self) -> &Self::Target {
125        match self {
126            ConfigOption::String(s) => s.as_str(),
127            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be dereferenced as &str"),
128        }
129    }
130}
131
132/// Allows for the conversion from a `&str` to `ConfigOption`.
133///
134/// This is useful for passing string literals or string slices directly as `ConfigOption`.
135/// The conversion will wrap the `&str` in the `ConfigOption::String` variant.
136///
137/// # Example
138///
139/// ```rust
140/// use server::config::ConfigOption;
141/// let config_option: ConfigOption = "example text".into();
142/// assert_eq!(config_option, ConfigOption::String("example text".to_string()));
143/// ```
144impl From<&str> for ConfigOption {
145    fn from(value: &str) -> Self {
146        ConfigOption::String(value.to_string())
147    }
148}
149
150/// Convert `ConfigOption` into a `&[u8]` reference when the variant is a `Blob`.
151///
152/// # Example
153///
154/// ```rust
155/// use server::config::ConfigOption;
156/// let config = ConfigOption::Blob(vec![1, 2, 3]);
157/// let byte_ref: &[u8] = config.as_ref(); // Works
158/// ```
159impl AsRef<[u8]> for ConfigOption {
160    fn as_ref(&self) -> &[u8] {
161        match self {
162            ConfigOption::String(s) => s.as_bytes(),
163            ConfigOption::Blob(b) => b.as_slice(),
164        }
165    }
166}
167
168// The config read/write logic is identical for the system (admin DB) and
169// per-user surfaces — only the connection differs. The `*_on` helpers take an
170// explicit connection so both surfaces share one implementation; `User::config`
171// / `User::set_config` (see `user.rs`) pass a per-user connection, while the
172// `system_*` wrappers below pass the global admin connection.
173
174/// Fetches the `contents` of a field from the configdata.
175///
176/// This function attempts to retrieve the configuration stored in the
177/// `configdata` table.  If the field is not found, it returns a
178/// `ConfigError::NoConfig`.
179///
180/// # Arguments
181///
182/// - `field`: The name of the field to retrieve.
183///
184/// # Returns
185///
186/// - `Ok(Some(ConfigOption))` if the field exists.
187/// - `Err(ConfigError::NoConfig)` if the field is absent from both the
188///   `config` and `configdata` tables (callers that treat "absent" as
189///   "not set" — e.g. the `GetConfig` command — normalise this to `Ok(None)`).
190/// - `Err(ConfigError)` on any other retrieval error.
191///
192/// # Example
193///
194/// ```rust,ignore
195/// let config = system_configdata("example_field").await?;
196/// if let Some(option) = config {
197///     println!("Config value: {}", option);
198/// }
199/// ```
200///
201/// # Errors
202///
203/// Returns a `ConfigError::DB` if there is an issue with the database connection.
204async fn configdata_on(
205    conn: &mut sqlx::PgConnection,
206    field: &str,
207) -> Result<Option<ConfigOption>, ConfigError> {
208    let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
209        .fetch_one(&mut *conn)
210        .await
211        .map_err(|err| {
212            if let RowNotFound = err {
213                ConfigError::NoConfig(String::from(field))
214            } else {
215                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
216                ConfigError::DB
217            }
218        })?;
219
220    Ok(Some(ConfigOption::Blob(value)))
221}
222
223/// Reads a config field on the given connection: the `config` (string) table
224/// first, falling back to `configdata` (blob).
225///
226/// # Errors
227/// `ConfigError::DB` on a database error.
228pub async fn config_on(
229    conn: &mut sqlx::PgConnection,
230    field: &str,
231) -> Result<Option<ConfigOption>, ConfigError> {
232    let value = sqlx::query_file_scalar!("../server/sql/select/config/string.sql", field)
233        .fetch_one(&mut *conn)
234        .await;
235    match value {
236        Ok(l) => Ok(Some(ConfigOption::String(l))),
237        Err(RowNotFound) => configdata_on(conn, field).await,
238        Err(err) => {
239            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
240            Err(ConfigError::DB)
241        }
242    }
243}
244
245/// Upserts a config field on the given connection (string → `config`, blob →
246/// `configdata`). The upsert keys on `lower(field)` (see the SQL), so it is
247/// idempotent and case-insensitive.
248///
249/// # Errors
250/// `ConfigError::DB` on a database error, `ConfigError::NoConfig` if the row is
251/// missing for an update.
252pub async fn set_config_on(
253    conn: &mut sqlx::PgConnection,
254    field: &str,
255    contents: ConfigOption,
256) -> Result<(), ConfigError> {
257    let id = uuid::Uuid::new_v4();
258    match contents {
259        ConfigOption::String(s) => {
260            sqlx::query_file!("../server/sql/set/config/string.sql", &id, field, s)
261        }
262        ConfigOption::Blob(b) => {
263            sqlx::query_file!("../server/sql/set/config/blob.sql", &id, field, b)
264        }
265    }
266    .execute(&mut *conn)
267    .await
268    .map_err(|err| {
269        if let RowNotFound = err {
270            ConfigError::NoConfig(String::from(field))
271        } else {
272            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
273            ConfigError::DB
274        }
275    })?;
276
277    Ok(())
278}
279
280/// Reads a SYSTEM (server-wide) config field from the global admin database.
281/// For server-wide keys (infra URLs, locale) and the bootstrap reads that run
282/// before any user is resolved. Per-user config goes through `User::config`.
283///
284/// # Errors
285/// `ConfigError::DB` on a database error.
286pub async fn system_config(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
287    let mut conn = get_connection().await.map_err(|err| {
288        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
289        ConfigError::DB
290    })?;
291    config_on(&mut conn, field).await
292}
293
294/// Reads a SYSTEM config blob field from the global admin database.
295///
296/// # Errors
297/// `ConfigError::DB` on a database error.
298pub async fn system_configdata(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
299    let mut conn = get_connection().await.map_err(|err| {
300        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
301        ConfigError::DB
302    })?;
303    configdata_on(&mut conn, field).await
304}
305
306/// Writes a SYSTEM config field to the global admin database.
307///
308/// # Errors
309/// `ConfigError::DB` on a database error.
310pub async fn set_system_config(field: &str, contents: ConfigOption) -> Result<(), ConfigError> {
311    let mut conn = get_connection().await.map_err(|err| {
312        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
313        ConfigError::DB
314    })?;
315    set_config_on(&mut conn, field, contents).await
316}
317
318static LOCALE_NAME: LazyLock<String> = LazyLock::new(|| var("LANG").unwrap_or(String::from("en")));
319
320pub async fn load_config() -> Result<(), ConfigError> {
321    if let Ok(Some(locale)) = system_config("locale").await {
322        rust_i18n::set_locale(&locale);
323        log::info!(
324            "{}",
325            t!(
326                "Loaded the %{loc} locale from config, setting as main",
327                loc = &locale
328            )
329        );
330    } else {
331        rust_i18n::set_locale(&LOCALE_NAME);
332    }
333    Ok(())
334}
335
336#[cfg(test)]
337mod config_tests {
338    use super::*;
339    use crate::db::DB_POOL;
340    use sqlx::PgPool;
341    use supp_macro::local_db_sqlx_test;
342    use tokio::sync::OnceCell;
343
344    /// Context for keeping environment intact
345    static CONTEXT: OnceCell<()> = OnceCell::const_new();
346
347    async fn setup() {
348        CONTEXT
349            .get_or_init(|| async {
350                #[cfg(feature = "testlog")]
351                let _ = env_logger::builder()
352                    .is_test(true)
353                    .filter_level(log::LevelFilter::Trace)
354                    .try_init();
355            })
356            .await;
357    }
358
359    #[local_db_sqlx_test]
360    async fn test_config_string(pool: PgPool) {
361        let opt = ConfigOption::String("testval".to_string());
362        set_system_config("testfield", opt.clone()).await.unwrap();
363        let val = system_config("testfield").await.unwrap().unwrap();
364        assert_eq!(val, opt);
365    }
366
367    #[local_db_sqlx_test]
368    async fn test_config_configdata(pool: PgPool) {
369        let opt = ConfigOption::Blob(vec![0, 1, 2, 3]);
370        set_system_config("testblob", opt.clone()).await.unwrap();
371        let val = system_config("testblob").await.unwrap().unwrap();
372        assert_eq!(val, opt);
373    }
374}