Skip to main content

yggdrasil/pages/admin/system/backup/
task.rs

1//! 组件作用域持有请求与轮询:只有当前任务可以更新进度,组件卸载即停止轮询。
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4
5use crate::api::database::backup::{
6    create_backup, delete_backup, list_backups, restore_backup, BackupInfo,
7};
8use crate::api::database::tasks::{get_task_progress, TaskProgress, TaskStatus};
9use crate::utils::error::failure_message;
10use leptos::prelude::*;
11
12/// 备份任务的操作锁:同一时刻只允许一个创建 / 恢复 / 删除。
13#[derive(Clone, PartialEq)]
14enum Operation {
15    Idle,
16    Starting,
17    Running(String),
18    Deleting,
19}
20
21/// 备份 tab 的共享状态(全部是 `Copy` 的信号,可直接在闭包间传递)。
22#[derive(Clone, Copy)]
23pub(super) struct BackupState {
24    pub backups: RwSignal<Vec<BackupInfo>>,
25    pub loading: RwSignal<bool>,
26    pub importing: RwSignal<bool>,
27    pub error: RwSignal<Option<String>>,
28    pub progress: RwSignal<Option<TaskProgress>>,
29    operation: RwSignal<Operation>,
30    list_generation: StoredValue<u64>,
31}
32
33impl BackupState {
34    /// 有操作进行中(响应式读取,供视图使用)。
35    pub fn busy(self) -> bool {
36        self.operation.with(|op| *op != Operation::Idle) || self.importing.get()
37    }
38
39    /// 同 [`busy`](Self::busy),非响应式读取,供事件处理器使用。
40    pub fn busy_untracked(self) -> bool {
41        self.operation.with_untracked(|op| *op != Operation::Idle) || self.importing.get_untracked()
42    }
43
44    /// 重新读取备份列表。只接受最新一次请求的结果。
45    pub fn refresh(self) {
46        let _ = self.error.try_set(None);
47        let Some(generation) = self.list_generation.try_update_value(|g| {
48            *g += 1;
49            *g
50        }) else {
51            return;
52        };
53        let _ = self.loading.try_set(true);
54        leptos::task::spawn_local(async move {
55            let result = list_backups().await;
56            if self.list_generation.try_get_value() != Some(generation) {
57                return;
58            }
59            let _ = self.loading.try_set(false);
60            match result {
61                Ok(list) => {
62                    let _ = self.backups.try_set(list);
63                }
64                Err(error) => {
65                    let _ = self
66                        .error
67                        .try_set(Some(failure_message("读取备份列表失败", &error)));
68                }
69            }
70        });
71    }
72
73    /// 创建备份(`restore` 为 `None`)或从指定文件恢复。
74    pub fn start(self, restore: Option<String>) {
75        if self.busy_untracked() {
76            return;
77        }
78        self.operation.set(Operation::Starting);
79        self.progress.set(None);
80        self.error.set(None);
81        leptos::task::spawn_local(async move {
82            let restoring = restore.is_some();
83            let result = match restore {
84                Some(filename) => restore_backup(filename, true).await,
85                None => create_backup().await,
86            };
87            match result {
88                Ok(id) => {
89                    let _ = self.operation.try_set(Operation::Running(id));
90                }
91                Err(error) => {
92                    let action = if restoring {
93                        "恢复失败"
94                    } else {
95                        "备份失败"
96                    };
97                    let _ = self.error.try_set(Some(failure_message(action, &error)));
98                    let _ = self.operation.try_set(Operation::Idle);
99                }
100            }
101        });
102    }
103
104    /// 删除一份备份,成功后刷新列表。
105    pub fn delete(self, filename: String) {
106        if self.busy_untracked() {
107            return;
108        }
109        self.operation.set(Operation::Deleting);
110        self.error.set(None);
111        leptos::task::spawn_local(async move {
112            match delete_backup(filename).await {
113                Ok(()) => self.refresh(),
114                Err(error) => {
115                    let _ = self
116                        .error
117                        .try_set(Some(failure_message("删除失败", &error)));
118                }
119            }
120            let _ = self.operation.try_set(Operation::Idle);
121        });
122    }
123}
124
125/// 创建 tab 状态,挂载时读取列表,并在有运行中的任务时轮询进度。
126pub(super) fn use_backup_task() -> BackupState {
127    let state = BackupState {
128        backups: RwSignal::new(Vec::new()),
129        loading: RwSignal::new(false),
130        importing: RwSignal::new(false),
131        error: RwSignal::new(None),
132        progress: RwSignal::new(None),
133        operation: RwSignal::new(Operation::Idle),
134        list_generation: StoredValue::new(0),
135    };
136    // Effect 只在浏览器执行:挂载后读一次列表。
137    Effect::new(move |_| state.refresh());
138    // 任务 id 变化(或卸载)时 effect 重跑 / 销毁,清理回调让上一轮轮询停止。
139    Effect::new(move |_| {
140        let Operation::Running(id) = state.operation.get() else {
141            return;
142        };
143        let alive = Arc::new(AtomicBool::new(true));
144        let stop = alive.clone();
145        on_cleanup(move || stop.store(false, Ordering::Relaxed));
146        leptos::task::spawn_local(async move {
147            loop {
148                crate::utils::time::sleep_ms(1500).await;
149                if !alive.load(Ordering::Relaxed) {
150                    return;
151                }
152                let result = get_task_progress(id.clone()).await;
153                if !alive.load(Ordering::Relaxed) {
154                    return;
155                }
156                match result {
157                    Ok(progress) => {
158                        let done = progress.status != TaskStatus::Running;
159                        let _ = state.progress.try_set(Some(progress));
160                        if done {
161                            state.refresh();
162                            let _ = state.operation.try_set(Operation::Idle);
163                            return;
164                        }
165                    }
166                    Err(error) => {
167                        let _ = state
168                            .error
169                            .try_set(Some(failure_message("获取备份进度失败", &error)));
170                        let _ = state.operation.try_set(Operation::Idle);
171                        return;
172                    }
173                }
174            }
175        });
176    });
177    state
178}