From 8ecb8a879a1d2f07733b2b27ea5a8e9d2c43a7d3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 3 Oct 2026 15:51:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(backup):=20=E4=BF=AE=E5=A4=8D=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=A4=87=E4=BB=BD=E4=BF=9D=E7=95=99=E4=BF=AE=E5=89=AA?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E5=A4=B1=E6=95=88=EF=BC=8C=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E4=BF=9D=E7=95=99=E9=99=8D=E5=88=B0=202=20?= =?UTF-8?q?=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bug:prune_local 里 ls 的 cd 只发生在命令替换的子 shell 中, rm 却拿裸文件名在 PROJECT_ROOT 下执行——目标文件不存在, 而 rm -f 对不存在的文件静默成功。结果是"清理成功"日志照打、 本地备份数量却一直超出 KEEP(生产 configs 目录累积到 43 份, dump 10 份,均超出 KEEP=7)。 修复:rm 改用 ${dir}/${file} 绝对路径,并补源码级守卫测试锁定。 顺带把 .env.server.example 的默认保留从 7 降到 2: 本地备份只是断网应急,OSS 已有完整备份;而部署脚本的磁盘预检 要求 可用 ≥ 备份目录×2,本地留太多会直接把部署卡死 (这次就是 3.8G 备份触发预检失败 → 被迫清构建缓存 → 全量构建变慢)。 --- .env.server.example | 4 +++- apps/backend/src/db-backup.test.ts | 18 ++++++++++++++++++ deploy/backup-db.sh | 8 ++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.env.server.example b/.env.server.example index 116012d9..d641b2b7 100644 --- a/.env.server.example +++ b/.env.server.example @@ -85,7 +85,9 @@ STORAGE_OSS_REGION=oss-cn-hangzhou # 默认复用上面的 STORAGE_OSS_* 上传到同桶 backups/ 前缀,以下仅需按需调整。 DB_BACKUP_OSS_PREFIX=backups/postgres # 服务器本地保留的备份份数(OSS 端过期请配置桶生命周期规则) -DB_BACKUP_KEEP=7 +# 本地只是断网应急用,OSS 已有完整备份;而 deploy 脚本的磁盘预检要求 +# 可用 >= 备份目录x2,本地留太多会把部署卡死(49G 盘建议不超过 2~3 份)。 +DB_BACKUP_KEEP=2 # 建议为备份单独创建 RAM 用户、只授予 backups/ 前缀权限(策略见 docs/数据库备份与恢复.md); # 使用独立桶/密钥时取消注释覆盖: # DB_BACKUP_OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com diff --git a/apps/backend/src/db-backup.test.ts b/apps/backend/src/db-backup.test.ts index 194d3908..d8335382 100644 --- a/apps/backend/src/db-backup.test.ts +++ b/apps/backend/src/db-backup.test.ts @@ -200,3 +200,21 @@ test('runLatest 跳过没有 manifest 的新对象并选择最近的有效备份 'backups/postgres/valid.dump', ) }) + +test('backup-db.sh 的本地保留修剪必须用绝对路径删除(曾静默失效导致备份无限累积)', async () => { + const { readFile } = await import('node:fs/promises') + const script = await readFile(new URL('../../../deploy/backup-db.sh', import.meta.url), 'utf8') + + const pruneBody = script.slice(script.indexOf('prune_local() {'), script.indexOf('main() {')) + + // 曾经的 bug:cd 只发生在 ls 的命令替换子 shell 里,rm 却拿裸文件名在 + // PROJECT_ROOT 下删(目标不存在),rm -f 静默吞错 → 本地备份远超 KEEP 份数。 + assert.ok( + pruneBody.includes('rm -f -- "${dir}/${file}"'), + '修剪必须删除 ${dir}/${file};只删文件名会在项目根目录下落空', + ) + assert.ok( + !/"\$\{file\}"\s*$/m.test(pruneBody.replace('rm -f -- "${dir}/${file}"', '')), + '不得再出现只按文件名删除的写法', + ) +}) diff --git a/deploy/backup-db.sh b/deploy/backup-db.sh index dc14b621..00d07d8c 100755 --- a/deploy/backup-db.sh +++ b/deploy/backup-db.sh @@ -157,8 +157,12 @@ prune_local() { return fi while IFS= read -r file; do - rm -f -- "${file}" - log "清理本地旧备份:${file}(本地保留 ${KEEP} 份,OSS 仍由生命周期规则管理)" + [[ -n "${file}" ]] || continue + # 上面的 cd 只在命令替换的子 shell 里生效,本循环运行时 cwd 是 PROJECT_ROOT。 + # 必须拼上目录再删:只删文件名等于在 PROJECT_ROOT 下找同名文件(不存在), + # rm -f 会静默吞掉错误——这正是本地备份数量一直超出 KEEP 的原因。 + rm -f -- "${dir}/${file}" + log "清理本地旧备份:${dir}/${file}(本地保留 ${KEEP} 份,OSS 仍由生命周期规则管理)" done <<<"${removed}" }