refactor: manifest 的写入收进模块函数,依赖写在签名上

Save-ItemRecord 原先直接用脚本级的 $manifest —— "这个函数会改全局账本"这条事实只存在于读代码的人的注意力里。搬进模块后 Manifest 是必填参数,13 个调用点每一个都能看出自己在动账本。

关键判断:**不需要接返回值**。$Manifest.items 是哈希表,传参是引用语义,函数里的写入直接落在调用方那份 manifest 上,所以调用点的形状只是"多了一个 -Manifest $manifest",行为一字不改。这是"有状态依赖该做成什么"的答案里最省的一种:显式参数 + 引用语义。

两条刻意保留、容易被改错的语义也写进了注释并原样搬过来:warnings 与 security 描述的是"当前在位的归档"而不是"这次尝试",只有真的换掉归档才更新 —— 否则"因不完整而保留旧归档"之后,下一次就失去保护了。

搬的过程里把 $Record.finishedAt 那行写坏了(外层双引号把 $Record 提前插值成空,文件里变成 ".finishedAt = ...")。**注意这是上一轮刚记下的同一个坑**:往文件里写 PowerShell 代码时外层一律用单引号。这次是"绿了才提交"把它拦在提交之外 —— 流程修正当场生效。

验收:test.ps1 9/9 全绿(7 与 5.1)、真实清单只读冒烟 4/4。Backup.ps1 从 889 行降到 846 行。
This commit is contained in:
Shuery committed 2026-09-27 10:45:57 +08:00
1 parent 319ec1da78
commit c4452a749c
4 files changed
+76 -56

No files matched your search

