From c28eab74dd52c04104527b0c25e7acfb3dac13b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Sun, 28 Dec 2025 23:44:50 +0100 Subject: [PATCH] Cached config operations --- src/api/web.rs | 32 +++++++++-------- src/config.rs | 94 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/api/web.rs b/src/api/web.rs index 98d51a5e..b2ab1e44 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -12,6 +12,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, + config::CachedConfigOperation, db::models::{AttachmentId, CipherId}, error::Error, util::Cached, @@ -52,19 +53,18 @@ fn not_found() -> ApiResult> { Ok(Html(text)) } -#[get("/css/vaultwarden.css")] -fn vaultwarden_css() -> Cached> { +static VAULTWARDEN_CSS_CACHE: CachedConfigOperation = CachedConfigOperation::new(|config| { let css_options = json!({ - "emergency_access_allowed": CONFIG.emergency_access_allowed(), + "emergency_access_allowed": config.emergency_access_allowed(), "load_user_scss": true, - "mail_2fa_enabled": CONFIG._enable_email_2fa(), - "mail_enabled": CONFIG.mail_enabled(), - "sends_allowed": CONFIG.sends_allowed(), - "signup_disabled": CONFIG.is_signup_disabled(), - "sso_enabled": CONFIG.sso_enabled(), - "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), - "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), - "webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), + "mail_2fa_enabled": config._enable_email_2fa(), + "mail_enabled": config.mail_enabled(), + "sends_allowed": config.sends_allowed(), + "signup_disabled": config.is_signup_disabled(), + "sso_enabled": config.sso_enabled(), + "sso_only": config.sso_enabled() && config.sso_only(), + "yubico_enabled": config._enable_yubico() && config.yubico_client_id().is_some() && config.yubico_secret_key().is_some(), + "webauthn_2fa_supported": config.is_webauthn_2fa_supported(), }); let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { @@ -78,7 +78,7 @@ fn vaultwarden_css() -> Cached> { } }; - let css = match grass_compiler::from_string( + match grass_compiler::from_string( scss, &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed), ) { @@ -97,10 +97,12 @@ fn vaultwarden_css() -> Cached> { ) .expect("SCSS to compile") } - }; + } +}); - // Cache for one day should be enough and not too much - Cached::ttl(Css(css), 86_400, false) +#[get("/css/vaultwarden.css")] +fn vaultwarden_css() -> Css { + Css(CONFIG.cached_operation(&VAULTWARDEN_CSS_CACHE)) } #[get("/")] diff --git a/src/config.rs b/src/config.rs index 812b12f6..93dcd166 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{ fmt, process::exit, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, LazyLock, RwLock, }, }; @@ -103,6 +103,7 @@ macro_rules! make_config { struct Inner { rocket_shutdown_handle: Option, + revision: usize, templates: Handlebars<'static>, config: ConfigItems, @@ -322,7 +323,7 @@ macro_rules! make_config { } #[derive(Clone, Default)] - struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ } + struct ConfigItems { $($( pub $name: make_config! {@type $ty, $none_action}, )+)+ } #[derive(Serialize)] struct ElementDoc { @@ -1467,6 +1468,23 @@ pub enum PathType { RsaKey, } +pub struct CachedConfigOperation { + generator: fn(&Config) -> T, + value_cache: RwLock>, + revision: AtomicUsize, +} + +impl CachedConfigOperation { + #[allow(private_interfaces)] + pub const fn new(generator: fn(&Config) -> T) -> Self { + CachedConfigOperation { + generator, + value_cache: RwLock::new(None), + revision: AtomicUsize::new(0), + } + } +} + impl Config { pub async fn load() -> Result { // Loading from env and file @@ -1486,6 +1504,7 @@ impl Config { Ok(Config { inner: RwLock::new(Inner { rocket_shutdown_handle: None, + revision: 1, templates: load_templates(&config.templates_folder), config, _env, @@ -1524,6 +1543,7 @@ impl Config { writer.config = config; writer._usr = builder; writer._overrides = overrides; + writer.revision += 1; } //Save to file @@ -1542,6 +1562,51 @@ impl Config { self.update_config(builder, false).await } + pub async fn delete_user_config(&self) -> Result<(), Error> { + let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + operator.delete(&CONFIG_FILENAME).await?; + + // Empty user config + let usr = ConfigBuilder::default(); + + // Config now is env + defaults + let config = { + let env = &self.inner.read().unwrap()._env; + env.build() + }; + + // Save configs + { + let mut writer = self.inner.write().unwrap(); + writer.config = config; + writer._usr = usr; + writer._overrides = Vec::new(); + writer.revision += 1; + } + + Ok(()) + } + + pub fn cached_operation(&self, operation: &CachedConfigOperation) -> T { + let config_revision = self.inner.read().unwrap().revision; + let cache_revision = operation.revision.load(Ordering::Relaxed); + + // If the current revision matches the cached revision, return the cached value + if cache_revision == config_revision { + let reader = operation.value_cache.read().unwrap(); + return reader.as_ref().unwrap().clone(); + } + + // Otherwise, compute the value, update the cache and revision, and return the new value + let value = (operation.generator)(&CONFIG); + { + let mut writer = operation.value_cache.write().unwrap(); + *writer = Some(value.clone()); + operation.revision.store(config_revision, Ordering::Relaxed); + } + value + } + /// Tests whether an email's domain is allowed. A domain is allowed if it /// is in signups_domains_whitelist, or if no whitelist is set (so there /// are no domain restrictions in effect). @@ -1591,33 +1656,10 @@ impl Config { } } - pub async fn delete_user_config(&self) -> Result<(), Error> { - let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; - operator.delete(&CONFIG_FILENAME).await?; - - // Empty user config - let usr = ConfigBuilder::default(); - - // Config now is env + defaults - let config = { - let env = &self.inner.read().unwrap()._env; - env.build() - }; - - // Save configs - { - let mut writer = self.inner.write().unwrap(); - writer.config = config; - writer._usr = usr; - writer._overrides = Vec::new(); - } - - Ok(()) - } - pub fn private_rsa_key(&self) -> String { format!("{}.pem", self.rsa_key_filename()) } + pub fn mail_enabled(&self) -> bool { let inner = &self.inner.read().unwrap().config; inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)