1. S3 凭证加密存储:使用 SecretEncryptor (AES-256-GCM) 加密 SecretAccessKey, 防止备份文件中泄露 S3 凭证,兼容旧的未加密数据 2. 修复 saveRecord 竞态条件:添加 recordsMu 互斥锁保护 records 的 load/save 3. 恢复操作增加服务端验证:handler 层要求重新输入管理员密码,通过 bcrypt 校验,前端弹出密码输入框 4. pg_dump/psql/S3 操作抽象为接口:定义 DBDumper 和 BackupObjectStore 接口, 实现放入 repository 层,遵循项目依赖注入架构规范 5. 改为流式处理避免大数据库 OOM:备份时 pg_dump stdout -> gzip -> io.Pipe -> S3 upload;恢复时 S3 download -> gzip reader -> psql stdin,不再全量加载 6. loadRecords 区分"无数据"和"数据损坏"场景:JSON 解析失败返回明确错误 7. 添加 18 个核心逻辑单元测试:覆盖加密、并发、流式备份/恢复、错误处理等 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
3.0 KiB
TypeScript
115 lines
3.0 KiB
TypeScript
import { apiClient } from '../client'
|
|
|
|
export interface BackupS3Config {
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key?: string
|
|
prefix: string
|
|
force_path_style: boolean
|
|
}
|
|
|
|
export interface BackupScheduleConfig {
|
|
enabled: boolean
|
|
cron_expr: string
|
|
retain_days: number
|
|
retain_count: number
|
|
}
|
|
|
|
export interface BackupRecord {
|
|
id: string
|
|
status: 'pending' | 'running' | 'completed' | 'failed'
|
|
backup_type: string
|
|
file_name: string
|
|
s3_key: string
|
|
size_bytes: number
|
|
triggered_by: string
|
|
error_message?: string
|
|
started_at: string
|
|
finished_at?: string
|
|
expires_at?: string
|
|
}
|
|
|
|
export interface CreateBackupRequest {
|
|
expire_days?: number
|
|
}
|
|
|
|
export interface TestS3Response {
|
|
ok: boolean
|
|
message: string
|
|
}
|
|
|
|
// S3 Config
|
|
export async function getS3Config(): Promise<BackupS3Config> {
|
|
const { data } = await apiClient.get<BackupS3Config>('/admin/backups/s3-config')
|
|
return data
|
|
}
|
|
|
|
export async function updateS3Config(config: BackupS3Config): Promise<BackupS3Config> {
|
|
const { data } = await apiClient.put<BackupS3Config>('/admin/backups/s3-config', config)
|
|
return data
|
|
}
|
|
|
|
export async function testS3Connection(config: BackupS3Config): Promise<TestS3Response> {
|
|
const { data } = await apiClient.post<TestS3Response>('/admin/backups/s3-config/test', config)
|
|
return data
|
|
}
|
|
|
|
// Schedule
|
|
export async function getSchedule(): Promise<BackupScheduleConfig> {
|
|
const { data } = await apiClient.get<BackupScheduleConfig>('/admin/backups/schedule')
|
|
return data
|
|
}
|
|
|
|
export async function updateSchedule(config: BackupScheduleConfig): Promise<BackupScheduleConfig> {
|
|
const { data } = await apiClient.put<BackupScheduleConfig>('/admin/backups/schedule', config)
|
|
return data
|
|
}
|
|
|
|
// Backup operations
|
|
export async function createBackup(req?: CreateBackupRequest): Promise<BackupRecord> {
|
|
const { data } = await apiClient.post<BackupRecord>('/admin/backups', req || {}, { timeout: 600000 })
|
|
return data
|
|
}
|
|
|
|
export async function listBackups(): Promise<{ items: BackupRecord[] }> {
|
|
const { data } = await apiClient.get<{ items: BackupRecord[] }>('/admin/backups')
|
|
return data
|
|
}
|
|
|
|
export async function getBackup(id: string): Promise<BackupRecord> {
|
|
const { data } = await apiClient.get<BackupRecord>(`/admin/backups/${id}`)
|
|
return data
|
|
}
|
|
|
|
export async function deleteBackup(id: string): Promise<void> {
|
|
await apiClient.delete(`/admin/backups/${id}`)
|
|
}
|
|
|
|
export async function getDownloadURL(id: string): Promise<{ url: string }> {
|
|
const { data } = await apiClient.get<{ url: string }>(`/admin/backups/${id}/download-url`)
|
|
return data
|
|
}
|
|
|
|
// Restore
|
|
export async function restoreBackup(id: string, password: string): Promise<void> {
|
|
await apiClient.post(`/admin/backups/${id}/restore`, { password }, { timeout: 600000 })
|
|
}
|
|
|
|
export const backupAPI = {
|
|
getS3Config,
|
|
updateS3Config,
|
|
testS3Connection,
|
|
getSchedule,
|
|
updateSchedule,
|
|
createBackup,
|
|
listBackups,
|
|
getBackup,
|
|
deleteBackup,
|
|
getDownloadURL,
|
|
restoreBackup,
|
|
}
|
|
|
|
export default backupAPI
|