fix: make frozen product backup unique and atomic on --force

This commit is contained in:
2026-07-16 05:12:08 -04:00
parent a0c7e043e8
commit 8f349c5c62
2 changed files with 92 additions and 5 deletions
+39 -5
View File
@@ -105,17 +105,41 @@ class SplitBuildResult:
return getattr(self, key)
def _unique_backup_path(path: Path, suffix: str) -> Path:
"""求 path 的唯一 .bak.<suffix> 备份路径,已存在则追加递增序号避免覆盖。
首选 ``<name>.bak.<suffix>``;若已存在,退化为 ``<name>.bak.<suffix>.2``、
``.3`` … 直到找到不存在的名字。保证连续 forced freeze(同 suffix 或都缺
manifest 用 'prev')不会静默覆盖此前保留的备份。
参数:
path: 待备份的原文件路径。
suffix: 备份后缀(旧 pools_sha256 前 8 位或 'prev')。
返回:
目录内唯一、尚不存在的备份路径。
"""
candidate = path.with_name(f"{path.name}.bak.{suffix}")
counter = 2
while candidate.exists():
candidate = path.with_name(f"{path.name}.bak.{suffix}.{counter}")
counter += 1
return candidate
def _guard_frozen_products(out_path: Path, manifest_path: Path, *, force: bool) -> None:
"""冻结前的覆盖保护:产物已存在时按 force 决定报错或备份。
参数:
out_path: 目标 pools.json 路径。
manifest_path: 目标 split_manifest.json 路径。
force: False 时已存在即 FileExistsErrorTrue 时把旧产物重命名为
.bak.<旧 pools_sha256 前 8 位或 'prev'> 再放行。
force: False 时已存在即 FileExistsErrorTrue 时把旧产物重命名为唯一的
.bak.<旧 pools_sha256 前 8 位或 'prev'>(同名已存在则追加递增序号)再放行。
异常:
FileExistsError: force=False 且产物已存在(防静默覆盖冻结锚点)。
OSError: 备份 rename 失败;已备份的文件先 rollback 回原名再抛出,保证
要么两文件都备份、要么都不动(原子性,不留半备份的不一致目录)。
"""
if not out_path.exists() and not manifest_path.exists():
return
@@ -132,9 +156,19 @@ def _guard_frozen_products(out_path: Path, manifest_path: Path, *, force: bool)
suffix = str(old.get("pools_sha256", "prev"))[:8] or "prev"
except (json.JSONDecodeError, OSError):
suffix = "prev"
for p in (out_path, manifest_path):
if p.exists():
p.rename(p.with_name(f"{p.name}.bak.{suffix}"))
# 先为每个存在的文件求唯一备份路径(互不冲突),再逐个 rename;
# 中途失败则把已备份的 rollback 回原名,保证原子性。
to_backup = [p for p in (out_path, manifest_path) if p.exists()]
done: list[tuple[Path, Path]] = [] # (备份路径, 原路径),供 rollback
try:
for p in to_backup:
dst = _unique_backup_path(p, suffix)
p.rename(dst)
done.append((dst, p))
except OSError:
for backup_path, original in reversed(done):
backup_path.rename(original)
raise
def build_split(