From 319ec1da78e1d25206b48b3ba47d698ddb09ac73 Mon Sep 17 00:00:00 2001 From: Shuery <2463253700@qq.com> Date: Sun, 27 Sep 2026 10:35:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9D=A1=E7=9B=AE=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E7=9A=84=E5=B7=A5=E5=8E=82=E6=90=AC=E8=BF=9B=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=88manifest=20=E7=9A=84=20schema=20=E6=9C=89?= =?UTF-8?q?=E4=BA=86=E5=94=AF=E4=B8=80=E8=90=BD=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New-ItemRecord 是纯工厂:我先用正则扫过它读了哪些外部状态 —— 一处都没有,输入全在参数里,只有一个 Get-Date。所以这是"有状态核心"里唯一可以零判断搬走的一块。 搬走并改名 New-BakNRetItemRecord,同时补上字段说明:这个有序哈希就是 manifest 每个条目的 schema。它原先藏在 Backup.ps1 里,于是"manifest 条目长什么样"这件事只有一个隐式落点;恢复端与工具脚本读的也是同一份 schema,改字段时漏掉某一侧的风险就出在这里。 新增一条断言把这个 schema 钉住:26 个字段的名字与**顺序**逐项比对(顺序是 manifest diff 可读性的前提),另外断言 attemptedAt 每次现取 —— 后者是"把它误写成模块加载时的常量"这种错唯一能被发现的地方。 验收:test.ps1 9/9 全绿(7 与 5.1)、104 个文件两版解析零错、真实清单只读冒烟 4/4。Backup.ps1 从 920 行降到 889 行。 --- Backup.ps1 | 37 ++----------------- BakNRet/BakNRet.psd1 | 1 + BakNRet/BakNRet.psm1 | 3 ++ BakNRet/Public/New-BakNRetItemRecord.ps1 | 45 ++++++++++++++++++++++++ tests/Run-Tests.ps1 | 20 +++++++++++ 5 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 BakNRet/Public/New-BakNRetItemRecord.ps1 diff --git a/Backup.ps1 b/Backup.ps1 index 46b09b6..09a743a 100644 --- a/Backup.ps1 +++ b/Backup.ps1 @@ -182,37 +182,6 @@ if ($freeSpaceGB -ge 0) { } -function New-ItemRecord { - param([string]$BaseName, [string]$Source, [string]$ResolvedSource, [string]$Phase) - return [ordered]@{ - baseName = $BaseName - source = $Source - resolvedSource = $ResolvedSource - roots = @() - layouts = @() - catalog = $null - archive = $null - action = $null - reason = $null - phase = $Phase - attemptedAt = (Get-Date).ToString('o') - finishedAt = $null - durationSec = $null - exitCode = $null - verified = $false - warnings = $false - attemptWarnings = $false - encrypted = $false - security = $null - sourceFiles = $null - sourceBytes = $null - archiveBytes = $null - sha256 = $null - lastSuccessAt = $null - successCount = 0 - failCount = 0 - } -} function Save-ItemRecord { param($Record, [string]$Action, [string]$Reason, [bool]$ArchiveWarnings = $false) @@ -523,7 +492,7 @@ foreach ($line in $lines) { $resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catalogPath if (-not $resolved.BaseName) { - $record = New-ItemRecord -BaseName ('raw:' + $displayPath) -Source $displayPath -ResolvedSource $displayPath -Phase 'parse' + $record = New-BakNRetItemRecord -BaseName ('raw:' + $displayPath) -Source $displayPath -ResolvedSource $displayPath -Phase 'parse' Save-ItemRecord -Record $record -Action 'failed' -Reason '无法生成归档名' | Out-Null $failed++; $failures += $displayPath continue @@ -543,7 +512,7 @@ foreach ($line in $lines) { if ($seenBaseNames.ContainsKey($baseName)) { $reason = "归档名 '$baseName' 与清单中的 '$($seenBaseNames[$baseName])' 重复(由 '$displayPath' 生成),两者会互相覆盖" Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR - $record = New-ItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup' + $record = New-BakNRetItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup' Save-ItemRecord -Record $record -Action 'failed' -Reason $reason | Out-Null $failed++; $failures += $displayPath continue @@ -555,7 +524,7 @@ foreach ($line in $lines) { continue } - $record = New-ItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup' + $record = New-BakNRetItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup' $record.archive = $baseName + $tool.Extension if ($resolved.CatalogEntry) { $record.catalog = $resolved.CatalogEntry.Path } $finalPath = Join-Path $BackupDir $record.archive diff --git a/BakNRet/BakNRet.psd1 b/BakNRet/BakNRet.psd1 index d9e9ea1..eb96a5d 100644 --- a/BakNRet/BakNRet.psd1 +++ b/BakNRet/BakNRet.psd1 @@ -19,6 +19,7 @@ CompatiblePSEditions = @('Desktop', 'Core') FunctionsToExport = @( + 'New-BakNRetItemRecord', 'Find-BakNRet7zExecutable', 'Resolve-BakNRetRootedPath', 'Test-BakNRetItemSelected', diff --git a/BakNRet/BakNRet.psm1 b/BakNRet/BakNRet.psm1 index 03668c3..82415b4 100644 --- a/BakNRet/BakNRet.psm1 +++ b/BakNRet/BakNRet.psm1 @@ -138,7 +138,10 @@ . (Join-Path $PSScriptRoot 'Public\Find-BakNRet7zExecutable.ps1') +. (Join-Path $PSScriptRoot 'Public\New-BakNRetItemRecord.ps1') + Export-ModuleMember -Function @( + 'New-BakNRetItemRecord', 'Find-BakNRet7zExecutable', 'Resolve-BakNRetRootedPath', 'Test-BakNRetItemSelected', diff --git a/BakNRet/Public/New-BakNRetItemRecord.ps1 b/BakNRet/Public/New-BakNRetItemRecord.ps1 new file mode 100644 index 0000000..40bde25 --- /dev/null +++ b/BakNRet/Public/New-BakNRetItemRecord.ps1 @@ -0,0 +1,45 @@ +function New-BakNRetItemRecord { + <# + .SYNOPSIS + 造一条 manifest 条目的初始记录(未完成状态)。 + + .DESCRIPTION + 这个有序哈希就是 manifest 里每个条目的 schema:谁(baseName / source / resolvedSource)、 + 这次做了什么(phase / action / reason / exitCode)、结果如何(verified / warnings / sha256 / + archiveBytes)、以及累计计数(successCount / failCount / lastSuccessAt)。 + + 它是纯工厂:不读任何外部状态,所有输入都是参数,时间戳自己取。原先它躺在 Backup.ps1 里, + 搬进模块之后这个"manifest 条目长什么样"的事实就有了唯一的落点 —— 恢复端与工具脚本读的 + 也是同一份 schema,改字段时不会再漏掉某一侧。 + #> + param([string]$BaseName, [string]$Source, [string]$ResolvedSource, [string]$Phase) + + return [ordered]@{ + baseName = $BaseName + source = $Source + resolvedSource = $ResolvedSource + roots = @() + layouts = @() + catalog = $null + archive = $null + action = $null + reason = $null + phase = $Phase + attemptedAt = (Get-Date).ToString('o') + finishedAt = $null + durationSec = $null + exitCode = $null + verified = $false + warnings = $false + attemptWarnings = $false + encrypted = $false + security = $null + sourceFiles = $null + sourceBytes = $null + archiveBytes = $null + sha256 = $null + lastSuccessAt = $null + successCount = 0 + failCount = 0 + } +} diff --git a/tests/Run-Tests.ps1 b/tests/Run-Tests.ps1 index 932c750..c4bd481 100644 --- a/tests/Run-Tests.ps1 +++ b/tests/Run-Tests.ps1 @@ -1364,6 +1364,26 @@ Test-Case '模块可以合回单文件:加载器的顺序声明完整、导出 Assert-Equal 0 $noFile.Count ("导出名单里有名字没有对应文件:" + ($noFile -join '、')) } +Test-Case '条目记录的字段名与顺序是 manifest 的 schema(顺序变了 diff 就没法读)' { + # manifest.json 是给人看的:字段按固定顺序排列,两次运行之间只有值会变, + # 于是 git diff / 文本对比才读得下去。顺序被无意改掉时,这个断言会拦下来。 + $expected = @( + 'baseName', 'source', 'resolvedSource', 'roots', 'layouts', 'catalog', 'archive', + 'action', 'reason', 'phase', 'attemptedAt', 'finishedAt', 'durationSec', 'exitCode', + 'verified', 'warnings', 'attemptWarnings', 'encrypted', 'security', 'sourceFiles', + 'sourceBytes', 'archiveBytes', 'sha256', 'lastSuccessAt', 'successCount', 'failCount' + ) + $record = New-BakNRetItemRecord -BaseName 'x' -Source 's' -ResolvedSource 'rs' -Phase 'backup' + Assert-Equal $expected.Count @($record.Keys).Count '字段数不对' + for ($i = 0; $i -lt $expected.Count; $i++) { + Assert-Equal $expected[$i] @($record.Keys)[$i] "第 $($i + 1) 个字段应当是 $($expected[$i])" + } + Assert-True (@($record.attemptedAt) -match '^\d{4}-\d{2}-\d{2}T') 'attemptedAt 应当是 ISO 时间' + # 时间戳每次现取,不能被写成模块加载时的常量 + $again = New-BakNRetItemRecord -BaseName 'y' -Source 's' -ResolvedSource 'rs' -Phase 'backup' + Assert-True ($record.attemptedAt -ne $again.attemptedAt) '两条记录的 attemptedAt 不该相同' +} + # ============================================================================ Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue