Skip to main content

yggdrasil/api/database/
tasks.rs

1#![allow(clippy::unused_unit, deprecated)]
2
3//! 备份/恢复的异步任务进度表(DashMap)。
4//!
5//! create_backup/restore_backup 立即返回 task_id,后台任务跑时通过
6//! `update` 更新进度,前端轮询 [`get_task_progress`][crate::api::database::tasks::get_task_progress]。
7//! 已完成超过 1 小时的任务惰性清理,避免内存累积。
8
9use chrono::{DateTime, Utc};
10use dioxus::prelude::*;
11use serde::{Deserialize, Serialize};
12
13// admin 鉴权仅 server 构建用到。
14#[cfg(feature = "server")]
15use crate::api::auth::get_current_admin_user;
16// DashMap / LazyLock 仅 server 构建持有任务进度表;WASM 端只序列化 TaskProgress。
17#[cfg(feature = "server")]
18use dashmap::DashMap;
19#[cfg(feature = "server")]
20use std::sync::LazyLock;
21
22#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
23pub enum TaskKind {
24    Backup,
25    Restore,
26}
27
28#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
29pub enum TaskStatus {
30    Running,
31    Done,
32    Failed,
33}
34
35#[derive(Clone, Serialize, Deserialize, Debug)]
36pub struct TaskProgress {
37    pub id: String,
38    pub kind: TaskKind,
39    pub stage: String,
40    pub percent: u8,
41    pub detail: Option<String>,
42    pub status: TaskStatus,
43    pub error: Option<String>,
44    pub created_at: DateTime<Utc>,
45    /// 完成后的备份文件名(下载/恢复用)。
46    pub result_filename: Option<String>,
47}
48
49#[cfg(feature = "server")]
50static TASKS: LazyLock<DashMap<String, TaskProgress>> = LazyLock::new(DashMap::new);
51
52/// 注册新任务(初始 Running,0%)。
53#[cfg(feature = "server")]
54pub(super) fn insert(id: String, kind: TaskKind) {
55    TASKS.insert(
56        id.clone(),
57        TaskProgress {
58            id,
59            kind,
60            stage: "排队中".to_string(),
61            percent: 0,
62            detail: None,
63            status: TaskStatus::Running,
64            error: None,
65            created_at: Utc::now(),
66            result_filename: None,
67        },
68    );
69}
70
71/// 更新任务进度(后台任务调用)。
72#[cfg(feature = "server")]
73pub(super) fn update(
74    id: &str,
75    stage: &str,
76    percent: u8,
77    status: TaskStatus,
78    detail: Option<String>,
79    error: Option<String>,
80    result_filename: Option<String>,
81) {
82    if let Some(mut p) = TASKS.get_mut(id) {
83        p.stage = stage.to_string();
84        p.percent = percent;
85        p.status = status;
86        p.detail = detail;
87        p.error = error;
88        p.result_filename = result_filename;
89    }
90}
91
92/// 惰性清理已完成超过 1 小时的任务(查询时顺便清)。
93#[cfg(feature = "server")]
94fn gc_old() {
95    let cutoff = Utc::now() - chrono::Duration::hours(1);
96    TASKS.retain(|_, p| {
97        !(matches!(p.status, TaskStatus::Done | TaskStatus::Failed) && p.created_at < cutoff)
98    });
99}
100
101/// 查询任务进度(轮询用)。
102#[server(GetTaskProgress, "/api")]
103pub async fn get_task_progress(task_id: String) -> Result<TaskProgress, ServerFnError> {
104    let _user = get_current_admin_user().await?;
105    #[cfg(feature = "server")]
106    {
107        gc_old();
108        TASKS
109            .get(&task_id)
110            .map(|p| p.clone())
111            .ok_or_else(|| crate::api::error::AppError::NotFound("任务不存在").into())
112    }
113    #[cfg(not(feature = "server"))]
114    {
115        Ok(TaskProgress {
116            id: task_id,
117            kind: TaskKind::Backup,
118            stage: String::new(),
119            percent: 0,
120            detail: None,
121            status: TaskStatus::Done,
122            error: None,
123            created_at: Utc::now(),
124            result_filename: None,
125        })
126    }
127}