重构为可核对、可恢复的备份工具(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:
Shuery committed 2026-09-21 20:10:18 +08:00
commit dbc0c00554
11 files changed
+2934

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
<#
.SYNOPSIS
注册 / 移除 BakNRet 的每日备份计划任务。
.DESCRIPTION
任务直接调用 Backup.ps1。脚本自身会写日志并按失败数返回退出码,
因此「上次运行结果」在任务计划程序里是可读的,不需要额外包装。
注册计划任务需要管理员权限(本脚本不自己提权,请从管理员终端运行)。
.EXAMPLE
# 先看将要注册什么(只读,不需要管理员)
.\tools\Register-BackupTask.ps1 -At '21:30' -DryRun
.EXAMPLE
.\tools\Register-BackupTask.ps1 -At '21:30'
.EXAMPLE
.\tools\Register-BackupTask.ps1 -Remove
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$TaskName = 'BakNRet Backup',
# 每日触发时间,HH:mm
[ValidatePattern('^\d{1,2}:\d{2}$')]
[string]$At = '21:30',
# 额外传给 Backup.ps1 的参数,例如 @('-Snapshot')
[string[]]$BackupArgument = @(),
[ValidateSet('S4U', 'Interactive')]
[string]$LogonType = 'S4U',
# 用哪个 PowerShell 宿主执行(默认优先 pwsh.exe)
[string]$PowerShellPath,
[switch]$Remove,
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
$backupScript = Join-Path $projectRoot 'Backup.ps1'
if (-not (Test-Path -LiteralPath $backupScript)) {
Write-Error "找不到 Backup.ps1:$backupScript"
exit 1
}
if (-not $PowerShellPath) {
$pwsh = Get-Command pwsh.exe -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
$PowerShellPath = if ($pwsh) { $pwsh } else { (Get-Command powershell.exe -ErrorAction Stop).Source }
}
$argumentParts = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$backupScript`"") + $BackupArgument
$argument = ($argumentParts -join ' ')
Write-Host ''
Write-Host "任务名 : $TaskName"
Write-Host "执行程序 : $PowerShellPath"
Write-Host "参数 : $argument"
Write-Host "触发 : 每天 $At"
Write-Host "登录类型 : $LogonType(S4U = 未登录也运行,但访问不到网络资源)"
Write-Host ''
if ($Remove) {
if ($DryRun) {
Write-Host '[试运行] 将移除计划任务' -ForegroundColor Yellow
exit 0
}
if (-not (Get-Command Unregister-ScheduledTask -ErrorAction SilentlyContinue)) {
Write-Error '当前环境没有计划任务 cmdlet。'
exit 1
}
try {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
Write-Host "已移除计划任务:$TaskName" -ForegroundColor Green
} catch {
Write-Error "移除失败(多半是权限不足,请用管理员终端):$_"
exit 1
}
exit 0
}
if ($DryRun) {
Write-Host '[试运行] 未实际注册。去掉 -DryRun 并确保在管理员终端里执行即可注册。' -ForegroundColor Yellow
exit 0
}
foreach ($cmdlet in 'Register-ScheduledTask', 'New-ScheduledTaskTrigger', 'New-ScheduledTaskAction', 'New-ScheduledTaskSettingsSet', 'New-ScheduledTaskPrincipal') {
if (-not (Get-Command $cmdlet -ErrorAction SilentlyContinue)) {
Write-Error "当前环境缺少 $cmdlet,无法注册计划任务。"
exit 1
}
}
$action = New-ScheduledTaskAction -Execute $PowerShellPath -Argument $argument -WorkingDirectory $projectRoot
$trigger = New-ScheduledTaskTrigger -Daily -At $At
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-MultipleInstances IgnoreNew `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 6)
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$principal = New-ScheduledTaskPrincipal -UserId $identity -LogonType $LogonType -RunLevel Highest
try {
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal -Force -ErrorAction Stop | Out-Null
Write-Host "已注册计划任务:$TaskName(每天 $At)" -ForegroundColor Green
Write-Host "查看上次运行结果:Get-ScheduledTaskInfo -TaskName '$TaskName'" -ForegroundColor DarkGray
Write-Host "手动跑一次验证 :Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor DarkGray
} catch {
Write-Error "注册失败(多半是权限不足,请用管理员终端):$_"
exit 1
}
exit 0