fix: 暂存目录半途失败会留下指向真实数据的 junction

缺陷:Backup.ps1 用 `$stagingRoot = $null` + try/finally 清理暂存目录,而
New-BaknretArchiveStaging 中途抛错时没有返回值 —— 赋值没发生,finally 拿到的还是 $null,
而 Remove-BaknretArchiveStaging 对 $null 直接 return。结果是已经建好的 junction 与临时
目录永久留在 %TEMP%,而那些 junction 指向真实数据;临时目录迟早会被某次
Remove-Item -Recurse 扫到,那一下就会走进真实数据。全仓唯一会伤到数据的缺陷。

修法:把清理责任放回函数自己身上 —— 循环包进 try,catch 先调
Remove-BaknretArchiveStaging 清掉已经建出来的东西,再 throw 原始错误。调用方的 finally
保持不动(它管的是"暂存建好之后下游才失败"那条路)。自清理失败时只告警并点名残留路径,
不覆盖真正的失败原因 —— 那才是排查需要的。

回归断言(零依赖与 Pester 各一份):第一项走 junction 成功挂上,第二项因源文件不
存在必然抛错;然后断言沙盒里不留任何条目,尤其不留 junction。

断言的有效性做了红绿证明:把 catch 里那行清理临时停用后,同一段场景残留 1 个
junction(.\sandbox\stage\Good,指向真实目录)→ 断言变红;还原后文件哈希一致、断言转绿。
一个不会红的断言不算保护。

验收:test.ps1 9/9 全绿(7 与 5.1);tests\Run-RealSmoke.ps1 4/4 全绿。
This commit is contained in:
Shuery committed 2026-09-26 22:36:44 +08:00
1 parent 8736bb2c67
commit d32d4b511f
3 files changed
+95 -23

No files matched your search

+39 -23
View File
@@ -1541,33 +1541,49 @@ function New-BaknretArchiveStaging {
New-Item -ItemType Directory -Path $Root -Force | Out-Null
}
foreach ($item in $Items) {
if ([string]::IsNullOrWhiteSpace([string]$item.ArchivePath)) {
throw "归档项缺少归档内路径:$($item.RealPath)"
}
$linkPath = Join-Path $Root $item.ArchivePath
$parent = Split-Path -Path $linkPath -Parent
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
if (Test-Path -LiteralPath $linkPath) { Remove-BaknretJunction -Path $linkPath }
if ($item.IsFile) {
try {
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
} catch {
Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
# 半途失败必须在这里自己清干净,不能把责任留给调用方。
#
# 原因:调用方拿到的是**返回值**,而抛错时根本没有返回值 —— Backup.ps1 的 finally 里
# `$stagingRoot` 还是 $null,而 Remove-BaknretArchiveStaging 对 $null 是直接 return。
# 结果是已经建好的 junction 与临时目录永久留在 %TEMP%,而那些 junction 指向的是真实
# 数据;临时目录迟早会被某次 Remove-Item -Recurse 扫到,那一下就会走进真实数据。
try {
foreach ($item in $Items) {
if ([string]::IsNullOrWhiteSpace([string]$item.ArchivePath)) {
throw "归档项缺少归档内路径:$($item.RealPath)"
}
} else {
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
$linkPath = Join-Path $Root $item.ArchivePath
$parent = Split-Path -Path $linkPath -Parent
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
if (Test-Path -LiteralPath $linkPath) { Remove-BaknretJunction -Path $linkPath }
if ($item.IsFile) {
try {
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
} catch {
Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
}
} else {
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
}
Write-Log ("暂存:{0} -> {1}" -f $item.ArchivePath, $item.RealPath) -Level DEBUG
}
Write-Log ("暂存:{0} -> {1}" -f $item.ArchivePath, $item.RealPath) -Level DEBUG
return $Root
} catch {
try {
Remove-BaknretArchiveStaging -Root $Root
} catch {
# 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径
Write-Log ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
}
throw
}
return $Root
}
function Remove-BaknretArchiveStaging {