fix: make frozen product backup unique and atomic on --force
This commit is contained in:
@@ -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 时已存在即 FileExistsError;True 时把旧产物重命名为
|
||||
.bak.<旧 pools_sha256 前 8 位或 'prev'> 再放行。
|
||||
force: False 时已存在即 FileExistsError;True 时把旧产物重命名为唯一的
|
||||
.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(
|
||||
|
||||
Reference in New Issue
Block a user