yggdrasil/api/database/
backup.rs1#![allow(clippy::unused_unit, deprecated)]
2
3#[cfg(feature = "server")]
30mod commands;
31#[cfg(feature = "server")]
32mod creation;
33#[cfg(feature = "server")]
34mod files;
35#[cfg(feature = "server")]
36mod restore;
37#[cfg(feature = "server")]
38mod transfer;
39
40use crate::api::error::ApiError;
41use leptos::prelude::*;
42use serde::{Deserialize, Serialize};
43
44#[cfg(feature = "server")]
45use crate::api::auth::get_current_admin_user;
46#[cfg(feature = "server")]
47use crate::api::database::tasks::{self, TaskKind};
48#[cfg(feature = "server")]
49use crate::api::error::AppError;
50#[cfg(feature = "server")]
51pub(crate) use creation::run_auto_backup;
52#[cfg(feature = "server")]
53use files::{backup_path, has_valid_signature, is_valid_backup_filename, read_first_line};
54#[cfg(feature = "server")]
55pub(crate) use transfer::import_max_bytes;
56#[cfg(feature = "server")]
57pub use transfer::{download_backup, import_backup};
58
59#[cfg(feature = "server")]
61pub(crate) const MULTIPART_FRAME_SLACK: u64 = 1024 * 1024;
62
63#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
65pub struct BackupInfo {
66 pub filename: String,
67 pub size_bytes: u64,
68 pub mode: String,
70 pub created_at: Option<String>,
71 pub origin: String,
73 pub uploads_filename: Option<String>,
75 pub uploads_size_bytes: Option<u64>,
76}
77
78#[cfg(feature = "server")]
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub(crate) enum BackupOrigin {
82 Manual,
84 Auto,
86}
87
88#[cfg(feature = "server")]
89impl BackupOrigin {
90 fn file_prefix(self) -> &'static str {
91 match self {
92 Self::Manual => "backup",
93 Self::Auto => "auto",
94 }
95 }
96}
97
98#[cfg(feature = "server")]
100#[derive(Debug)]
101pub(crate) struct BackupRunOutcome {
102 pub sql_filename: String,
104 pub uploads_filename: Option<String>,
106 pub warning: Option<String>,
108}
109
110#[server(CreateBackup, endpoint = "create_backup", input = leptos::server_fn::codec::Json)]
112pub async fn create_backup() -> Result<String, ApiError> {
113 let _user = get_current_admin_user().await?;
114
115 #[cfg(feature = "server")]
116 {
117 let guard = creation::BackupGuard::try_acquire()
118 .ok_or_else(|| AppError::BadRequest(creation::BACKUP_BUSY.to_string()))?;
119 let task_id = uuid::Uuid::new_v4().to_string();
120 tasks::insert(task_id.clone(), TaskKind::Backup);
121 let tid = task_id.clone();
122 tokio::spawn(async move {
123 let _ = creation::run_backup(&tid, BackupOrigin::Manual, guard).await;
125 });
126 Ok(task_id)
127 }
128 #[cfg(not(feature = "server"))]
129 {
130 Ok(String::new())
131 }
132}
133
134#[server(RestoreBackup, endpoint = "restore_backup", input = leptos::server_fn::codec::Json)]
136pub async fn restore_backup(filename: String, confirm: bool) -> Result<String, ApiError> {
137 let _user = get_current_admin_user().await?;
138
139 #[cfg(feature = "server")]
142 {
143 if !confirm {
144 return Err(AppError::BadRequest("需确认恢复(会覆盖现有数据)".to_string()).into());
145 }
146 if !is_valid_backup_filename(&filename) {
148 return Err(AppError::BadRequest("无效的文件名".to_string()).into());
149 }
150 let path = backup_path(&filename);
151 if !path.exists() {
152 return Err(AppError::NotFound("备份文件不存在").into());
153 }
154
155 let first_line = read_first_line(&path).unwrap_or_default();
157 if !has_valid_signature(&first_line) {
158 return Err(
159 AppError::BadRequest("非本系统生成的备份文件,拒绝恢复".to_string()).into(),
160 );
161 }
162
163 let task_id = uuid::Uuid::new_v4().to_string();
164 tasks::insert(task_id.clone(), TaskKind::Restore);
165 let tid = task_id.clone();
166 let f = filename;
167 tokio::spawn(async move {
168 restore::run_restore(&tid, &f).await;
169 });
170 Ok(task_id)
171 }
172 #[cfg(not(feature = "server"))]
173 {
174 let _ = (filename, confirm);
176 Ok(String::new())
177 }
178}
179
180#[server(ListBackups, endpoint = "list_backups", input = leptos::server_fn::codec::Json)]
182pub async fn list_backups() -> Result<Vec<BackupInfo>, ApiError> {
183 let _user = get_current_admin_user().await?;
184 #[cfg(feature = "server")]
185 {
186 Ok(files::list_backup_files())
187 }
188 #[cfg(not(feature = "server"))]
189 {
190 Ok(vec![])
191 }
192}
193
194#[server(DeleteBackup, endpoint = "delete_backup", input = leptos::server_fn::codec::Json)]
196pub async fn delete_backup(filename: String) -> Result<(), ApiError> {
197 let _user = get_current_admin_user().await?;
198 #[cfg(feature = "server")]
199 {
200 files::delete_backup_files(&filename).map_err(Into::into)
201 }
202 #[cfg(not(feature = "server"))]
203 {
204 let _ = filename;
205 Ok(())
206 }
207}