Skip to main content

yggdrasil/api/settings/
mod.rs

1//! 回收站、自动备份、素材上传与站点配置接口。
2//!
3//! 按子域拆分为目录模块:`trash`(回收站)、`backup`(自动备份)、`security`
4//! (安全)、`image_cache`(图片磁盘缓存)、`asset_purge`(孤儿素材清理)、
5//! `rate_limit`(限流)、`upload`(素材上传)、`site`(站点公开配置)、
6//! `system`(系统启动信息)、`webp`(WebP 编码)、`image_limit`(图片尺寸限制)、
7//! `runner`(代码运行器)。全部公开项经本模块 `pub use` 聚合,
8//! `crate::api::settings::xxx` 路径保持不变,外部调用零改动。
9//!
10//! - 回收站配置:读取与更新自动清理设置,需要 admin 权限。
11//! - 自动备份配置:读取与更新定时备份设置(含上次结果/下次执行时间),需要 admin。
12//!   `load_backup_settings` / `save_last_backup_run` 同时供备份核心与调度任务复用。
13//!   同名 `BACKUP_*` 环境变量仅在对应 settings 键缺失时播种初始值(首次部署),
14//!   之后以面板写入的 DB 值为准。
15//! - 素材上传配置:读取与更新上传弹窗并发数,需要 admin。`UPLOAD_CONCURRENCY`
16//!   环境变量播种语义与 `BACKUP_*` 一致(仅键缺失时生效)。
17//! - 站点公开配置:页脚 GitHub 链接等,`get_site_settings` 公开读取(前台页脚 SSR),
18//!   `update_site_settings` 仅 admin。配置持久化到 settings 键值表。
19//! Dioxus server function,注册在 `/api` 路径下。
20
21mod asset_purge;
22mod backup;
23mod image_cache;
24mod image_limit;
25mod rate_limit;
26mod runner;
27mod security;
28mod site;
29mod system;
30mod trash;
31mod upload;
32mod webp;
33
34#[cfg(feature = "server")]
35use crate::api::error::AppError;
36#[cfg(feature = "server")]
37use std::collections::HashMap;
38
39/// Batch-insert first-boot environment seeds.
40///
41/// Each settings group validates its own environment variables, then delegates
42/// the write here so one group costs one round trip instead of one per key.
43#[cfg(feature = "server")]
44pub(crate) async fn insert_env_seeds(
45    client: &tokio_postgres::Client,
46    seeds: Vec<(&'static str, String)>,
47) -> Result<(), AppError> {
48    if seeds.is_empty() {
49        return Ok(());
50    }
51
52    let keys: Vec<&str> = seeds.iter().map(|(key, _)| *key).collect();
53    let values: Vec<&str> = seeds.iter().map(|(_, value)| value.as_str()).collect();
54    client
55        .execute(
56            "INSERT INTO settings (key, value)
57             SELECT * FROM UNNEST($1::text[], $2::text[])
58             ON CONFLICT (key) DO NOTHING",
59            &[&keys, &values],
60        )
61        .await
62        .map_err(AppError::query)?;
63
64    for (key, value) in seeds {
65        tracing::info!("配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
66    }
67    Ok(())
68}
69
70/// Load a settings group with one indexed query.
71#[cfg(feature = "server")]
72pub(crate) async fn load_setting_values(
73    client: &tokio_postgres::Client,
74    keys: &[&str],
75) -> Result<HashMap<String, String>, AppError> {
76    if keys.is_empty() {
77        return Ok(HashMap::new());
78    }
79
80    let keys: Vec<&str> = keys.to_vec();
81    let rows = client
82        .query(
83            "SELECT key, value FROM settings WHERE key = ANY($1::text[])",
84            &[&keys],
85        )
86        .await
87        .map_err(AppError::query)?;
88
89    let mut values = HashMap::with_capacity(rows.len());
90    for row in rows {
91        values.insert(row.get::<_, String>("key"), row.get::<_, String>("value"));
92    }
93    Ok(values)
94}
95
96/// Seed all first-boot settings and bake Tier-B settings into process config.
97///
98/// Environment seeding is best-effort by design. Tier-B reads are startup
99/// critical: silently falling back would permanently replace administrator-
100/// configured limits until the next restart.
101#[cfg(feature = "server")]
102pub(crate) async fn bootstrap_startup_settings(
103    client: &tokio_postgres::Client,
104) -> Result<(), AppError> {
105    let (backup, upload, security, image_cache, asset_purge, rate_limit, webp, image_limit, runner) = tokio::join!(
106        seed_backup_settings_from_env(client),
107        seed_upload_settings_from_env(client),
108        seed_security_settings_from_env(client),
109        seed_image_cache_settings_from_env(client),
110        seed_asset_purge_settings_from_env(client),
111        seed_rate_limit_from_env(client),
112        seed_webp_settings_from_env(client),
113        seed_image_limit_settings_from_env(client),
114        seed_runner_settings_from_env(client),
115    );
116
117    for (name, result) in [
118        ("backup", backup),
119        ("upload", upload),
120        ("security", security),
121        ("image_cache", image_cache),
122        ("asset_purge", asset_purge),
123        ("rate_limit", rate_limit),
124        ("webp", webp),
125        ("image_limit", image_limit),
126        ("runner", runner),
127    ] {
128        if let Err(error) = result {
129            tracing::warn!(setting_group = name, error = ?error, "启动设置环境变量播种失败");
130        }
131    }
132
133    let (rate_limit, webp, image_limit, runner) = tokio::join!(
134        load_rate_limit_settings(client),
135        load_webp_settings(client),
136        load_image_limit_settings(client),
137        load_runner_settings(client),
138    );
139
140    let rate_limit = rate_limit.map_err(|error| {
141        tracing::error!(error = ?error, "限流启动配置加载失败,拒绝使用默认值");
142        error
143    })?;
144    let webp = webp.map_err(|error| {
145        tracing::error!(error = ?error, "WebP 启动配置加载失败,拒绝使用默认值");
146        error
147    })?;
148    let image_limit = image_limit.map_err(|error| {
149        tracing::error!(error = ?error, "图片限制启动配置加载失败,拒绝使用默认值");
150        error
151    })?;
152    let runner = runner.map_err(|error| {
153        tracing::error!(error = ?error, "代码运行器启动配置加载失败,拒绝使用默认值");
154        error
155    })?;
156
157    crate::config::set_rate_limit(rate_limit);
158    crate::config::set_webp(webp);
159    crate::config::set_image_limit(image_limit);
160    crate::config::set_runner(runner);
161    Ok(())
162}
163
164pub use asset_purge::*;
165pub use backup::*;
166pub use image_cache::*;
167pub use image_limit::*;
168pub use rate_limit::*;
169pub use runner::*;
170pub use security::*;
171pub use site::*;
172// system 的唯一调用方是 system_section.rs 的 #[cfg(target_arch = "wasm32")] 导入,
173// 原生构建下该 glob 无人消费;binary crate 里 pub 不豁免 unused_imports,故放行。
174#[allow(unused_imports)]
175pub use system::*;
176pub use trash::*;
177pub use upload::*;
178pub use webp::*;