Skip to main content

yggdrasil/api/settings/
backup.rs

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