feat(backup): 新增差异备份(differential)模式 (#88)

文件备份新增差异模式:仅打包自上次全量以来的变更并记录删除,恢复自动按全量+差异链还原。含基线解析、链式恢复、保留链保护与本机文件任务校验;清单/比对/删除/往返/保留保护单测全覆盖。
This commit is contained in:
Wu Qing
2026-05-27 19:03:40 +08:00
committed by GitHub
parent f584a0802a
commit 90b58d58d6
17 changed files with 761 additions and 60 deletions

View File

@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
import { CronInput } from '../CronInput'
import type { StorageTargetDetail, StorageTargetPayload, StorageTargetSummary } from '../../types/storage-targets'
import type { StorageConnectionTestResult } from '../../types/storage-targets'
import type { BackupTaskDetail, BackupTaskPayload, BackupTaskType } from '../../types/backup-tasks'
import type { BackupMode, BackupTaskDetail, BackupTaskPayload, BackupTaskType } from '../../types/backup-tasks'
import type { NodeSummary } from '../../types/nodes'
import { DatabasePicker } from '../common/DatabasePicker'
import { DirectoryPicker } from '../common/DirectoryPicker'
@@ -69,6 +69,8 @@ function createEmptyDraft(storageTargets?: StorageTargetSummary[]): BackupTaskPa
keepWeekly: 0,
keepMonthly: 0,
keepYearly: 0,
backupMode: 'full',
diffFullIntervalDays: 7,
extraConfig: undefined,
verifyEnabled: false,
verifyCronExpr: '',
@@ -142,6 +144,8 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
keepWeekly: initialValue.keepWeekly ?? 0,
keepMonthly: initialValue.keepMonthly ?? 0,
keepYearly: initialValue.keepYearly ?? 0,
backupMode: initialValue.backupMode ?? 'full',
diffFullIntervalDays: initialValue.diffFullIntervalDays ?? 7,
extraConfig: initialValue.extraConfig,
verifyEnabled: initialValue.verifyEnabled ?? false,
verifyCronExpr: initialValue.verifyCronExpr ?? '',
@@ -588,6 +592,34 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
<Typography.Text></Typography.Text>
<Select value={draft.compression} options={backupCompressionOptions as unknown as { label: string; value: string }[]} onChange={(value) => updateDraft({ compression: value as BackupTaskPayload['compression'] })} />
</div>
{isFileBackupTask(draft.type) && (
<div>
<Typography.Text></Typography.Text>
<Select
value={draft.backupMode}
options={[
{ label: '全量备份', value: 'full' },
{ label: '差异备份(仅文件、本机)', value: 'differential' },
]}
onChange={(value) => updateDraft({ backupMode: value as BackupMode })}
/>
{draft.backupMode === 'differential' && (
<div style={{ marginTop: 8 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
+
</Typography.Text>
<InputNumber
style={{ width: '100%', marginTop: 4 }}
placeholder="强制全量间隔(天)"
prefix="全量间隔(天)"
min={1}
value={draft.diffFullIntervalDays}
onChange={(value) => updateDraft({ diffFullIntervalDays: Number(value ?? 7) })}
/>
</div>
)}
</div>
)}
<div>
<Typography.Text></Typography.Text>
<InputNumber style={{ width: '100%' }} value={draft.retentionDays} min={0} onChange={(value) => updateDraft({ retentionDays: Number(value ?? 0) })} />

View File

@@ -113,6 +113,7 @@ export function BackupRecordsPage() {
<Space size={4}>
<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>}
</Space>
<Typography.Text type="secondary">{formatBytes(record.fileSize)}</Typography.Text>
{record.checksum && (

View File

@@ -26,6 +26,7 @@ export interface BackupRecordSummary {
startedAt: string
completedAt?: string
locked: boolean
backupKind: 'full' | 'differential'
}
export interface StorageUploadResultItem {

View File

@@ -1,6 +1,7 @@
export type BackupTaskType = 'file' | 'mysql' | 'sqlite' | 'postgresql' | 'saphana' | 'mongodb'
export type BackupTaskStatus = 'idle' | 'running' | 'success' | 'failed'
export type BackupCompression = 'gzip' | 'none'
export type BackupMode = 'full' | 'differential'
export interface BackupTaskSummary {
id: number
@@ -25,6 +26,8 @@ export interface BackupTaskSummary {
keepWeekly: number
keepMonthly: number
keepYearly: number
backupMode: BackupMode
diffFullIntervalDays: number
lastRunAt?: string
lastStatus: BackupTaskStatus
verifyEnabled: boolean
@@ -81,6 +84,8 @@ export interface BackupTaskPayload {
keepWeekly: number
keepMonthly: number
keepYearly: number
backupMode: BackupMode
diffFullIntervalDays: number
/** 类型特有的扩展配置(如 SAP HANA 的 backupLevel/backupChannels 等) */
extraConfig?: Record<string, unknown>
verifyEnabled: boolean