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:
1 parent
8736bb2c67
commit
d32d4b511f
3 files changed
+95
-23
No files matched your search
+39
-23
@@ -1541,33 +1541,49 @@ function New-BaknretArchiveStaging {
|
|||||||
New-Item -ItemType Directory -Path $Root -Force | Out-Null
|
New-Item -ItemType Directory -Path $Root -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($item in $Items) {
|
# 半途失败必须在这里自己清干净,不能把责任留给调用方。
|
||||||
if ([string]::IsNullOrWhiteSpace([string]$item.ArchivePath)) {
|
#
|
||||||
throw "归档项缺少归档内路径:$($item.RealPath)"
|
# 原因:调用方拿到的是**返回值**,而抛错时根本没有返回值 —— Backup.ps1 的 finally 里
|
||||||
}
|
# `$stagingRoot` 还是 $null,而 Remove-BaknretArchiveStaging 对 $null 是直接 return。
|
||||||
|
# 结果是已经建好的 junction 与临时目录永久留在 %TEMP%,而那些 junction 指向的是真实
|
||||||
$linkPath = Join-Path $Root $item.ArchivePath
|
# 数据;临时目录迟早会被某次 Remove-Item -Recurse 扫到,那一下就会走进真实数据。
|
||||||
$parent = Split-Path -Path $linkPath -Parent
|
try {
|
||||||
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
|
foreach ($item in $Items) {
|
||||||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
if ([string]::IsNullOrWhiteSpace([string]$item.ArchivePath)) {
|
||||||
}
|
throw "归档项缺少归档内路径:$($item.RealPath)"
|
||||||
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
|
$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 {
|
function Remove-BaknretArchiveStaging {
|
||||||
|
|||||||
@@ -615,6 +615,32 @@ Describe '归档项与暂存目录' {
|
|||||||
(Get-BaknretArchiveTopName -ArchivePath '') | Should -Be ''
|
(Get-BaknretArchiveTopName -ArchivePath '') | Should -Be ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It '暂存半途失败会自己清干净(不留指向真实数据的 junction)' {
|
||||||
|
# 函数抛错时没有返回值,调用方的 finally 拿到 $null 就什么都不会清 —— 所以清理
|
||||||
|
# 必须由函数自己在 catch 里做,不能指望调用方。
|
||||||
|
$base = Join-Path $env:TEMP ("bnr-stage-leak-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||||
|
$realDir = Join-Path $base 'real'
|
||||||
|
$sandbox = Join-Path $base 'sandbox'
|
||||||
|
New-Item -ItemType Directory -Path $realDir, $sandbox -Force | Out-Null
|
||||||
|
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $realDir 'inner.txt') -Value 'inner'
|
||||||
|
|
||||||
|
$items = @(
|
||||||
|
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
|
||||||
|
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
|
||||||
|
New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false
|
||||||
|
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
|
||||||
|
New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true
|
||||||
|
)
|
||||||
|
|
||||||
|
{ New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') } | Should -Throw
|
||||||
|
|
||||||
|
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
|
||||||
|
@($left | Where-Object { $_.LinkType -eq 'Junction' }).Count | Should -Be 0
|
||||||
|
$left.Count | Should -Be 0
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
It '目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径)' {
|
It '目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径)' {
|
||||||
$items = @(
|
$items = @(
|
||||||
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
|
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
|
||||||
|
|||||||
@@ -415,6 +415,36 @@ Test-Case '归档项的属性集与契约一致(没有旧的 Sources / Dirs /
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Test-Case '暂存半途失败会自己清干净(不留指向真实数据的 junction)' {
|
||||||
|
# 这是"全仓唯一会把 junction 留在真实数据上"那个缺陷的回归断言。
|
||||||
|
# 函数抛错时没有返回值,调用方的 finally 拿到 $null 就什么都不会清 —— 所以清理必须
|
||||||
|
# 由函数自己在 catch 里做,不能指望调用方。
|
||||||
|
$base = Join-Path $env:TEMP ("bnr-stage-leak-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||||
|
$realDir = Join-Path $base 'real'
|
||||||
|
$sandbox = Join-Path $base 'sandbox'
|
||||||
|
New-Item -ItemType Directory -Path $realDir, $sandbox -Force | Out-Null
|
||||||
|
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $realDir 'inner.txt') -Value 'inner'
|
||||||
|
|
||||||
|
$items = @(
|
||||||
|
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
|
||||||
|
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
|
||||||
|
(New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false)
|
||||||
|
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
|
||||||
|
(New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true)
|
||||||
|
)
|
||||||
|
|
||||||
|
$threw = $false
|
||||||
|
try { New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') | Out-Null } catch { $threw = $true }
|
||||||
|
Assert-True $threw '第二项应当抛错(源文件不存在,硬链接与复制都会失败)'
|
||||||
|
|
||||||
|
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
|
||||||
|
$junctions = @($left | Where-Object { $_.LinkType -eq 'Junction' })
|
||||||
|
Assert-Equal 0 $junctions.Count ('残留了指向真实数据的连接点:' + ($junctions.FullName -join '、'))
|
||||||
|
Assert-Equal 0 $left.Count ('暂存目录没有清干净,残留:' + ($left.FullName -join '、'))
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
|
Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
|
||||||
$root = Join-Path $itemSandbox 'stage-src'
|
$root = Join-Path $itemSandbox 'stage-src'
|
||||||
New-Item -ItemType Directory -Path (Join-Path $root 'App') -Force | Out-Null
|
New-Item -ItemType Directory -Path (Join-Path $root 'App') -Force | Out-Null
|
||||||
|
|||||||
Reference in new issue
Block a user