refactor: 公共面补 BakNRet 前缀,产品名大小写全仓统一

16 个没有前缀的公共函数补上 BakNRet(Write-Log → Write-BakNRetLog、Resolve-BackupEntry →
Resolve-BakNRetBackupEntry、Find-ChildDirectoryByName → Find-BakNRetChildDirectoryByName 等),
另外把全仓的 Baknret 统一成 BakNRet(47 个文件、940 处、65 个定义文件重命名)。

这不是审美问题:静态分析直接拓出一条实据 —— Write-Log 与本机某个已装模块导出的命令
**重名**(PSAvoidOverwritingBuiltInCmdlets),而重名的后果是导入两个模块时有一方的命令被
静默遮蔽。补前缀正是这条规则的解法,改名后它归零。

为什么敢做这个规模:PowerShell 的函数名解析大小写不敏感,所以 Baknret → BakNRet 在功能
上是零风险;真正要验证的是 16 个补前缀的调用点,而 276 个断言几乎覆盖了每个函数。另外
"名字与文件名一致"这条不变式有断言盯着(加载器点源的文件集合 vs 磁盘)。

踩到并记下的坑:Windows 文件系统大小写不敏感,所以**只改大小写**的重命名会被 Move-Item
当成同一个文件而静默跳过 —— 同一批里同时改了名字的那 16 个文件却成功了,于是"看起来能跑"。
最后用"先移到临时名、再移到目标名"的两步走解决,判断与替换全部改用显式大小写敏感的形式
(-creplace / -cmatch)。

顺带把名录指纹缓存从 MD5 换成 SHA256(PSAvoidUsingBrokenHashAlgorithms):它只是缓存键,
没有兼容负担。

验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、276 个断言全过、
构建工具仍能合回单文件(3300 行)。
This commit is contained in:
Shuery committed 2026-09-27 09:56:36 +08:00
1 parent 187d2759fd
commit 42f02d0eca
84 files changed
+1043 -1043

No files matched your search

