重构为可核对、可恢复的备份工具(P0-P3)
修复(P0) - 退出码:改用 .NET Process 继承控制台启动外部命令。Start-Process -PassThru 的 ExitCode 在 PowerShell 7.7.0-preview.4 上恒为 $null,会把成功的压缩判成失败, 并让 "exit 2 -> 删档重试" 的自愈分支永远不可达。 - BackupList.txt 解析:先按第一个 :: 切开再处理引号,修正整行被引号包住时 排除表被吞进路径的问题(该条目此前被静默跳过,其 2.8 GB 归档成了孤儿)。 - 排除分隔符同时接受 , 与 ;:此前解析器只认 ; 而清单里写的是 ,, 等于所有排除规则都没生效。 - 7z 排除参数不再嵌引号,含空格的模式自动转成 ?:旧写法 -x!"路径" 会让引号 成为模式的一部分,导致排除对所有条目都失效。 加固(P1) - 先写临时归档 -> 7z t 校验 -> 原子替换,中断不再污染正式归档。 - 放弃 7z 的更新模式 u:固实压缩下收益极小,却让排除规则改动与已删文件 永远进不了归档。 - 新增 Backups/manifest.json 与 logs/*.log,跳过/失败有据可查。 - 结尾按失败数 exit;恢复支持 -WhatIf / -DryRun / -VerifyOnly / -Only。 - 恢复优先用 manifest 定位归档,并精确比较 BaseName(不再用 -Filter 通配)。 - 修正 tar 分支用 $LASTEXITCODE 判断成功与否的缺陷。 - 有警告(文件被占用)时拒绝用不完整的归档覆盖完整归档,需显式 -AcceptWarnings。 策略与安全(P2) - Edge 条目加排除规则:解压后 4.22 GB 中 3.79 GB 是可再生的缓存/遥测/扩展本体, 保留书签、密码、偏好、历史与站点数据。 - 可选 7z 加密(@encrypt 标记或全局开关),取不到口令时明确失败,绝不写明文。 - 磁盘空间守卫:放不下就跳过该条目,低于阈值告警。 工程化(P3) - 新增 BackupConfig.psd1、README.md、.gitignore。 - tests/Run-Tests.ps1(32 项)与 tests/Run-E2E.ps1(16 项端到端验收)。 - tools/Register-BackupTask.ps1 注册每日计划任务。 - 归档命名算法保持不变,已有归档不会失联。
This commit is contained in:
commit
dbc0c00554
11 files changed
+2934
No files matched your search
@@ -0,0 +1,111 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
BakNRet 测试用的极简断言运行器(零依赖,PS 5.1 / 7.x 通用)。
|
||||
|
||||
.DESCRIPTION
|
||||
不用 Pester:本机只装了 3.4.0,缺少 `Should -Be` 这类 Pester 4+ 语法,
|
||||
为跑几个测试去装 Pester 5 不划算。这里只需要 Test-Case + Assert-* 就够。
|
||||
#>
|
||||
|
||||
$script:Passed = 0
|
||||
$script:Failed = 0
|
||||
$script:Failures = @()
|
||||
|
||||
function Reset-TestResult {
|
||||
$script:Passed = 0
|
||||
$script:Failed = 0
|
||||
$script:Failures = @()
|
||||
}
|
||||
|
||||
function Get-TestResult {
|
||||
return [pscustomobject]@{
|
||||
Passed = $script:Passed
|
||||
Failed = $script:Failed
|
||||
Failures = @($script:Failures)
|
||||
}
|
||||
}
|
||||
|
||||
function Test-Case {
|
||||
param([string]$Name, [scriptblock]$Body)
|
||||
|
||||
try {
|
||||
& $Body
|
||||
$script:Passed++
|
||||
Write-Host " [PASS] $Name" -ForegroundColor Green
|
||||
} catch {
|
||||
$script:Failed++
|
||||
$script:Failures += "$Name —— $($_.Exception.Message)"
|
||||
Write-Host " [FAIL] $Name" -ForegroundColor Red
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Equal {
|
||||
param($Expected, $Actual, [string]$Because)
|
||||
if ($Expected -ne $Actual) {
|
||||
throw ("期望 [{0}],实际 [{1}]{2}" -f $Expected, $Actual, $(if ($Because) { "($Because)" } else { '' }))
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-True {
|
||||
param($Condition, [string]$Because)
|
||||
if (-not $Condition) { throw ("断言为真失败{0}" -f $(if ($Because) { ":$Because" } else { '' })) }
|
||||
}
|
||||
|
||||
function Assert-False {
|
||||
param($Condition, [string]$Because)
|
||||
if ($Condition) { throw ("断言为假失败{0}" -f $(if ($Because) { ":$Because" } else { '' })) }
|
||||
}
|
||||
|
||||
function Assert-Null {
|
||||
param($Value, [string]$Because)
|
||||
if ($null -ne $Value) {
|
||||
throw ("期望为 null,实际 [{0}]{1}" -f $Value, $(if ($Because) { "($Because)" } else { '' }))
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-FileExists {
|
||||
param([string]$Path, [string]$Because)
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw ("文件应当存在但不存在:{0}{1}" -f $Path, $(if ($Because) { "($Because)" } else { '' }))
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-FileMissing {
|
||||
param([string]$Path, [string]$Because)
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
throw ("文件不应存在但存在:{0}{1}" -f $Path, $(if ($Because) { "($Because)" } else { '' }))
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-FileHashEqual {
|
||||
param([string]$ExpectedPath, [string]$ActualPath)
|
||||
Assert-FileExists $ExpectedPath
|
||||
Assert-FileExists $ActualPath
|
||||
$a = (Get-FileHash -LiteralPath $ExpectedPath -Algorithm SHA256).Hash
|
||||
$b = (Get-FileHash -LiteralPath $ActualPath -Algorithm SHA256).Hash
|
||||
if ($a -ne $b) { throw ("内容不一致:{0} vs {1}" -f $ExpectedPath, $ActualPath) }
|
||||
}
|
||||
|
||||
function Write-TestSummary {
|
||||
param([string]$Title = '测试')
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ("=" * 60)
|
||||
if ($script:Failed -eq 0) {
|
||||
Write-Host "$Title 全部通过:$($script:Passed) 项" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "$Title 通过 $($script:Passed) 项,失败 $($script:Failed) 项" -ForegroundColor Red
|
||||
foreach ($failure in $script:Failures) { Write-Host " - $failure" -ForegroundColor Red }
|
||||
}
|
||||
Write-Host ("=" * 60)
|
||||
|
||||
return $script:Failed
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Reset-TestResult', 'Get-TestResult', 'Test-Case',
|
||||
'Assert-Equal', 'Assert-True', 'Assert-False', 'Assert-Null',
|
||||
'Assert-FileExists', 'Assert-FileMissing', 'Assert-FileHashEqual',
|
||||
'Write-TestSummary'
|
||||
)
|
||||
Reference in new issue
Block a user