Skip to main content

yggdrasil/api/settings/
image_cache.rs

1// 与 posts 模块一致:Dioxus `#[server]` 宏触发 deprecated/unit/too_many_arguments
2// 提示,按项目惯例放行(限流/运行器等配置项天然参数多)。
3#![allow(clippy::unused_unit, deprecated, clippy::too_many_arguments)]
4
5use dioxus::prelude::*;
6
7#[cfg(feature = "server")]
8use crate::api::auth::get_current_admin_user;
9#[cfg(feature = "server")]
10use crate::api::error::AppError;
11#[cfg(feature = "server")]
12use crate::db::pool::get_conn;
13use crate::models::settings::ImageCacheSettings;
14// 仅 server 构建的函数体引用(WASM 端 server fn 体被宏剥离)。
15#[cfg(feature = "server")]
16use crate::cache::invalidate_image_cache_settings;
17
18// ============================================================================
19// 图片磁盘缓存配置(即时生效)
20// ============================================================================
21
22/// 启动时用 `IMAGE_DISK_CACHE_MAX_MB` / `_MAX_AGE_HOURS` 环境变量播种。
23///
24/// 语义与 [`seed_backup_settings_from_env`][crate::api::settings::seed_backup_settings_from_env] 一致。
25#[cfg(feature = "server")]
26pub(crate) async fn seed_image_cache_settings_from_env(
27    client: &tokio_postgres::Client,
28) -> Result<(), AppError> {
29    let mut seeds: Vec<(&'static str, String)> = Vec::new();
30
31    if let Ok(v) = std::env::var("IMAGE_DISK_CACHE_MAX_MB") {
32        match v.trim().parse::<u32>() {
33            Ok(n) => seeds.push((
34                "image_disk_cache_max_mb",
35                crate::models::settings::ImageCacheSettings::clamp_max_mb(n).to_string(),
36            )),
37            Err(_) => tracing::warn!("IMAGE_DISK_CACHE_MAX_MB={v:?} 非法(期望正整数),跳过"),
38        }
39    }
40    if let Ok(v) = std::env::var("IMAGE_DISK_CACHE_MAX_AGE_HOURS") {
41        match v.trim().parse::<u32>() {
42            Ok(n) => seeds.push((
43                "image_disk_cache_max_age_hours",
44                crate::models::settings::ImageCacheSettings::clamp_max_age_hours(n).to_string(),
45            )),
46            Err(_) => {
47                tracing::warn!("IMAGE_DISK_CACHE_MAX_AGE_HOURS={v:?} 非法(期望正整数),跳过")
48            }
49        }
50    }
51
52    for (key, value) in seeds {
53        client
54            .execute(
55                "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
56                &[&key, &value],
57            )
58            .await
59            .map_err(AppError::query)?;
60        tracing::info!("图片缓存配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
61    }
62    Ok(())
63}
64
65/// 从 settings 表读取图片磁盘缓存配置(缺键回退默认值)。
66#[cfg(feature = "server")]
67pub(crate) async fn load_image_cache_settings(
68    client: &tokio_postgres::Client,
69) -> Result<crate::models::settings::ImageCacheSettings, AppError> {
70    async fn read_key(
71        client: &tokio_postgres::Client,
72        key: &str,
73    ) -> Result<Option<String>, AppError> {
74        let row = client
75            .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
76            .await
77            .map_err(AppError::query)?;
78        Ok(row.map(|r| r.get::<_, String>("value")))
79    }
80
81    let disk_cache_max_mb = read_key(client, "image_disk_cache_max_mb")
82        .await?
83        .and_then(|v| v.parse().ok())
84        .map(crate::models::settings::ImageCacheSettings::clamp_max_mb)
85        .unwrap_or(crate::models::settings::DEFAULT_IMAGE_DISK_CACHE_MAX_MB);
86    let disk_cache_max_age_hours = read_key(client, "image_disk_cache_max_age_hours")
87        .await?
88        .and_then(|v| v.parse().ok())
89        .map(crate::models::settings::ImageCacheSettings::clamp_max_age_hours)
90        .unwrap_or(crate::models::settings::DEFAULT_IMAGE_DISK_CACHE_MAX_AGE_HOURS);
91
92    Ok(crate::models::settings::ImageCacheSettings {
93        disk_cache_max_mb,
94        disk_cache_max_age_hours,
95    })
96}
97
98/// 运行时读取图片磁盘缓存配置:先查 moka 缓存,未命中查 DB 并回填。
99#[cfg(feature = "server")]
100pub(crate) async fn runtime_image_cache_settings() -> crate::models::settings::ImageCacheSettings {
101    if let Some(s) = crate::cache::get_image_cache_settings().await {
102        return s;
103    }
104    let fallback = crate::models::settings::ImageCacheSettings::default();
105    // 无 DATABASE_URL 时(如单元测试环境)直接回退默认值,
106    // 避免触发 DB_POOL LazyLock 的防御性 panic。
107    if std::env::var("DATABASE_URL").is_err() {
108        return fallback;
109    }
110    match get_conn().await {
111        Ok(client) => match load_image_cache_settings(&client).await {
112            Ok(s) => {
113                crate::cache::set_image_cache_settings(s.clone()).await;
114                s
115            }
116            Err(e) => {
117                tracing::warn!("读取图片缓存配置失败,回退默认值:{e:?}");
118                fallback
119            }
120        },
121        Err(e) => {
122            tracing::warn!("获取连接读取图片缓存配置失败,回退默认值:{e:?}");
123            fallback
124        }
125    }
126}
127
128/// 读取图片磁盘缓存配置(面板用)。
129#[server(GetImageCacheSettings, "/api")]
130pub async fn get_image_cache_settings() -> Result<ImageCacheSettings, ServerFnError> {
131    let _user = get_current_admin_user().await?;
132
133    #[cfg(feature = "server")]
134    {
135        let client = get_conn().await.map_err(AppError::db_conn)?;
136        let s = load_image_cache_settings(&client)
137            .await
138            .map_err(ServerFnError::from)?;
139        crate::cache::set_image_cache_settings(s.clone()).await;
140        Ok(s)
141    }
142
143    #[cfg(not(feature = "server"))]
144    {
145        Ok(ImageCacheSettings::default())
146    }
147}
148
149/// 更新图片磁盘缓存配置。
150#[server(UpdateImageCacheSettings, "/api")]
151pub async fn update_image_cache_settings(
152    disk_cache_max_mb: u32,
153    disk_cache_max_age_hours: u32,
154) -> Result<ImageCacheSettings, ServerFnError> {
155    let _user = get_current_admin_user().await?;
156
157    let disk_cache_max_mb = ImageCacheSettings::clamp_max_mb(disk_cache_max_mb);
158    let disk_cache_max_age_hours =
159        ImageCacheSettings::clamp_max_age_hours(disk_cache_max_age_hours);
160
161    #[cfg(feature = "server")]
162    {
163        let client = get_conn().await.map_err(AppError::db_conn)?;
164
165        for (key, value) in [
166            ("image_disk_cache_max_mb", disk_cache_max_mb.to_string()),
167            (
168                "image_disk_cache_max_age_hours",
169                disk_cache_max_age_hours.to_string(),
170            ),
171        ] {
172            client
173                .execute(
174                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
175                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
176                    &[&key, &value],
177                )
178                .await
179                .map_err(AppError::query)?;
180        }
181
182        invalidate_image_cache_settings();
183        tracing::info!(
184            "Image cache settings updated: max_mb={}, max_age_hours={}",
185            disk_cache_max_mb,
186            disk_cache_max_age_hours
187        );
188    }
189
190    Ok(ImageCacheSettings {
191        disk_cache_max_mb,
192        disk_cache_max_age_hours,
193    })
194}