yggdrasil/pages/admin/system/
backup.rs1mod 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
16fn 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#[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}