yggdrasil/tasks/
backup.rs1use std::sync::LazyLock;
15use std::time::Duration;
16
17use chrono::Utc;
18use tokio::sync::Notify;
19
20use crate::api::database::backup::{self, BackupRunOutcome};
21use crate::api::settings::{load_backup_settings, save_last_backup_run};
22use crate::db::pool::get_conn;
23use crate::models::settings::LastBackupRun;
24
25static SETTINGS_CHANGED: LazyLock<Notify> = LazyLock::new(Notify::new);
27
28pub(crate) fn notify_settings_changed() {
30 SETTINGS_CHANGED.notify_waiters();
31}
32
33pub async fn run_scheduler() {
35 const IDLE_RETRY: Duration = Duration::from_secs(3600);
37
38 loop {
39 let settings = match get_conn().await {
40 Ok(conn) => match load_backup_settings(&conn).await {
41 Ok(s) => s,
42 Err(e) => {
43 tracing::error!("Auto-backup: failed to load settings: {e:?}");
44 tokio::select! {
45 _ = SETTINGS_CHANGED.notified() => {}
46 _ = tokio::time::sleep(IDLE_RETRY) => {}
47 }
48 continue;
49 }
50 },
51 Err(e) => {
52 tracing::error!("Auto-backup: failed to get DB connection: {e:?}");
53 tokio::time::sleep(IDLE_RETRY).await;
54 continue;
55 }
56 };
57
58 if !settings.auto_enabled {
59 tokio::select! {
60 _ = SETTINGS_CHANGED.notified() => {}
61 _ = tokio::time::sleep(IDLE_RETRY) => {}
62 }
63 continue;
64 }
65
66 let now = Utc::now();
67 let Some(next) = settings.next_run_after(now) else {
68 tracing::error!(
70 "Auto-backup: invalid backup_time_utc {:?}",
71 settings.time_utc
72 );
73 tokio::select! {
74 _ = SETTINGS_CHANGED.notified() => {}
75 _ = tokio::time::sleep(IDLE_RETRY) => {}
76 }
77 continue;
78 };
79 let wait = (next - now).to_std().unwrap_or(Duration::ZERO);
80 tracing::info!("Auto-backup: next run scheduled at {}", next.to_rfc3339());
81
82 tokio::select! {
84 _ = SETTINGS_CHANGED.notified() => continue,
85 _ = tokio::time::sleep(wait) => {}
86 }
87
88 tracing::info!("Auto-backup: starting scheduled backup");
89 let outcome = backup::run_auto_backup().await;
90 persist_last_run(&outcome).await;
91 match &outcome {
92 Ok(o) => tracing::info!(
93 "Auto-backup: done (sql={}, uploads={:?}, warning={:?})",
94 o.sql_filename,
95 o.uploads_filename,
96 o.warning
97 ),
98 Err(e) => tracing::error!("Auto-backup: failed: {e}"),
99 }
100 }
101}
102
103async fn persist_last_run(outcome: &Result<BackupRunOutcome, String>) {
105 let run = match outcome {
106 Ok(o) => LastBackupRun {
107 at: Utc::now().to_rfc3339(),
108 ok: true,
109 file: Some(o.sql_filename.clone()),
110 error: o.warning.clone(),
111 },
112 Err(e) => LastBackupRun {
113 at: Utc::now().to_rfc3339(),
114 ok: false,
115 file: None,
116 error: Some(e.clone()),
117 },
118 };
119 match get_conn().await {
120 Ok(conn) => {
121 if let Err(e) = save_last_backup_run(&conn, &run).await {
122 tracing::error!("Auto-backup: failed to persist last_run: {e:?}");
123 }
124 }
125 Err(e) => tracing::error!("Auto-backup: failed to get DB connection for last_run: {e:?}"),
126 }
127}