feat(backup): 新增 CDC 内容寻址仓库模式

使用内容定义分块、不可变 pack 与快照索引实现跨文件和跨快照去重。

接入多目标上传、软配额、恢复、下载、校验、保留和安全垃圾回收,并补充前端入口、中英文文档及端到端测试。

Closes #94
This commit is contained in:
Awuqing
2026-08-07 05:53:16 +08:00
parent 50ce6587d8
commit 05dd1baa61
21 changed files with 2391 additions and 45 deletions
@@ -251,6 +251,12 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
if (validPaths.length === 0 && !value.sourcePath.trim()) {
return '请输入至少一个源路径'
}
if (value.backupMode === 'repository' && (((value.nodeId ?? 0) > 0 && value.nodeId !== localNodeId) || value.nodePoolTag?.trim())) {
return 'CDC 仓库模式当前仅支持 Master 本机执行'
}
if (value.backupMode === 'repository' && value.replicationTargetIds.length > 0) {
return 'CDC 仓库模式请直接多选存储目标,不能使用对象级副本复制'
}
}
if (isSQLiteBackupTask(value.type) && !value.dbPath.trim()) {
return '请输入 SQLite 数据库路径'
@@ -314,7 +320,13 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
onChange={(value) => {
const nodeId = Number(value ?? 0)
// 固定节点与节点池互斥:切到固定节点时清空 NodePoolTag
updateDraft(nodeId > 0 ? { nodeId, nodePoolTag: '' } : { nodeId })
updateDraft(nodeId > 0
? {
nodeId,
nodePoolTag: '',
backupMode: nodeId !== localNodeId && draft.backupMode === 'repository' ? 'full' : draft.backupMode,
}
: { nodeId })
}}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
@@ -327,7 +339,10 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
placeholder="填写标签后从节点池动态调度(与固定节点互斥)"
value={draft.nodePoolTag ?? ''}
disabled={(draft.nodeId ?? 0) > 0}
onChange={(value) => updateDraft({ nodePoolTag: value })}
onChange={(value) => updateDraft({
nodePoolTag: value,
backupMode: value.trim() && draft.backupMode === 'repository' ? 'full' : draft.backupMode,
})}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
"本机 / 未指定" Labels tag 线
@@ -600,8 +615,11 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
options={[
{ label: '全量备份', value: 'full' },
{ label: '差异备份(仅文件、本机)', value: 'differential' },
{ label: 'CDC 去重仓库(仅文件、本机)', value: 'repository' },
]}
onChange={(value) => updateDraft({ backupMode: value as BackupMode })}
onChange={(value) => updateDraft(value === 'repository'
? { backupMode: value as BackupMode, nodeId: 0, nodePoolTag: '', replicationTargetIds: [] }
: { backupMode: value as BackupMode })}
/>
{draft.backupMode === 'differential' && (
<div style={{ marginTop: 8 }}>
@@ -618,6 +636,11 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
/>
</div>
)}
{draft.backupMode === 'repository' && (
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 8 }}>
pack Master
</Typography.Paragraph>
)}
</div>
)}
<div>
@@ -736,10 +759,13 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
value={draft.replicationTargetIds}
placeholder="选择副本目标(不选 = 不启用复制)"
options={storageTargetOptions.filter((opt) => !(draft.storageTargetIds ?? []).includes(opt.value as number))}
disabled={draft.backupMode === 'repository'}
onChange={(values: number[]) => updateDraft({ replicationTargetIds: values })}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
3-2-1 2 1 provider
{draft.backupMode === 'repository'
? 'CDC 仓库包含共享 pack 与索引,不能只复制单个快照对象;请在“存储目标”中直接多选以生成完整仓库副本。'
: '备份成功后自动镜像到副本存储。满足 3-2-1 规则:至少 2 份副本、至少 1 份异地。建议选不同 provider 的目标。'}
</Typography.Paragraph>
</div>
@@ -114,6 +114,7 @@ export function BackupRecordsPage() {
<Typography.Text>{record.fileName || '-'}</Typography.Text>
{record.locked && <Tag color="orange" size="small" bordered></Tag>}
{record.backupKind === 'differential' && <Tag color="purple" size="small" bordered></Tag>}
{record.backupKind === 'repository' && <Tag color="blue" size="small" bordered>CDC</Tag>}
</Space>
<Typography.Text type="secondary">{formatBytes(record.fileSize)}</Typography.Text>
{record.checksum && (
+1 -1
View File
@@ -26,7 +26,7 @@ export interface BackupRecordSummary {
startedAt: string
completedAt?: string
locked: boolean
backupKind: 'full' | 'differential'
backupKind: 'full' | 'differential' | 'repository'
}
export interface BackupRecordContentEntry {
+1 -1
View File
@@ -1,7 +1,7 @@
export type BackupTaskType = 'file' | 'mysql' | 'sqlite' | 'postgresql' | 'saphana' | 'mongodb'
export type BackupTaskStatus = 'idle' | 'running' | 'success' | 'failed'
export type BackupCompression = 'gzip' | 'zstd' | 'none'
export type BackupMode = 'full' | 'differential'
export type BackupMode = 'full' | 'differential' | 'repository'
export interface BackupTaskSummary {
id: number