mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-17 21:34:45 +08:00
fix: 按 review 意见重构数据库备份服务(安全性 + 架构 + 健壮性)
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>
This commit is contained in:
@@ -93,8 +93,8 @@ export async function getDownloadURL(id: string): Promise<{ url: string }> {
|
||||
}
|
||||
|
||||
// Restore
|
||||
export async function restoreBackup(id: string): Promise<void> {
|
||||
await apiClient.post(`/admin/backups/${id}/restore`, {}, { timeout: 600000 })
|
||||
export async function restoreBackup(id: string, password: string): Promise<void> {
|
||||
await apiClient.post(`/admin/backups/${id}/restore`, { password }, { timeout: 600000 })
|
||||
}
|
||||
|
||||
export const backupAPI = {
|
||||
|
||||
@@ -1034,6 +1034,7 @@ export default {
|
||||
download: 'Download',
|
||||
restore: 'Restore',
|
||||
restoreConfirm: 'Are you sure you want to restore from this backup? This will overwrite the current database!',
|
||||
restorePasswordPrompt: 'Please enter your admin password to confirm the restore operation',
|
||||
restoreSuccess: 'Database restored successfully',
|
||||
deleteConfirm: 'Are you sure you want to delete this backup?',
|
||||
deleted: 'Backup deleted'
|
||||
|
||||
@@ -1056,6 +1056,7 @@ export default {
|
||||
download: '下载',
|
||||
restore: '恢复',
|
||||
restoreConfirm: '确定要从此备份恢复吗?这将覆盖当前数据库!',
|
||||
restorePasswordPrompt: '请输入管理员密码以确认恢复操作',
|
||||
restoreSuccess: '数据库恢复成功',
|
||||
deleteConfirm: '确定要删除此备份吗?',
|
||||
deleted: '备份已删除'
|
||||
|
||||
@@ -440,9 +440,11 @@ async function downloadBackup(id: string) {
|
||||
|
||||
async function restoreBackup(id: string) {
|
||||
if (!window.confirm(t('admin.backup.actions.restoreConfirm'))) return
|
||||
const password = window.prompt(t('admin.backup.actions.restorePasswordPrompt'))
|
||||
if (!password) return
|
||||
restoringId.value = id
|
||||
try {
|
||||
await adminAPI.backup.restoreBackup(id)
|
||||
await adminAPI.backup.restoreBackup(id, password)
|
||||
appStore.showSuccess(t('admin.backup.actions.restoreSuccess'))
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
|
||||
Reference in New Issue
Block a user