原先结尾只有一行计数加一个失败清单。计数回答有几个,而人真正要读的是哪几个、为什么——尤其当失败或跳过发生在你没盯着屏幕的时候(计划任务)。 新的 Write-BakNRetRunSummary 按动作分组逐条列出:成功组带归档名/体积/耗时/sha256 前 12 位、以及有文件没打进归档的警告标记;跳过组带具体原因(源未更新/源不存在/路径无效);失败组带退出码与原因原文,并且再单列一遍。另有安全描述符与孤儿归档两段,空分组不打印,末尾给耗时。 数据来源是 manifest 里本次运行写下的记录(按 finishedAt 落在运行窗口内筛),而不是让调用方另维护一份清单。 搬的过程里翻车一次并被自己的断言拦住:Get-RecField 只用 PSObject.Properties.Name 判字段存在,而那是 PSCustomObject 的形态;本进程新造的记录是 [ordered] 字典,于是字段一律读成 $null,现象是"本次运行没有写下任何条目记录"(记录明明在)。现在两种形态都认。 一个已知没做的:排除规则只在内联逐条打印,没进汇总——我没定位到那个打印点,不愿意凭猜往条目循环里插桩。 断言:零依赖套件新增一条,用真日志文件验收汇总内容,并断言空分组不出现。验收:test.ps1 9/9 全绿(7 与 5.1)。
138 lines
7.2 KiB
PowerShell
138 lines
7.2 KiB
PowerShell
function Write-BakNRetRunSummary {
|
||
<#
|
||
.SYNOPSIS
|
||
在运行结尾打印分类详细的结果汇总(成功 / 跳过 / 失败 / 安全 / 孤儿归档)。
|
||
|
||
.DESCRIPTION
|
||
为什么需要它:结尾原先只有一行计数加一个失败清单。计数回答"有几个",而人真正要读的是
|
||
"哪几个、为什么" —— 尤其当失败或跳过发生在你没盯着屏幕的时候(计划任务)。
|
||
|
||
数据来源是 manifest 里**本次运行**写下的记录(按 finishedAt 落在运行窗口内筛),而不是让
|
||
调用方另外维护一份清单:那正是"两份数据会不一致"的老问题,也是这个仓库踩过的坑。
|
||
|
||
按动作分组,每组逐条列出,条目带原因与体量;失败额外带退出码与原因原文。
|
||
#>
|
||
param(
|
||
[Parameter(Mandatory = $true)][ValidateSet('backup', 'restore', 'verify')][string]$Mode,
|
||
$Manifest,
|
||
[Parameter(Mandatory = $true)][datetime]$StartedAt,
|
||
[string[]]$Failures = @(),
|
||
[hashtable]$Counters = @{},
|
||
$OrphanArchives = @(),
|
||
[int]$SecurityFailed = 0,
|
||
[int]$SecurityErrorCount = 0,
|
||
[int]$SecurityApplied = 0
|
||
)
|
||
|
||
function Get-RecField {
|
||
param($Record, [string]$Name)
|
||
# 两种数据形态都要认:manifest 从 JSON 读回来是 PSCustomObject(走 PSObject.Properties),
|
||
# 而本进程新造的记录是 [ordered] 字典(走 Contains + 索引器)。
|
||
# 第一版只写了 PSObject 那条路,于是对字典型的记录一律返回 $null —— 表现为
|
||
# "本次运行没有写下任何条目记录",而记录其实好端端地在就不会被列出来。
|
||
if ($Record -is [System.Collections.IDictionary]) {
|
||
if ($Record.Contains($Name)) { return $Record[$Name] }
|
||
return $null
|
||
}
|
||
if ($Record -and ($Record.PSObject.Properties.Name -contains $Name)) { return $Record.$Name }
|
||
return $null
|
||
}
|
||
|
||
Write-BakNRetLog '================ 结果汇总 ================' -Level INFO
|
||
|
||
# 本次运行的记录:按 finishedAt 落在运行窗口内筛(manifest 里还有历次运行的记录)
|
||
$thisRun = @()
|
||
if ($Manifest -and (Get-RecField $Manifest 'items')) {
|
||
foreach ($key in @($Manifest.items.Keys)) {
|
||
$record = $Manifest.items[$key]
|
||
$finished = Get-RecField $record 'finishedAt'
|
||
$stamp = $null
|
||
if ($finished) { $stamp = $finished -as [datetime] }
|
||
if ($stamp -and $stamp -ge $StartedAt) { $thisRun += $record }
|
||
}
|
||
}
|
||
|
||
$groups = [ordered]@{
|
||
'backed-up' = @{ Title = '成功'; Level = 'INFO' }
|
||
'planned' = @{ Title = '试运行计划(没有真的打包)'; Level = 'INFO' }
|
||
'skip-unchanged' = @{ Title = '跳过:源未更新'; Level = 'INFO' }
|
||
'missing-source' = @{ Title = '跳过:源不存在'; Level = 'WARN' }
|
||
'invalid-path' = @{ Title = '跳过:路径无效'; Level = 'WARN' }
|
||
'failed' = @{ Title = '失败'; Level = 'ERROR' }
|
||
}
|
||
|
||
foreach ($action in $groups.Keys) {
|
||
$title = $groups[$action].Title
|
||
$level = $groups[$action].Level
|
||
$bucket = @($thisRun | Where-Object { (Get-RecField $_ 'action') -eq $action })
|
||
if ($bucket.Count -eq 0) { continue }
|
||
|
||
# 成功的那组带上合计体量:这是"这次到底备了多少"最直接的答案
|
||
$head = "$title $($bucket.Count) 个"
|
||
if ($action -eq 'backed-up') {
|
||
$bytes = ($bucket | ForEach-Object { [long](Get-RecField $_ 'sourceBytes') } | Measure-Object -Sum).Sum
|
||
if ($bytes -gt 0) { $head += ('(源合计 {0:N1} MB)' -f ($bytes / 1MB)) }
|
||
}
|
||
Write-BakNRetLog $head -Level $level
|
||
|
||
foreach ($record in ($bucket | Sort-Object { Get-RecField $_ 'baseName' })) {
|
||
$name = Get-RecField $record 'baseName'
|
||
$detail = @()
|
||
$archive = Get-RecField $record 'archive'
|
||
if ($archive) { $detail += $archive }
|
||
$archiveBytes = Get-RecField $record 'archiveBytes'
|
||
if ($archiveBytes) { $detail += ('{0:N1} MB' -f ([long]$archiveBytes / 1MB)) }
|
||
$duration = Get-RecField $record 'durationSec'
|
||
if ($duration) { $detail += ('{0:N1} 秒' -f [double]$duration) }
|
||
$sha = Get-RecField $record 'sha256'
|
||
if ($sha) { $detail += ('sha256 ' + $sha.Substring(0, [Math]::Min(12, $sha.Length))) }
|
||
if (Get-RecField $record 'warnings') { $detail += '有文件没打进归档(归档可能不完整)' }
|
||
$reason = Get-RecField $record 'reason'
|
||
if ($reason) { $detail += $reason }
|
||
$roots = Get-RecField $record 'roots'
|
||
if ($roots -and @($roots).Count -gt 0) { $detail += ('归档顶层:' + (@($roots) -join '、')) }
|
||
$suffix = if ($detail.Count -gt 0) { ' —— ' + ($detail -join ',') } else { '' }
|
||
Write-BakNRetLog " - $name$suffix" -Level $level
|
||
}
|
||
}
|
||
|
||
if ($thisRun.Count -eq 0) {
|
||
Write-BakNRetLog '本次运行没有写下任何条目记录(可能清单为空、或全部被 -Only / -Skip 筛掉)' -Level WARN
|
||
}
|
||
|
||
# 失败清单单独再列一遍:它是这份日志里最该被读到的东西,值得重复且带 Level=ERROR
|
||
if ($Failures.Count -gt 0) {
|
||
Write-BakNRetLog "失败明细($($Failures.Count) 条,退出码非 0 的判据):" -Level ERROR
|
||
foreach ($failure in $Failures) { Write-BakNRetLog " ✗ $failure" -Level ERROR }
|
||
}
|
||
|
||
if ($SecurityFailed -gt 0) {
|
||
Write-BakNRetLog "安全描述符:有 $SecurityFailed 个条目完全没存下来(manifest 的 security.error 里有原文)" -Level WARN
|
||
}
|
||
if ($SecurityErrorCount -gt 0) {
|
||
Write-BakNRetLog "安全描述符:有 $SecurityErrorCount 个条目存在读不到安全描述符的对象;那些对象恢复后的属主/ACL 是新建对象的默认值" -Level WARN
|
||
}
|
||
if ($SecurityApplied -gt 0) {
|
||
Write-BakNRetLog "安全描述符:本次回放成功 $SecurityApplied 个对象" -Level INFO
|
||
}
|
||
|
||
if (@($OrphanArchives).Count -gt 0) {
|
||
Write-BakNRetLog "孤儿归档:$(@($OrphanArchives).Count) 个(当前清单没有任何条目指向,恢复不到)" -Level WARN
|
||
foreach ($orphan in @($OrphanArchives)) {
|
||
$note = ''
|
||
if ($Manifest -and (Get-RecField $Manifest 'items') -and $Manifest.items.Contains($orphan.BaseName)) { $note = ';manifest 里还留着历史记录' }
|
||
Write-BakNRetLog (' - {0}({1:N1} MB,{2}){3}' -f $orphan.Name, ($orphan.Length / 1MB), $orphan.LastWriteTime, $note) -Level WARN
|
||
}
|
||
}
|
||
|
||
# 计数:分组之外的"没进记录"的那些(被 -Only / -Skip 筛掉、试运行里没走到等)靠它兜底
|
||
if ($Counters.Count -gt 0) {
|
||
$pairs = foreach ($k in $Counters.Keys) { '{0} {1}' -f $k, $Counters[$k] }
|
||
Write-BakNRetLog ('计数:' + ($pairs -join ',')) -Level INFO
|
||
}
|
||
|
||
$elapsed = (Get-Date) - $StartedAt
|
||
Write-BakNRetLog ('耗时:{0:N1} 分钟(开始于 {1:yyyy-MM-dd HH:mm:ss})' -f $elapsed.TotalMinutes, $StartedAt) -Level INFO
|
||
Write-BakNRetLog '=========================================' -Level INFO
|
||
}
|