Skip to main content

yggdrasil/api/settings/
backup.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::BackupSettingsView;
14// 仅 server 构建的函数体引用(WASM 端 server fn 体被宏剥离)。
15#[cfg(feature = "server")]
16use crate::models::settings::{BackupSettings, LastBackupRun};
17
18// ============================================================================
19// 自动备份配置
20// ============================================================================
21
22/// 从 settings 表读取自动备份配置(缺键回退默认值)。
23///
24/// 供 `get_backup_settings`、备份核心(include_uploads / retention_count)
25/// 与定时调度任务三处共用。
26#[cfg(feature = "server")]
27pub(crate) async fn load_backup_settings(
28    client: &tokio_postgres::Client,
29) -> Result<BackupSettings, AppError> {
30    async fn read_key(
31        client: &tokio_postgres::Client,
32        key: &str,
33    ) -> Result<Option<String>, AppError> {
34        let row = client
35            .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
36            .await
37            .map_err(AppError::query)?;
38        Ok(row.map(|r| r.get::<_, String>("value")))
39    }
40
41    let auto_enabled = read_key(client, "backup_auto_enabled")
42        .await?
43        .and_then(|v| v.parse().ok())
44        .unwrap_or(crate::models::settings::DEFAULT_BACKUP_AUTO_ENABLED);
45    let time_utc = read_key(client, "backup_time_utc")
46        .await?
47        .map(|v| BackupSettings::normalize_time_utc(&v))
48        .unwrap_or_else(|| crate::models::settings::DEFAULT_BACKUP_TIME_UTC.to_string());
49    let retention_count = read_key(client, "backup_retention_count")
50        .await?
51        .and_then(|v| v.parse().ok())
52        .map(BackupSettings::clamp_retention)
53        .unwrap_or(crate::models::settings::DEFAULT_BACKUP_RETENTION_COUNT);
54    let include_uploads = read_key(client, "backup_include_uploads")
55        .await?
56        .and_then(|v| v.parse().ok())
57        .unwrap_or(crate::models::settings::DEFAULT_BACKUP_INCLUDE_UPLOADS);
58
59    Ok(BackupSettings {
60        auto_enabled,
61        time_utc,
62        retention_count,
63        include_uploads,
64    })
65}
66
67/// 读取最近一次自动备份结果(未执行过 → None)。
68#[cfg(feature = "server")]
69async fn load_last_backup_run(
70    client: &tokio_postgres::Client,
71) -> Result<Option<LastBackupRun>, AppError> {
72    let rows = client
73        .query(
74            "SELECT key, value FROM settings WHERE key = ANY(ARRAY[
75                'backup_last_run_at', 'backup_last_run_ok',
76                'backup_last_run_file', 'backup_last_run_error'])",
77            &[],
78        )
79        .await
80        .map_err(AppError::query)?;
81    let get = |k: &str| {
82        rows.iter()
83            .find(|r| r.get::<_, String>("key") == k)
84            .map(|r| r.get::<_, String>("value"))
85            .filter(|v| !v.is_empty())
86    };
87    let (Some(at), Some(ok)) = (get("backup_last_run_at"), get("backup_last_run_ok")) else {
88        return Ok(None);
89    };
90    Ok(Some(LastBackupRun {
91        at,
92        ok: ok.parse().unwrap_or(false),
93        file: get("backup_last_run_file"),
94        error: get("backup_last_run_error"),
95    }))
96}
97
98/// 落库最近一次自动备份结果(调度任务执行后调用;空串表示 None)。
99#[cfg(feature = "server")]
100pub(crate) async fn save_last_backup_run(
101    client: &tokio_postgres::Client,
102    run: &LastBackupRun,
103) -> Result<(), AppError> {
104    let pairs: [(&str, String); 4] = [
105        ("backup_last_run_at", run.at.clone()),
106        ("backup_last_run_ok", run.ok.to_string()),
107        ("backup_last_run_file", run.file.clone().unwrap_or_default()),
108        (
109            "backup_last_run_error",
110            run.error.clone().unwrap_or_default(),
111        ),
112    ];
113    for (key, value) in pairs {
114        client
115            .execute(
116                "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
117                 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
118                &[&key, &value],
119            )
120            .await
121            .map_err(AppError::query)?;
122    }
123    Ok(())
124}
125
126/// 启动时用 `BACKUP_*` 环境变量播种自动备份配置。
127///
128/// 仅当对应 settings 键**不存在**时插入(`ON CONFLICT DO NOTHING`)——
129/// 即 env 只决定首次部署的初始值,之后以后台面板写入的 DB 值为准,
130/// 重启不会被 env 覆盖。单个变量非法只告警跳过,不影响其他变量与启动。
131#[cfg(feature = "server")]
132pub(crate) async fn seed_backup_settings_from_env(
133    client: &tokio_postgres::Client,
134) -> Result<(), AppError> {
135    let mut seeds: Vec<(&'static str, String)> = Vec::new();
136
137    if let Ok(v) = std::env::var("BACKUP_AUTO_ENABLED") {
138        match v.trim().parse::<bool>() {
139            Ok(b) => seeds.push(("backup_auto_enabled", b.to_string())),
140            Err(_) => tracing::warn!("BACKUP_AUTO_ENABLED={v:?} 非法(期望 true/false),跳过"),
141        }
142    }
143    if let Ok(v) = std::env::var("BACKUP_TIME_UTC") {
144        if BackupSettings::is_valid_time_utc(&v) {
145            seeds.push(("backup_time_utc", BackupSettings::normalize_time_utc(&v)));
146        } else {
147            tracing::warn!("BACKUP_TIME_UTC={v:?} 非法(期望 HH:MM,UTC),跳过");
148        }
149    }
150    if let Ok(v) = std::env::var("BACKUP_RETENTION_COUNT") {
151        match v.trim().parse::<i32>() {
152            Ok(n) => seeds.push((
153                "backup_retention_count",
154                BackupSettings::clamp_retention(n).to_string(),
155            )),
156            Err(_) => tracing::warn!("BACKUP_RETENTION_COUNT={v:?} 非法(期望整数),跳过"),
157        }
158    }
159    if let Ok(v) = std::env::var("BACKUP_INCLUDE_UPLOADS") {
160        match v.trim().parse::<bool>() {
161            Ok(b) => seeds.push(("backup_include_uploads", b.to_string())),
162            Err(_) => tracing::warn!("BACKUP_INCLUDE_UPLOADS={v:?} 非法(期望 true/false),跳过"),
163        }
164    }
165
166    for (key, value) in seeds {
167        client
168            .execute(
169                "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
170                &[&key, &value],
171            )
172            .await
173            .map_err(AppError::query)?;
174        tracing::info!("备份配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
175    }
176    Ok(())
177}
178
179/// 组装面板视图:设置 + 上次结果 + 下次执行时间。
180#[cfg(feature = "server")]
181async fn load_backup_settings_view(
182    client: &tokio_postgres::Client,
183) -> Result<BackupSettingsView, AppError> {
184    let settings = load_backup_settings(client).await?;
185    let last_run = load_last_backup_run(client).await?;
186    let next_run_at = if settings.auto_enabled {
187        settings
188            .next_run_after(chrono::Utc::now())
189            .map(|dt| dt.to_rfc3339())
190    } else {
191        None
192    };
193    Ok(BackupSettingsView {
194        settings,
195        last_run,
196        next_run_at,
197    })
198}
199
200/// 读取自动备份配置(面板用)。
201#[server(GetBackupSettings, "/api")]
202pub async fn get_backup_settings() -> Result<BackupSettingsView, ServerFnError> {
203    let _user = get_current_admin_user().await?;
204
205    #[cfg(feature = "server")]
206    {
207        let client = get_conn().await.map_err(AppError::db_conn)?;
208        Ok(load_backup_settings_view(&client).await?)
209    }
210
211    #[cfg(not(feature = "server"))]
212    {
213        Ok(BackupSettingsView {
214            settings: BackupSettings::default(),
215            last_run: None,
216            next_run_at: None,
217        })
218    }
219}
220
221/// 更新自动备份配置。
222///
223/// time_utc 会被规范化(非法回退默认),retention_count 会被 clamp。
224/// 写入后唤醒调度任务立即重排,无需等待下一次 tick。
225#[server(UpdateBackupSettings, "/api")]
226pub async fn update_backup_settings(
227    auto_enabled: bool,
228    time_utc: String,
229    retention_count: i32,
230    include_uploads: bool,
231) -> Result<BackupSettingsView, ServerFnError> {
232    let _user = get_current_admin_user().await?;
233
234    let time_utc = BackupSettings::normalize_time_utc(&time_utc);
235    let retention_count = BackupSettings::clamp_retention(retention_count);
236
237    #[cfg(feature = "server")]
238    {
239        let client = get_conn().await.map_err(AppError::db_conn)?;
240        let pairs = [
241            ("backup_auto_enabled", auto_enabled.to_string()),
242            ("backup_time_utc", time_utc.clone()),
243            ("backup_retention_count", retention_count.to_string()),
244            ("backup_include_uploads", include_uploads.to_string()),
245        ];
246        for (key, value) in pairs {
247            client
248                .execute(
249                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
250                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
251                    &[&key, &value],
252                )
253                .await
254                .map_err(AppError::query)?;
255        }
256        tracing::info!(
257            "Backup settings updated: auto_enabled={}, time_utc={}, retention={}, include_uploads={}",
258            auto_enabled,
259            time_utc,
260            retention_count,
261            include_uploads
262        );
263        // 唤醒调度器立即重排(设置变更不应等到原定的下次触发才生效)。
264        crate::tasks::backup::notify_settings_changed();
265        Ok(load_backup_settings_view(&client).await?)
266    }
267
268    #[cfg(not(feature = "server"))]
269    {
270        Ok(BackupSettingsView {
271            settings: BackupSettings {
272                auto_enabled,
273                time_utc,
274                retention_count,
275                include_uploads,
276            },
277            last_run: None,
278            next_run_at: None,
279        })
280    }
281}