diff --git a/Backup.ps1 b/Backup.ps1 index 09a743a..1a5cc51 100644 --- a/Backup.ps1 +++ b/Backup.ps1 @@ -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++ } diff --git a/BakNRet/BakNRet.psd1 b/BakNRet/BakNRet.psd1 index eb96a5d..239d0cc 100644 --- a/BakNRet/BakNRet.psd1 +++ b/BakNRet/BakNRet.psd1 @@ -19,6 +19,7 @@ CompatiblePSEditions = @('Desktop', 'Core') FunctionsToExport = @( + 'Save-BakNRetItemRecord', 'New-BakNRetItemRecord', 'Find-BakNRet7zExecutable', 'Resolve-BakNRetRootedPath', diff --git a/BakNRet/BakNRet.psm1 b/BakNRet/BakNRet.psm1 index 82415b4..1559181 100644 --- a/BakNRet/BakNRet.psm1 +++ b/BakNRet/BakNRet.psm1 @@ -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', diff --git a/BakNRet/Public/Save-BakNRetItemRecord.ps1 b/BakNRet/Public/Save-BakNRetItemRecord.ps1 new file mode 100644 index 0000000..fe304f2 --- /dev/null +++ b/BakNRet/Public/Save-BakNRetItemRecord.ps1 @@ -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 +}