Skip to main content

yggdrasil/api/settings/
image_limit.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::ImageLimitSettings;
14
15// ============================================================================
16// 图片尺寸限制配置(需重启生效)
17// ============================================================================
18
19/// 启动时用 `MAX_IMAGE_DIMENSION` / `MAX_IMAGE_PIXELS` /
20/// `IMAGE_DIMENSIONS_CACHE_TTL_SECS` 环境变量播种图片尺寸限制配置。
21#[cfg(feature = "server")]
22pub(crate) async fn seed_image_limit_settings_from_env(
23    client: &tokio_postgres::Client,
24) -> Result<(), AppError> {
25    use crate::models::settings as m;
26
27    let mut seeds: Vec<(&'static str, String)> = Vec::new();
28
29    if let Ok(v) = std::env::var("MAX_IMAGE_DIMENSION") {
30        match v.trim().parse::<u32>() {
31            Ok(n) => seeds.push((
32                "image_max_dimension",
33                m::ImageLimitSettings::clamp_max_dimension(n).to_string(),
34            )),
35            Err(_) => tracing::warn!("MAX_IMAGE_DIMENSION={v:?} 非法(期望正整数),跳过"),
36        }
37    }
38    if let Ok(v) = std::env::var("MAX_IMAGE_PIXELS") {
39        match v.trim().parse::<u64>() {
40            Ok(n) => seeds.push((
41                "image_max_pixels",
42                m::ImageLimitSettings::clamp_max_pixels(n).to_string(),
43            )),
44            Err(_) => tracing::warn!("MAX_IMAGE_PIXELS={v:?} 非法(期望正整数),跳过"),
45        }
46    }
47    if let Ok(v) = std::env::var("IMAGE_DIMENSIONS_CACHE_TTL_SECS") {
48        match v.trim().parse::<u64>() {
49            Ok(n) => seeds.push((
50                "image_dimensions_cache_ttl_secs",
51                m::ImageLimitSettings::clamp_dimensions_cache_ttl_secs(n).to_string(),
52            )),
53            Err(_) => {
54                tracing::warn!("IMAGE_DIMENSIONS_CACHE_TTL_SECS={v:?} 非法(期望正整数),跳过")
55            }
56        }
57    }
58
59    for (key, value) in seeds {
60        client
61            .execute(
62                "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
63                &[&key, &value],
64            )
65            .await
66            .map_err(AppError::query)?;
67        tracing::info!("图片尺寸限制配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
68    }
69    Ok(())
70}
71
72/// 从 settings 表读取图片尺寸限制配置(缺键回退默认值)。
73#[cfg(feature = "server")]
74pub(crate) async fn load_image_limit_settings(
75    client: &tokio_postgres::Client,
76) -> Result<ImageLimitSettings, AppError> {
77    use crate::models::settings as m;
78
79    async fn read_key(
80        client: &tokio_postgres::Client,
81        key: &str,
82    ) -> Result<Option<String>, AppError> {
83        let row = client
84            .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
85            .await
86            .map_err(AppError::query)?;
87        Ok(row.map(|r| r.get::<_, String>("value")))
88    }
89
90    let max_dimension = read_key(client, "image_max_dimension")
91        .await?
92        .and_then(|v| v.parse().ok())
93        .map(m::ImageLimitSettings::clamp_max_dimension)
94        .unwrap_or(m::DEFAULT_IMAGE_MAX_DIMENSION);
95    let max_pixels = read_key(client, "image_max_pixels")
96        .await?
97        .and_then(|v| v.parse().ok())
98        .map(m::ImageLimitSettings::clamp_max_pixels)
99        .unwrap_or(m::DEFAULT_IMAGE_MAX_PIXELS);
100    let dimensions_cache_ttl_secs = read_key(client, "image_dimensions_cache_ttl_secs")
101        .await?
102        .and_then(|v| v.parse().ok())
103        .map(m::ImageLimitSettings::clamp_dimensions_cache_ttl_secs)
104        .unwrap_or(m::DEFAULT_IMAGE_DIMENSIONS_CACHE_TTL_SECS);
105
106    Ok(ImageLimitSettings {
107        max_dimension,
108        max_pixels,
109        dimensions_cache_ttl_secs,
110    })
111}
112
113/// 读取图片尺寸限制配置(面板用)。
114#[server(GetImageLimitSettings, "/api")]
115pub async fn get_image_limit_settings() -> Result<ImageLimitSettings, ServerFnError> {
116    let _user = get_current_admin_user().await?;
117
118    #[cfg(feature = "server")]
119    {
120        let client = get_conn().await.map_err(AppError::db_conn)?;
121        let s = load_image_limit_settings(&client)
122            .await
123            .map_err(ServerFnError::from)?;
124        Ok(s)
125    }
126
127    #[cfg(not(feature = "server"))]
128    {
129        Ok(ImageLimitSettings::default())
130    }
131}
132
133/// 更新图片尺寸限制配置。
134///
135/// 字段会被 clamp 后写入 DB。配置烘焙进 LazyLock,修改后需**重启进程**生效。
136#[server(UpdateImageLimitSettings, "/api")]
137pub async fn update_image_limit_settings(
138    max_dimension: u32,
139    max_pixels: u64,
140    dimensions_cache_ttl_secs: u64,
141) -> Result<ImageLimitSettings, ServerFnError> {
142    let _user = get_current_admin_user().await?;
143
144    let max_dimension = ImageLimitSettings::clamp_max_dimension(max_dimension);
145    let max_pixels = ImageLimitSettings::clamp_max_pixels(max_pixels);
146    let dimensions_cache_ttl_secs =
147        ImageLimitSettings::clamp_dimensions_cache_ttl_secs(dimensions_cache_ttl_secs);
148
149    #[cfg(feature = "server")]
150    {
151        let client = get_conn().await.map_err(AppError::db_conn)?;
152
153        for (key, value) in [
154            ("image_max_dimension", max_dimension.to_string()),
155            ("image_max_pixels", max_pixels.to_string()),
156            (
157                "image_dimensions_cache_ttl_secs",
158                dimensions_cache_ttl_secs.to_string(),
159            ),
160        ] {
161            client
162                .execute(
163                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
164                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
165                    &[&key, &value],
166                )
167                .await
168                .map_err(AppError::query)?;
169        }
170
171        tracing::info!(
172            "Image limit settings updated (需重启生效): max_dimension={}, max_pixels={}, ttl={}s",
173            max_dimension,
174            max_pixels,
175            dimensions_cache_ttl_secs
176        );
177    }
178
179    Ok(ImageLimitSettings {
180        max_dimension,
181        max_pixels,
182        dimensions_cache_ttl_secs,
183    })
184}