+13 -56
View File
@@ -183,49 +183,6 @@ if ($freeSpaceGB -ge 0) {
function Save-ItemRecord {
param($Record, [string]$Action, [string]$Reason, [bool]$ArchiveWarnings = $false)
$previous = $null
if ($manifest.items.Contains($Record.baseName)) { $previous = $manifest.items[$Record.baseName] }
$Record.action = $Action
$Record.reason = $Reason
$Record.finishedAt = (Get-Date).ToString('o')
# warnings 描述的是"当前在位的归档",不是"这次尝试"。
# 只有真正换掉了归档才更新它;否则沿用上一条记录,
# 否则"因为不完整而保留旧归档"之后,下一次就失去保护了。
if ($Action -eq 'backed-up') {
$Record.warnings = $ArchiveWarnings
}
elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) {
$Record.warnings = [bool]$previous.warnings
}
# security 描述的是"当前在位的归档"的旁挂文件,和 warnings 同理:
# 只有真的换了归档才更新它,否则跳过的那次会把已有记录清成 $null。
if ($Action -ne 'backed-up' -and $previous -and ($previous.PSObject.Properties.Name -contains 'security')) {
$Record.security = $previous.security
}
if ($previous) {
if ($previous.PSObject.Properties.Name -contains 'lastSuccessAt') { $Record.lastSuccessAt = $previous.lastSuccessAt }
if ($previous.PSObject.Properties.Name -contains 'successCount') { $Record.successCount = [int]$previous.successCount }
if ($previous.PSObject.Properties.Name -contains 'failCount') { $Record.failCount = [int]$previous.failCount }
}
if ($Action -eq 'backed-up') {
$Record.lastSuccessAt = $Record.finishedAt
$Record.successCount = [int]$Record.successCount + 1
}
elseif ($Action -eq 'failed') {
$Record.failCount = [int]$Record.failCount + 1
}
$manifest.items[$Record.baseName] = $Record
return $Record
}
# 压缩 + 校验 + 原子替换;返回 @{ Ok; ExitCode; Warnings; Reason }
#
@@ -493,7 +450,7 @@ foreach ($line in $lines) {
if (-not $resolved.BaseName) {
$record = New-BakNRetItemRecord -BaseName ('raw:' + $displayPath) -Source $displayPath -ResolvedSource $displayPath -Phase 'parse'
Save-ItemRecord -Record $record -Action 'failed' -Reason '无法生成归档名' | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason '无法生成归档名' | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -513,7 +470,7 @@ foreach ($line in $lines) {
$reason = "归档名 '$baseName' 与清单中的 '$($seenBaseNames[$baseName])' 重复(由 '$displayPath' 生成),两者会互相覆盖"
Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR
$record = New-BakNRetItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup'
Save-ItemRecord -Record $record -Action 'failed' -Reason $reason | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason $reason | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -537,7 +494,7 @@ foreach ($line in $lines) {
# 归档内路径冲突:明确失败,绝不把两块内容静默搅进同一棵树
if ($resolved.Blocking) {
Write-BakNRetLog "失败: $displayPath,$($resolved.Blocking)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $resolved.Blocking | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason $resolved.Blocking | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -561,7 +518,7 @@ foreach ($line in $lines) {
if ($resolved.Items.Count -eq 0) {
$reason = if ($resolved.Error) { $resolved.Error } else { '解析不出任何源路径' }
Write-BakNRetLog "跳过: $displayPath,$reason" -Level WARN
Save-ItemRecord -Record $record -Action 'missing-source' -Reason $reason | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'missing-source' -Reason $reason | Out-Null
$skipped++
continue
}
@@ -573,7 +530,7 @@ foreach ($line in $lines) {
if ($missingItems.Count -ge $resolved.Items.Count) {
$missingText = @($missingItems | ForEach-Object { $_.RealPath }) -join ';'
Write-BakNRetLog "跳过: $displayPath,源路径不存在" -Level WARN
Save-ItemRecord -Record $record -Action 'missing-source' -Reason ('源路径不存在:' + $missingText) | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'missing-source' -Reason ('源路径不存在:' + $missingText) | Out-Null
$skipped++
continue
}
@@ -602,7 +559,7 @@ foreach ($line in $lines) {
$primarySource = $liveItems[0].RealPath
if ([string]::IsNullOrWhiteSpace($primarySource)) {
Write-BakNRetLog "跳过: $displayPath,无法确定主源路径" -Level WARN
Save-ItemRecord -Record $record -Action 'invalid-path' -Reason '归档项没有可用路径' | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'invalid-path' -Reason '归档项没有可用路径' | Out-Null
$skipped++
continue
}
@@ -636,7 +593,7 @@ foreach ($line in $lines) {
if (-not $Force -and $archiveItem -and $sourceLatest -and $sourceLatest -le $archiveItem.LastWriteTime) {
Write-BakNRetLog "跳过: $displayPath,源目录未更新" -Level INFO
$record.archiveBytes = $archiveItem.Length
Save-ItemRecord -Record $record -Action 'skip-unchanged' -Reason ('源最新修改时间 {0} 不晚于归档时间 {1}' -f $sourceLatest, $archiveItem.LastWriteTime) | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'skip-unchanged' -Reason ('源最新修改时间 {0} 不晚于归档时间 {1}' -f $sourceLatest, $archiveItem.LastWriteTime) | Out-Null
$skipped++
continue
}
@@ -651,7 +608,7 @@ foreach ($line in $lines) {
if ($freeSpaceGB -ge 0 -and $estimatedGB -gt 0 -and $freeSpaceGB -lt $estimatedGB) {
$reason = ('剩余空间 {0} GB 不足以写入预估 {1} GB 的新归档' -f $freeSpaceGB, [math]::Round($estimatedGB, 2))
Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $reason | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason $reason | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -659,7 +616,7 @@ foreach ($line in $lines) {
if ($DryRun) {
Write-BakNRetLog ("[试运行] 将打包 {0} -> {1}" -f $sourcePath, $finalPath) -Level INFO
$record.reason = '试运行,未执行压缩'
Save-ItemRecord -Record $record -Action 'planned' -Reason '试运行,未执行压缩' | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'planned' -Reason '试运行,未执行压缩' | Out-Null
$planned++
continue
}
@@ -693,7 +650,7 @@ foreach ($line in $lines) {
if ($excludeError) {
Write-BakNRetLog "失败: $displayPath,$excludeError" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $excludeError | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason $excludeError | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -731,7 +688,7 @@ foreach ($line in $lines) {
if (-not $result.Ok) {
Write-BakNRetLog "备份失败: $displayPath,$($result.Reason)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $result.Reason | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason $result.Reason | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -793,7 +750,7 @@ foreach ($line in $lines) {
if ($securityFatal) {
Write-BakNRetLog "失败: $displayPath,归档已替换,但安全描述符没能存下来(Security.FailOnError = \$true)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason '归档已替换,但安全描述符没能存下来' | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'failed' -Reason '归档已替换,但安全描述符没能存下来' | Out-Null
$failed++; $failures += $displayPath
continue
}
@@ -813,7 +770,7 @@ foreach ($line in $lines) {
Write-BakNRetLog "已留存快照: $target" -Level INFO
}
Save-ItemRecord -Record $record -Action 'backed-up' -Reason $null -ArchiveWarnings $result.Warnings | Out-Null
Save-BakNRetItemRecord -Manifest $manifest -Record $record -Action 'backed-up' -Reason $null -ArchiveWarnings $result.Warnings | Out-Null
$processed++
}
+1
View File
@@ -19,6 +19,7 @@
CompatiblePSEditions = @('Desktop', 'Core')
FunctionsToExport = @(
'Save-BakNRetItemRecord',
'New-BakNRetItemRecord',
'Find-BakNRet7zExecutable',
'Resolve-BakNRetRootedPath',
+3
View File
@@ -140,7 +140,10 @@
. (Join-Path $PSScriptRoot 'Public\New-BakNRetItemRecord.ps1')
. (Join-Path $PSScriptRoot 'Public\Save-BakNRetItemRecord.ps1')
Export-ModuleMember -Function @(
'Save-BakNRetItemRecord',
'New-BakNRetItemRecord',
'Find-BakNRet7zExecutable',
'Resolve-BakNRetRootedPath',
+59
View File
@@ -0,0 +1,59 @@
function Save-BakNRetItemRecord {
<#
.SYNOPSIS
把一条条目记录并进 manifest,并维护"当前在位归档"的那几项(warnings / security / 计数)。
.DESCRIPTION
为什么 Manifest 是参数、而且**不需要**接返回值:$Manifest.items 是哈希表,传参是引用语义,
函数里的写入直接落在调用方持有的那份 manifest 上。原先它躺在 Backup.ps1 里直接用脚本级的
$manifest,那样"这个函数会改全局账本"这条事实只存在于读代码的人的注意力里;现在它写在
签名上,13 个调用点每一个都能看出来自己在动账本。
两条容易被改错、这里刻意保留下来的语义:
* warnings 描述的是"当前在位的归档",不是"这次尝试"。只有真的换掉了归档才更新它,
否则"因为不完整而保留旧归档"之后,下一次就失去保护了。
* security 同理:只有真的换了归档才更新旁挂文件的信息,否则跳过的那次会把已有记录清成 $null。
#>
param(
[Parameter(Mandatory = $true)]$Manifest,
[Parameter(Mandatory = $true)]$Record,
[Parameter(Mandatory = $true)][string]$Action,
[string]$Reason,
[bool]$ArchiveWarnings = $false
)
$previous = $null
if ($Manifest.items.Contains($Record.baseName)) { $previous = $Manifest.items[$Record.baseName] }
$Record.action = $Action
$Record.reason = $Reason
$Record.finishedAt = (Get-Date).ToString('o')
if ($Action -eq 'backed-up') {
$Record.warnings = $ArchiveWarnings
}
elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) {
$Record.warnings = [bool]$previous.warnings
}
if ($Action -ne 'backed-up' -and $previous -and ($previous.PSObject.Properties.Name -contains 'security')) {
$Record.security = $previous.security
}
if ($previous) {
if ($previous.PSObject.Properties.Name -contains 'lastSuccessAt') { $Record.lastSuccessAt = $previous.lastSuccessAt }
if ($previous.PSObject.Properties.Name -contains 'successCount') { $Record.successCount = [int]$previous.successCount }
if ($previous.PSObject.Properties.Name -contains 'failCount') { $Record.failCount = [int]$previous.failCount }
}
if ($Action -eq 'backed-up') {
$Record.lastSuccessAt = $Record.finishedAt
$Record.successCount = [int]$Record.successCount + 1
}
elseif ($Action -eq 'failed') {
$Record.failCount = [int]$Record.failCount + 1
}
$Manifest.items[$Record.baseName] = $Record
return $Record
}