+89 -89
View File
@@ -81,9 +81,9 @@ if (-not (Test-Path -LiteralPath $modulePath)) {
}
Import-Module $modulePath -Force
if ($PSBoundParameters.ContainsKey('Verbose')) { Set-BaknretDebug }
if ($PSBoundParameters.ContainsKey('Verbose')) { Set-BakNRetDebug }
$script:Config = Get-BaknretConfig -Path $ConfigPath
$script:Config = Get-BakNRetConfig -Path $ConfigPath
$SupportedFormats = @('.7z', '.rar', '.zip', '.tar')
function Resolve-ConfigPath {
@@ -97,33 +97,33 @@ function Resolve-ConfigPath {
if (-not $BackupDir) { $BackupDir = Resolve-ConfigPath -Path $null -Default $script:Config.BackupDir }
$logDir = Resolve-ConfigPath -Path $null -Default $script:Config.LogDir
$catalogPath = Resolve-CatalogPath -Configured $script:Config.SoftwareCatalog -Root $PSScriptRoot
$catalogPath = Resolve-BakNRetCatalogPath -Configured $script:Config.SoftwareCatalog -Root $PSScriptRoot
$manifestPath = Join-Path $BackupDir 'manifest.json'
$logPath = Start-BaknretLog -Directory $logDir -Prefix 'restore'
Write-Log "日志文件:$logPath"
Write-Log "备份目录:$BackupDir"
Write-Log ("软件名录:{0}{1}" -f $catalogPath, $(if (Test-Path -LiteralPath $catalogPath) { '' } else { '(不存在,将只支持字面路径)' }))
if ($WhatIfPreference) { Write-Log '试运行模式(-WhatIf / -DryRun):不会写入任何文件' -Level WARN }
$logPath = Start-BakNRetLog -Directory $logDir -Prefix 'restore'
Write-BakNRetLog "日志文件:$logPath"
Write-BakNRetLog "备份目录:$BackupDir"
Write-BakNRetLog ("软件名录:{0}{1}" -f $catalogPath, $(if (Test-Path -LiteralPath $catalogPath) { '' } else { '(不存在,将只支持字面路径)' }))
if ($WhatIfPreference) { Write-BakNRetLog '试运行模式(-WhatIf / -DryRun):不会写入任何文件' -Level WARN }
if (-not (Test-Administrator)) {
Write-Log '建议以管理员身份运行以获取完整的目录访问权限' -Level WARN
if (-not (Test-BakNRetAdministrator)) {
Write-BakNRetLog '建议以管理员身份运行以获取完整的目录访问权限' -Level WARN
}
# 同一份备份目录同一时间只允许一个进程操作(见 BakNRet 模块的「运行锁」一节)。
# 三种只读模式不取锁:它们一个字节都不写,没必要被正在跑的备份挡在外面。
$runLock = $null
if (-not $WhatIfPreference -and -not $VerifyOnly) {
$runLock = Enter-BaknretRunLock -Directory $BackupDir
$runLock = Enter-BakNRetRunLock -Directory $BackupDir
if (-not $runLock) {
Write-Log ("另一次运行正在进行中(锁文件:{0},里面写明了持有者)。本次不执行。" -f (Get-BaknretRunLockPath -Directory $BackupDir)) -Level ERROR
Stop-BaknretLog
Write-BakNRetLog ("另一次运行正在进行中(锁文件:{0},里面写明了持有者)。本次不执行。" -f (Get-BakNRetRunLockPath -Directory $BackupDir)) -Level ERROR
Stop-BakNRetLog
exit 1
}
Write-Log ("已取得运行锁:{0}" -f (Get-BaknretRunLockPath -Directory $BackupDir)) -Level DEBUG
Write-BakNRetLog ("已取得运行锁:{0}" -f (Get-BakNRetRunLockPath -Directory $BackupDir)) -Level DEBUG
}
$passwordFile = if ($KeyFile) { $KeyFile } else { $script:Config.Encryption.PasswordFile }
$password = Get-BaknretPassword -PasswordFile $passwordFile
$password = Get-BakNRetPassword -PasswordFile $passwordFile
# ============================================================================
# 归档查找
@@ -157,7 +157,7 @@ function Get-ArchiveForEntry {
if (Test-Path -LiteralPath $path) {
return [pscustomobject]@{ File = (Get-Item -LiteralPath $path); Source = 'manifest'; Record = $record }
}
Write-Log "manifest 记录的归档不存在,回退按文件名查找:$archiveName" -Level WARN
Write-BakNRetLog "manifest 记录的归档不存在,回退按文件名查找:$archiveName" -Level WARN
}
}
@@ -207,7 +207,7 @@ function Invoke-ExtractionRaw {
$sevenZip = Get-7zExecutable
if ($sevenZip) {
Write-Log '使用 7z 解压' -Level DEBUG
Write-BakNRetLog '使用 7z 解压' -Level DEBUG
$argument = @('x', '-bsp2', '-y', "-o$Destination")
if ($Password) { $argument += "-p$Password" }
$argument += $ArchiveFile.FullName
@@ -222,23 +222,23 @@ function Invoke-ExtractionRaw {
'.rar' {
$rarExe = Get-Command rar, unrar -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
if (-not $rarExe) { throw '未找到 RAR 工具' }
Write-Log '使用 RAR 解压' -Level DEBUG
Write-BakNRetLog '使用 RAR 解压' -Level DEBUG
$argument = @('x', '-idp', '-idn', '-y', $ArchiveFile.FullName, "$Destination\")
if ($RelativePath) { $argument += $RelativePath }
$exitCode = Invoke-ExternalCommand -FilePath $rarExe -ArgumentList $argument
if ($exitCode -ne 0) { throw "RAR 解压失败(退出码:$exitCode)" }
}
'.zip' {
Write-Log '使用内置 ZIP 解压' -Level DEBUG
Write-BakNRetLog '使用内置 ZIP 解压' -Level DEBUG
if ($RelativePath) {
Write-Log "内置 ZIP 不支持只解子树,将整包解压($RelativePath)" -Level WARN
Write-BakNRetLog "内置 ZIP 不支持只解子树,将整包解压($RelativePath)" -Level WARN
}
Expand-Archive -LiteralPath $ArchiveFile.FullName -DestinationPath $Destination -Force
}
'.tar' {
$tarExe = Get-Command tar -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
if (-not $tarExe) { throw '未找到 TAR 工具' }
Write-Log '使用 TAR 解压' -Level DEBUG
Write-BakNRetLog '使用 TAR 解压' -Level DEBUG
$argument = @('-xf', $ArchiveFile.FullName, '-C', $Destination)
if ($RelativePath) { $argument += $RelativePath }
$exitCode = Invoke-ExternalCommand -FilePath $tarExe -ArgumentList $argument
@@ -308,18 +308,18 @@ function Invoke-ExtractionByLayout {
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
}
$anchorName = Get-BaknretArchiveTopName -ArchivePath $archivePath
$anchorName = Get-BakNRetArchiveTopName -ArchivePath $archivePath
$anchorPath = if ($anchorName) { Join-Path $destParent $anchorName } else { $null }
$junctionCreated = $false
if ($anchorPath -and -not (Test-Path -LiteralPath $anchorPath)) {
try {
New-BaknretJunction -Path $anchorPath -Target $destPath | Out-Null
New-BakNRetJunction -Path $anchorPath -Target $destPath | Out-Null
$junctionCreated = $true
Write-Log ("落地:{0} -> {1}(经连接点 {2})" -f $archivePath, $destPath, $anchorPath) -Level DEBUG
Write-BakNRetLog ("落地:{0} -> {1}(经连接点 {2})" -f $archivePath, $destPath, $anchorPath) -Level DEBUG
}
catch {
Write-Log "无法建连接点($($_.Exception.Message)),改为先解到临时目录再合并" -Level WARN
Write-BakNRetLog "无法建连接点($($_.Exception.Message)),改为先解到临时目录再合并" -Level WARN
}
}
@@ -328,11 +328,11 @@ function Invoke-ExtractionByLayout {
return (Invoke-ExtractionRaw -ArchiveFile $ArchiveFile -Destination $destParent -RelativePath $archivePath -Password $Password)
}
finally {
Remove-BaknretJunction -Path $anchorPath
Remove-BakNRetJunction -Path $anchorPath
}
}
Write-Log ("落地:{0} -> {1}(先解到临时目录再合并)" -f $archivePath, $destPath) -Level WARN
Write-BakNRetLog ("落地:{0} -> {1}(先解到临时目录再合并)" -f $archivePath, $destPath) -Level WARN
$temp = Join-Path $env:TEMP ('bnr-merge-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $temp -Force | Out-Null
try {
@@ -353,7 +353,7 @@ function Invoke-ExtractionByLayout {
return $true
}
function Test-BaknretArchivePath {
function Test-BakNRetArchivePath {
<#
.SYNOPSIS
归档里有没有这条路径。
@@ -394,14 +394,14 @@ function Test-BaknretArchivePath {
$argument += $item
$null = Start-Process -FilePath $sevenZip `
-ArgumentList (ConvertTo-NativeArgumentString -ArgumentList $argument) `
-ArgumentList (ConvertTo-BakNRetNativeArgumentString -ArgumentList $argument) `
-RedirectStandardOutput $outFile -RedirectStandardError $errFile `
-NoNewWindow -Wait -PassThru
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
}
catch {
Write-Log "无法列出归档内容(跳过预判):$($_.Exception.Message)" -Level DEBUG
Write-BakNRetLog "无法列出归档内容(跳过预判):$($_.Exception.Message)" -Level DEBUG
return $true
}
finally {
@@ -443,13 +443,13 @@ function Invoke-Extraction {
$destPath = [string]$Item.RealPath
$legacyName = Split-Path -Path $destPath -Leaf
if (Test-BaknretArchivePath -ArchiveFile $ArchiveFile -RelativePath $archivePath -Password $Password) {
if (Test-BakNRetArchivePath -ArchiveFile $ArchiveFile -RelativePath $archivePath -Password $Password) {
return (Invoke-ExtractionByLayout -ArchiveFile $ArchiveFile -Item $Item -Password $Password)
}
if ($legacyName -and ($legacyName -ine $archivePath) -and
(Test-BaknretArchivePath -ArchiveFile $ArchiveFile -RelativePath $legacyName -Password $Password)) {
Write-Log ("归档里没有 '{0}'(可能是重构前的旧归档),按旧布局回退为 '{1}'" -f $archivePath, $legacyName) -Level WARN
(Test-BakNRetArchivePath -ArchiveFile $ArchiveFile -RelativePath $legacyName -Password $Password)) {
Write-BakNRetLog ("归档里没有 '{0}'(可能是重构前的旧归档),按旧布局回退为 '{1}'" -f $archivePath, $legacyName) -Level WARN
$parent = Split-Path -Path $destPath -Parent
if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
return (Invoke-ExtractionRaw -ArchiveFile $ArchiveFile -Destination $parent -RelativePath $legacyName -Password $Password)
@@ -464,15 +464,15 @@ function Invoke-Extraction {
# ============================================================================
if (-not (Test-Path -LiteralPath $BackupDir)) {
Write-Log "备份目录不存在: $BackupDir" -Level ERROR
Stop-BaknretLog
Write-BakNRetLog "备份目录不存在: $BackupDir" -Level ERROR
Stop-BakNRetLog
exit 1
}
$manifest = Read-BaknretManifest -Path $manifestPath
$manifest = Read-BakNRetManifest -Path $manifestPath
if (-not (Test-Path -LiteralPath $BackupListPath)) {
Write-Log '未找到配置文件,正在从备份内容生成...' -Level INFO
Write-BakNRetLog '未找到配置文件,正在从备份内容生成...' -Level INFO
$paths = @()
@@ -489,22 +489,22 @@ if (-not (Test-Path -LiteralPath $BackupListPath)) {
$backupFiles = Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -match '_from_' }
foreach ($file in $backupFiles) {
$original = Convert-BackupFileNameToPath -FileName $file.Name
$original = Convert-BakNRetBackupFileNameToPath -FileName $file.Name
if ($original) { $paths += $original }
}
}
$paths = @($paths | Sort-Object -Unique)
if ($paths.Count -eq 0) {
Write-Log '无法从备份内容还原出任何路径。' -Level ERROR
Stop-BaknretLog
Write-BakNRetLog '无法从备份内容还原出任何路径。' -Level ERROR
Stop-BakNRetLog
exit 1
}
$content = "# BackupList.txt(自动生成,排除规则需要手工补回)`n" + (($paths -join [Environment]::NewLine) + [Environment]::NewLine)
[System.IO.File]::WriteAllText($BackupListPath, $content, [System.Text.UTF8Encoding]::new($true))
Write-Log "已生成配置,包含 $($paths.Count) 个项目,请检查后重新运行" -Level INFO
Stop-BaknretLog
Write-BakNRetLog "已生成配置,包含 $($paths.Count) 个项目,请检查后重新运行" -Level INFO
Stop-BakNRetLog
exit 0
}
@@ -539,14 +539,14 @@ $referencedArchives = @()
# "试运行不会写入任何文件" 的承诺(已用 manifest 的 SHA256 复现)。
$manifestDirty = $false
Write-Log '开始执行恢复' -Level INFO
Write-BakNRetLog '开始执行恢复' -Level INFO
foreach ($line in $lines) {
$item = ConvertFrom-BackupListLine -Line $line
if (-not $item) { continue }
$displayPath = $item.Path
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $script:Config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $script:Config.CatalogMaxDepth
$baseName = $resolved.BaseName
if (-not $baseName) { $stats.skipped++; continue }
@@ -556,14 +556,14 @@ foreach ($line in $lines) {
# 仅备份的条目照样要登记归档名:审计要能看出"这个归档是有主的",
# 否则它会被误报成孤儿(只是它本来就恢复不到,因为行首写了 +)。
$referencedArchives += $baseName
Write-Log "跳过(行首 +,仅备份): $displayPath" -Level DEBUG
Write-BakNRetLog "跳过(行首 +,仅备份): $displayPath" -Level DEBUG
continue
}
# 解析阶段就定死的结构性错误(名录条目有问题、归档内路径冲突):
# 恢复一半比明确失败更危险,所以整条失败。
if ($resolved.Blocking) {
Write-Log "失败: $displayPath,$($resolved.Blocking)" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$($resolved.Blocking)" -Level ERROR
$stats.failed++
$failures += $displayPath
continue
@@ -571,7 +571,7 @@ foreach ($line in $lines) {
$found = Get-ArchiveForEntry -Entry ([pscustomobject]@{ baseName = $baseName }) -Manifest $manifest
if (-not $found) {
Write-Log "跳过: $displayPath,未找到归档 $baseName" -Level WARN
Write-BakNRetLog "跳过: $displayPath,未找到归档 $baseName" -Level WARN
$stats.skipped++
continue
}
@@ -639,7 +639,7 @@ foreach ($line in $lines) {
$targets = @($targets | Where-Object { $_.DestPath -and -not [string]::IsNullOrWhiteSpace($_.DestPath) })
if ($targets.Count -eq 0) {
$reason = "无法确定恢复目的地(清单条目 '$displayPath' 解析不出任何归档项)"
Write-Log "失败: $displayPath,$reason" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR
$stats.failed++
$failures += $displayPath
continue
@@ -657,7 +657,7 @@ foreach ($line in $lines) {
$isEncrypted = [bool]$found.Record.encrypted
}
if ($isEncrypted -and -not $password) {
Write-Log "失败: $displayPath,归档已加密但取不到口令(设置 BAKNRET_PASSWORD 或用 -KeyFile 指定密码文件)" -Level ERROR
Write-BakNRetLog "失败: $displayPath,归档已加密但取不到口令(设置 BAKNRET_PASSWORD 或用 -KeyFile 指定密码文件)" -Level ERROR
$stats.failed++
$failures += $displayPath
continue
@@ -665,12 +665,12 @@ foreach ($line in $lines) {
if ($VerifyOnly) {
if ($archiveFile.Extension.ToLower() -ne '.7z') {
Write-Log "跳过校验(非 7z): $($archiveFile.Name)" -Level DEBUG
Write-BakNRetLog "跳过校验(非 7z): $($archiveFile.Name)" -Level DEBUG
continue
}
$verifyTool = Get-7zExecutable
if (-not $verifyTool) {
Write-Log '未找到 7z,无法校验' -Level ERROR
Write-BakNRetLog '未找到 7z,无法校验' -Level ERROR
$stats.failed++
$failures += $displayPath
continue
@@ -680,45 +680,45 @@ foreach ($line in $lines) {
$verifyArgument += $archiveFile.FullName
$verifyCode = Invoke-ExternalCommand -FilePath $verifyTool -ArgumentList $verifyArgument
if ($verifyCode -eq 0) {
Write-Log "校验通过: $($archiveFile.Name)" -Level INFO
Write-BakNRetLog "校验通过: $($archiveFile.Name)" -Level INFO
$stats.verified++
}
else {
Write-Log "校验失败: $($archiveFile.Name)(退出码 $verifyCode)" -Level ERROR
Write-BakNRetLog "校验失败: $($archiveFile.Name)(退出码 $verifyCode)" -Level ERROR
$stats.failed++
$failures += $displayPath
}
continue
}
Write-Log "准备恢复: $displayPath <- $($archiveFile.Name)(来源:$($found.Source))" -Level INFO
Write-BakNRetLog "准备恢复: $displayPath <- $($archiveFile.Name)(来源:$($found.Source))" -Level INFO
if ((Test-Path -LiteralPath $destPath) -and -not $Force) {
try {
$destSummary = Get-FolderSummary -FolderPath $destPath
$destSummary = Get-BakNRetFolderSummary -FolderPath $destPath
$archiveTime = $archiveFile.LastWriteTime
if ($destSummary.LatestModifiedTime -and $destSummary.LatestModifiedTime -gt $archiveTime) {
Write-Log "跳过: $displayPath,目标目录比归档新(用 -Force 覆盖)" -Level WARN
Write-BakNRetLog "跳过: $displayPath,目标目录比归档新(用 -Force 覆盖)" -Level WARN
$stats.skipped++
continue
}
}
catch {
Write-Log "目标目录摘要读取失败,继续恢复:$_" -Level DEBUG
Write-BakNRetLog "目标目录摘要读取失败,继续恢复:$_" -Level DEBUG
}
}
$plannedTargets = @($targets | Where-Object { $_.DestPath })
# 说清楚"这条会把哪些目录还原到哪儿、为什么"
Write-Log ("恢复计划:{0}(归档 {1})" -f $displayPath, $archiveFile.Name)
Write-BakNRetLog ("恢复计划:{0}(归档 {1})" -f $displayPath, $archiveFile.Name)
foreach ($target in $plannedTargets) {
$targetExists = Test-Path -LiteralPath $target.DestPath
Write-Log (" 目标:{0}" -f $target.DestPath)
Write-Log (" 归档内路径:{0}({1});{2}" -f $target.ArchivePath,
Write-BakNRetLog (" 目标:{0}" -f $target.DestPath)
Write-BakNRetLog (" 归档内路径:{0}({1});{2}" -f $target.ArchivePath,
$(if ($target.IsFile) { '文件' } else { '目录' }),
$(if ($targetExists) { '已存在,将覆盖同名文件' } else { '不存在,将新建' }))
if ($target.Description) { Write-Log (" 介绍:{0}" -f $target.Description) }
if ($target.Description) { Write-BakNRetLog (" 介绍:{0}" -f $target.Description) }
}
foreach ($target in $plannedTargets) {
@@ -726,10 +726,10 @@ foreach ($line in $lines) {
# Split-Path -Parent 对根路径(如 "E:\")返回空串,此时无父目录可建
$targetParent = Split-Path -Path $target.DestPath -Parent
if ($targetParent) {
Write-Log "提示: 目标不存在,将新建 $targetParent" -Level DEBUG
Write-BakNRetLog "提示: 目标不存在,将新建 $targetParent" -Level DEBUG
}
else {
Write-Log "提示: 目标不存在,且没有可创建的父目录:$($target.DestPath)" -Level DEBUG
Write-BakNRetLog "提示: 目标不存在,且没有可创建的父目录:$($target.DestPath)" -Level DEBUG
}
}
@@ -740,7 +740,7 @@ foreach ($line in $lines) {
if (-not $shouldRun) {
foreach ($target in $plannedTargets) {
Write-Log "[试运行] 将解压 $($archiveFile.Name) -> $($target.DestPath)" -Level INFO
Write-BakNRetLog "[试运行] 将解压 $($archiveFile.Name) -> $($target.DestPath)" -Level INFO
}
$stats.planned++
continue
@@ -764,10 +764,10 @@ foreach ($line in $lines) {
# CREATOR OWNER 把全权给"对象的属主",属主一变,原程序就没了权限。
# 必须在解压**之后**、对真实目标路径做(连接点在 Invoke-Extraction 里已经拆掉了)。
if ($SkipSecurity) {
Write-Log '按 -SkipSecurity 跳过了安全描述符恢复' -Level DEBUG
Write-BakNRetLog '按 -SkipSecurity 跳过了安全描述符恢复' -Level DEBUG
}
elseif (([string]$script:Config.Security.Mode) -eq 'Off') {
Write-Log '配置里 Security.Mode = Off,跳过安全描述符恢复' -Level DEBUG
Write-BakNRetLog '配置里 Security.Mode = Off,跳过安全描述符恢复' -Level DEBUG
}
else {
$sidecarName = $null
@@ -776,9 +776,9 @@ foreach ($line in $lines) {
}
if (-not $sidecarName) { $sidecarName = "$baseName.acl.json" }
$sidecar = Read-BaknretSecuritySidecar -Path (Join-Path $BackupDir $sidecarName)
$sidecar = Read-BakNRetSecuritySidecar -Path (Join-Path $BackupDir $sidecarName)
if (-not $sidecar) {
Write-Log ("这个归档没有安全描述符旁挂文件({0}):恢复出来的属主/ACL 是新建对象的默认值 —— 原程序若依赖特殊权限(ProgramData 下的 CREATOR OWNER 最典型),会报无读写权限" -f $sidecarName) -Level WARN
Write-BakNRetLog ("这个归档没有安全描述符旁挂文件({0}):恢复出来的属主/ACL 是新建对象的默认值 —— 原程序若依赖特殊权限(ProgramData 下的 CREATOR OWNER 最典型),会报无读写权限" -f $sidecarName) -Level WARN
}
else {
$sidMap = @{}
@@ -787,7 +787,7 @@ foreach ($line in $lines) {
$secTotal = 0; $secApplied = 0; $secOwnerFailed = 0; $secSkipped = 0; $secFailed = 0
$secMessages = @()
foreach ($target in $plannedTargets) {
$sec = Restore-BaknretSecurity -Sidecar $sidecar -ArchiveRoot $target.ArchivePath `
$sec = Restore-BakNRetSecurity -Sidecar $sidecar -ArchiveRoot $target.ArchivePath `
-TargetPath $target.DestPath -SidMap $sidMap
$secTotal += $sec.Total
$secApplied += $sec.Applied
@@ -798,20 +798,20 @@ foreach ($line in $lines) {
}
$securityApplied += $secApplied
Write-Log ("安全描述符:回放 {0}/{1} 个对象(属主/属组未恢复 {2},跳过 {3},失败 {4})" -f `
Write-BakNRetLog ("安全描述符:回放 {0}/{1} 个对象(属主/属组未恢复 {2},跳过 {3},失败 {4})" -f `
$secApplied, $secTotal, $secOwnerFailed, $secSkipped, $secFailed) -Level INFO
foreach ($message in @($secMessages | Select-Object -First 5)) {
Write-Log (" ! {0}" -f $message) -Level WARN
Write-BakNRetLog (" ! {0}" -f $message) -Level WARN
}
if ($secFailed -gt 0) {
Write-Log ("恢复成功但安全描述符有 {0} 个对象失败,已计入失败条目(退出码 1)" -f $secFailed) -Level ERROR
Write-BakNRetLog ("恢复成功但安全描述符有 {0} 个对象失败,已计入失败条目(退出码 1)" -f $secFailed) -Level ERROR
$failures += $displayPath
}
}
}
$stats.restored++
Write-Log "恢复成功: $baseName" -Level INFO
Write-BakNRetLog "恢复成功: $baseName" -Level INFO
if ($manifest.items.Contains($baseName)) {
$record = $manifest.items[$baseName]
@@ -830,7 +830,7 @@ foreach ($line in $lines) {
}
}
catch {
Write-Log "恢复失败: $displayPath,$_" -Level ERROR
Write-BakNRetLog "恢复失败: $displayPath,$_" -Level ERROR
$stats.failed++
$failures += $displayPath
}
@@ -845,48 +845,48 @@ if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -notin $referencedArchives })
if ($orphans.Count -gt 0) {
Write-Log '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -Level WARN
Write-BakNRetLog '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -Level WARN
foreach ($orphan in $orphans) {
Write-Log (" - {0}({1} MB,{2})" -f $orphan.Name, [math]::Round($orphan.Length / 1MB, 2), $orphan.LastWriteTime) -Level WARN
Write-BakNRetLog (" - {0}({1} MB,{2})" -f $orphan.Name, [math]::Round($orphan.Length / 1MB, 2), $orphan.LastWriteTime) -Level WARN
}
}
}
elseif (-not $VerifyOnly) {
# 带 -Only/-Skip 时只有被选中的条目会被处理,其余归档都不在 $referencedArchives 里,
# 按上面的算法报出来全是假孤儿(还会吓唬人说"注意别误删"),所以整段跳过。
Write-Log '本次只恢复了部分条目,跳过孤儿归档审计(避免把未选中的归档误报成孤儿)' -Level DEBUG
Write-BakNRetLog '本次只恢复了部分条目,跳过孤儿归档审计(避免把未选中的归档误报成孤儿)' -Level DEBUG
}
try {
if ($manifestDirty) {
# 顺手维持"manifest 写了 archive,磁盘上就真有那个文件"这条不变式
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
Write-BaknretManifest -Path $manifestPath -Manifest $manifest | Out-Null
Write-Log 'manifest 已更新(记下本次恢复时间)' -Level DEBUG
$null = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $BackupDir
Write-BakNRetManifest -Path $manifestPath -Manifest $manifest | Out-Null
Write-BakNRetLog 'manifest 已更新(记下本次恢复时间)' -Level DEBUG
}
else {
Write-Log 'manifest 无需更新:本次没有实际恢复任何条目' -Level DEBUG
Write-BakNRetLog 'manifest 无需更新:本次没有实际恢复任何条目' -Level DEBUG
}
}
catch {
Write-Log "manifest 写回失败(不影响本次恢复):$_" -Level WARN
Write-BakNRetLog "manifest 写回失败(不影响本次恢复):$_" -Level WARN
}
if ($failures.Count -gt 0) {
Write-Log '失败条目:' -Level ERROR
foreach ($failure in $failures) { Write-Log " - $failure" -Level ERROR }
Write-BakNRetLog '失败条目:' -Level ERROR
foreach ($failure in $failures) { Write-BakNRetLog " - $failure" -Level ERROR }
}
$summaryText = "恢复完成 - 成功:$($stats.restored),跳过:$($stats.skipped),失败:$($stats.failed)"
if ($securityApplied -gt 0) { $summaryText += ",安全描述符:$securityApplied 个对象" }
if ($VerifyOnly) { $summaryText = "校验完成 - 通过:$($stats.verified),失败:$($stats.failed)" }
if ($stats.planned -gt 0) { $summaryText += ",试运行计划:$($stats.planned)" }
Write-Log $summaryText -Level INFO
Write-BakNRetLog $summaryText -Level INFO
$logPath = Get-BaknretLogPath
if ($logPath) { Write-Log "日志已写入:$logPath" -Level INFO }
Exit-BaknretRunLock -Lock $runLock
Stop-BaknretLog
$logPath = Get-BakNRetLogPath
if ($logPath) { Write-BakNRetLog "日志已写入:$logPath" -Level INFO }
Exit-BakNRetRunLock -Lock $runLock
Stop-BakNRetLog
if ($stats.failed -gt 0) { exit 1 }
exit 0