Skip to main content

yggdrasil/pages/admin/system/
backup.rs

1//! 备份 tab:创建与导入备份、任务进度、备份列表(下载、恢复、删除)。
2//! 定时备份的调度在「设置 › 自动备份」。
3mod import;
4mod row;
5mod task;
6
7use crate::api::database::backup::BackupInfo;
8use crate::api::database::tasks::TaskProgress;
9use crate::components::empty_state::EmptyState;
10use crate::components::ui::{button_class, ButtonSize, ButtonVariant, Icon, IconName};
11use crate::routes::Route;
12use import::ImportBackup;
13use leptos::prelude::*;
14use row::BackupRow;
15
16/// 读取当前进度的某个字段(无进度时取默认值)。
17fn progress_field<T: Default>(
18    progress: RwSignal<Option<TaskProgress>>,
19    f: impl FnOnce(&TaskProgress) -> T,
20) -> T {
21    progress.with(|p| p.as_ref().map(f).unwrap_or_default())
22}
23
24/// 备份 tab。
25#[component]
26pub(super) fn BackupTab() -> impl IntoView {
27    let state = task::use_backup_task();
28    let loading = state.loading;
29    let busy = Signal::derive(move || state.busy());
30    let has_backups = Memo::new(move |_| state.backups.with(|list| !list.is_empty()));
31    let show_empty = move || !has_backups.get() && !loading.get();
32    let has_progress = Memo::new(move |_| state.progress.with(Option::is_some));
33    let progress = state.progress;
34    let percent = move || progress_field(progress, |p| p.percent);
35
36    view! {
37        <div class="sys-backup-head">
38            <p>
39                "优先用 pg_dump 生成含表结构的完整备份;包含素材时另打包配对的 tar.gz。定时备份在 "
40                <a href=Route::SiteSettingsPage.to_string()>"设置 › 自动备份"</a>
41                " 里调整。"
42            </p>
43            <div class="sys-backup-actions">
44                <button
45                    class=button_class(ButtonVariant::Ghost, ButtonSize::Sm)
46                    type="button"
47                    disabled=move || loading.get() || busy.get()
48                    on:click=move |_| state.refresh()
49                >
50                    <Icon name=IconName::RefreshCw size=14/>
51                    "刷新列表"
52                </button>
53                <ImportBackup state/>
54                <button
55                    class=button_class(ButtonVariant::Primary, ButtonSize::Sm)
56                    type="button"
57                    disabled=move || busy.get()
58                    on:click=move |_| state.start(None)
59                >
60                    {move || if busy.get() { "处理中…" } else { "创建备份" }}
61                </button>
62            </div>
63        </div>
64
65        <Show when=move || has_progress.get()>
66            <div class="admin-panel sys-progress" role="status">
67                <span class="sys-progress-stage">
68                    {move || progress_field(progress, |p| p.stage.clone())}
69                </span>
70                <div class="sys-progress-bar" aria-hidden="true">
71                    <div style=move || format!("width: {}%", percent())></div>
72                </div>
73                <span class="sys-progress-pct">{move || format!("{}%", percent())}</span>
74                {move || {
75                    progress_field(progress, |p| p.detail.clone())
76                        .map(|detail| view! { <p class="sys-progress-detail">{detail}</p> })
77                }}
78                {move || {
79                    progress_field(progress, |p| p.error.clone())
80                        .map(|err| view! { <p class="sys-error">{format!("错误:{err}")}</p> })
81                }}
82            </div>
83        </Show>
84
85        {move || {
86            state
87                .error
88                .get()
89                .map(|err| view! { <p class="sys-error" role="alert">{err}</p> })
90        }}
91
92        {move || {
93            if has_backups.get() {
94                view! {
95                    <section class="admin-panel sys-section">
96                        <div class="sys-table-scroll">
97                            <table class="admin-table">
98                                <thead>
99                                    <tr>
100                                        <th>"文件"</th>
101                                        <th>"来源"</th>
102                                        <th>"模式"</th>
103                                        <th class="admin-table-num">"大小"</th>
104                                        <th class="admin-table-actions">
105                                            <span class="sr-only">"操作"</span>
106                                        </th>
107                                    </tr>
108                                </thead>
109                                <tbody>
110                                    <For
111                                        each=move || state.backups.get()
112                                        key=|b: &BackupInfo| {
113                                            (
114                                                b.filename.clone(),
115                                                b.size_bytes,
116                                                b.origin.clone(),
117                                                b.mode.clone(),
118                                                b.uploads_filename.clone(),
119                                                b.uploads_size_bytes,
120                                            )
121                                        }
122                                        children=move |b: BackupInfo| {
123                                            view! {
124                                                <BackupRow
125                                                    info=b
126                                                    busy=busy
127                                                    on_restore=move |filename: String| state.start(Some(filename))
128                                                    on_delete=move |filename: String| state.delete(filename)
129                                                />
130                                            }
131                                        }
132                                    />
133                                </tbody>
134                            </table>
135                        </div>
136                    </section>
137                }
138                    .into_any()
139            } else if show_empty() {
140                view! {
141                    <div class="admin-panel">
142                        <EmptyState
143                            icon=IconName::Archive
144                            title="还没有备份"
145                            description="创建一份手动备份,或在设置里开启定时备份。"
146                        />
147                    </div>
148                }
149                    .into_any()
150            } else {
151                ().into_any()
152            }
153        }}
154        <p class="sys-footnote">
155            "恢复只接受本系统生成的备份,且只恢复数据库;素材需要从配对的 tar.gz 手动还原。导入只接受本系统生成的 .sql 备份(签名校验,单个文件默认不超过 512 MB),以 auto_ 开头的文件会参与自动轮转。"
156        </p>
157    }
158}