1#![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#[cfg(feature = "server")]
16use crate::models::settings::{BackupSettings, LastBackupRun};
17
18#[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#[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#[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#[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#[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#[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#[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 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}