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

+101 -101
View File
@@ -91,9 +91,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
function Resolve-ConfigPath {
param([string]$Path, [string]$Default)
@@ -107,28 +107,28 @@ function Resolve-ConfigPath {
if (-not $BackupDir) { $BackupDir = Resolve-ConfigPath -Path $null -Default $script:Config.BackupDir }
$logDir = Resolve-ConfigPath -Path $null -Default $script:Config.LogDir
$snapshotDir = Resolve-ConfigPath -Path $null -Default $script:Config.SnapshotDir
$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 'backup'
Write-Log "日志文件:$logPath"
Write-Log "备份目录:$BackupDir"
Write-Log ("软件名录:{0}{1}" -f $catalogPath, $(if (Test-Path -LiteralPath $catalogPath) { '' } else { '(不存在,将只支持字面路径)' }))
$logPath = Start-BakNRetLog -Directory $logDir -Prefix 'backup'
Write-BakNRetLog "日志文件:$logPath"
Write-BakNRetLog "备份目录:$BackupDir"
Write-BakNRetLog ("软件名录:{0}{1}" -f $catalogPath, $(if (Test-Path -LiteralPath $catalogPath) { '' } else { '(不存在,将只支持字面路径)' }))
if (-not (Test-Administrator)) {
Write-Log '建议以管理员身份运行以获取完整的目录访问权限' -Level WARN
if (-not (Test-BakNRetAdministrator)) {
Write-BakNRetLog '建议以管理员身份运行以获取完整的目录访问权限' -Level WARN
}
# 同一份备份目录同一时间只允许一个进程操作(见 BakNRet 模块的「运行锁」一节)。
# -DryRun 不取锁:它一个字节都不写,没必要被正在跑的备份挡在外面。
$runLock = $null
if (-not $DryRun) {
$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
}
# ============================================================================
@@ -137,7 +137,7 @@ if (-not $DryRun) {
if (-not (Test-Path -LiteralPath $BackupDir)) {
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
Write-Log "创建备份目录: $BackupDir" -Level DEBUG
Write-BakNRetLog "创建备份目录: $BackupDir" -Level DEBUG
}
if (-not (Test-Path -LiteralPath $BackupListPath)) {
@@ -147,15 +147,15 @@ if (-not (Test-Path -LiteralPath $BackupListPath)) {
"# %UserProfile%\.ssh :encrypt`n" +
"# 完整语法见 README 与 BackupList.txt 自身的注释。`n"
[System.IO.File]::WriteAllText($BackupListPath, $template, [System.Text.UTF8Encoding]::new($true))
Write-Log '模板 BackupList.txt 已创建,请编辑后重试。' -Level INFO
Stop-BaknretLog
Write-BakNRetLog '模板 BackupList.txt 已创建,请编辑后重试。' -Level INFO
Stop-BakNRetLog
exit 0
}
$tool = Resolve-CompressionTool
$tool = Resolve-BakNRetCompressionTool
if (-not $tool) {
Write-Log '没有找到可用的压缩工具。' -Level ERROR
Stop-BaknretLog
Write-BakNRetLog '没有找到可用的压缩工具。' -Level ERROR
Stop-BakNRetLog
exit 1
}
@@ -164,13 +164,13 @@ $toolVersion = try {
if ($info.ProductVersion) { $info.ProductVersion } elseif ($info.FileVersion) { $info.FileVersion } else { $null }
}
catch { $null }
Write-Log ("压缩工具:{0}{1}" -f $tool.Name, $(if ($toolVersion) { "($toolVersion)" } else { '' }))
Write-BakNRetLog ("压缩工具:{0}{1}" -f $tool.Name, $(if ($toolVersion) { "($toolVersion)" } else { '' }))
$manifest = Read-BaknretManifest -Path $manifestPath
$manifest = Read-BakNRetManifest -Path $manifestPath
$manifest.compressor = [pscustomobject]@{ name = $tool.Name; command = $tool.Command; extension = $tool.Extension; version = $toolVersion }
$passwordFile = if ($KeyFile) { $KeyFile } else { $script:Config.Encryption.PasswordFile }
$password = Get-BaknretPassword -PasswordFile $passwordFile
$password = Get-BakNRetPassword -PasswordFile $passwordFile
$encryptAll = [bool]$script:Config.Encryption.Enabled
$showToolOutput = (-not $QuietTool) -and ($script:Config.ToolOutput -ne 'quiet')
$toolQuietArgument = if ($showToolOutput) { @() } else { @('-bso0', '-bsp0') }
@@ -181,11 +181,11 @@ $processed = 0; $skipped = 0; $failed = 0; $planned = 0
$securityErrorCount = 0 # 有条目"安全描述符里有读不到的对象"
$securityFailed = 0 # 有条目"安全描述符完全没存下来"
$failures = @()
$freeSpaceGB = Get-BaknretFreeSpaceGB -Path $BackupDir
$freeSpaceGB = Get-BakNRetFreeSpaceGB -Path $BackupDir
if ($freeSpaceGB -ge 0) {
Write-Log ("备份目录所在卷剩余空间:{0} GB" -f $freeSpaceGB)
Write-BakNRetLog ("备份目录所在卷剩余空间:{0} GB" -f $freeSpaceGB)
if ($freeSpaceGB -lt $script:Config.MinFreeSpaceGB) {
Write-Log ("剩余空间低于阈值 {0} GB,大条目可能失败" -f $script:Config.MinFreeSpaceGB) -Level WARN
Write-BakNRetLog ("剩余空间低于阈值 {0} GB,大条目可能失败" -f $script:Config.MinFreeSpaceGB) -Level WARN
}
}
@@ -282,7 +282,7 @@ function Save-ItemRecord {
# 压缩 + 校验 + 原子替换;返回 @{ Ok; ExitCode; Warnings; Reason }
#
# 归档内容由调用方决定:它已经用 New-BaknretArchiveStaging 把每个归档项按"归档内的名字"
# 归档内容由调用方决定:它已经用 New-BakNRetArchiveStaging 把每个归档项按"归档内的名字"
# 挂进了 $StagingRoot(目录走 junction、文件走硬链接/复制),所以这里只做三件事:
# 1. 以暂存目录为工作目录调用压缩工具,把项名加进去;
# 2. 用 7z t 校验,并核对多 Slot 条目的每个顶层名字都真的在包里;
@@ -376,11 +376,11 @@ function Invoke-BackupItem {
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
return [pscustomobject]@{ Ok = $false; ExitCode = $verifyCode; Warnings = $false; Reason = "归档校验失败(7z t 退出码 $verifyCode),已丢弃临时文件" }
}
Write-Log '归档校验通过(7z t)' -Level DEBUG
Write-BakNRetLog '归档校验通过(7z t)' -Level DEBUG
# 多个 Slot / 追加项时确认每一个顶层名字都真的进了归档:7z 的"警告"可能只体现在某一部分上
if ($SourceItems.Count -gt 1) {
$listed = @(Get-ArchiveTopLevelNames -ArchivePath $tempPath -SevenZip $tool.Command -Password $(if ($UseEncryption) { $password } else { $null }))
$listed = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $tempPath -SevenZip $tool.Command -Password $(if ($UseEncryption) { $password } else { $null }))
if ($listed.Count -gt 0) {
$expected = @($SourceItems | ForEach-Object { [string]$_.TopName } | Select-Object -Unique)
$absent = @($expected | Where-Object { $_ -notin $listed })
@@ -406,7 +406,7 @@ function Invoke-BackupItem {
}
}
Move-BaknretArchiveIntoPlace -TempPath $tempPath -DestinationPath $FinalPath
Move-BakNRetArchiveIntoPlace -TempPath $tempPath -DestinationPath $FinalPath
return [pscustomobject]@{ Ok = $true; ExitCode = 0; Warnings = $warnings; Reason = $null }
}
catch {
@@ -437,7 +437,7 @@ foreach ($planLine in $lines) {
if (-not $planItem) { continue }
$planDisplayPath = $planItem.Path
$planResolved = Resolve-BackupEntry -Entry $planItem -CatalogPath $catalogPath -MaxDepth $script:Config.CatalogMaxDepth
$planResolved = Resolve-BakNRetBackupEntry -Entry $planItem -CatalogPath $catalogPath -MaxDepth $script:Config.CatalogMaxDepth
if (-not $planResolved.BaseName) { continue }
if (-not (Test-ItemSelected -DisplayPath $planDisplayPath -BaseName $planResolved.BaseName)) { continue }
if ($planResolved.Direction -eq 'restore') { continue }
@@ -450,7 +450,7 @@ foreach ($planLine in $lines) {
$planSourceFiles = 0
$planLatest = $null
foreach ($planSource in $planItems) {
$planSummary = Get-FolderSummary -FolderPath $planSource.RealPath
$planSummary = Get-BakNRetFolderSummary -FolderPath $planSource.RealPath
$planSourceBytes += [int64]$planSummary.TotalSize
$planSourceFiles += [int]$planSummary.FileCount
if ($planSummary.LatestModifiedTime -and (-not $planLatest -or $planSummary.LatestModifiedTime -gt $planLatest)) {
@@ -487,10 +487,10 @@ foreach ($planLine in $lines) {
}
}
$freeNowGB = Get-BaknretFreeSpaceGB -Path $BackupDir
$freeNowGB = Get-BakNRetFreeSpaceGB -Path $BackupDir
if ($spacePlan.Count -eq 0) {
Write-Log '备份前空间预估:本次没有需要重打的条目(源未更新或源不存在),不会写入新归档' -Level INFO
Write-BakNRetLog '备份前空间预估:本次没有需要重打的条目(源未更新或源不存在),不会写入新归档' -Level INFO
}
else {
$spacePeak = [double]0
@@ -504,33 +504,33 @@ else {
$estimateGB = ((($spacePlan | Measure-Object -Property Estimate -Sum).Sum)) / 1GB
$existingGB = ((($spacePlan | Measure-Object -Property Existing -Sum).Sum)) / 1GB
Write-Log '==== 备份前空间预估(只读)====' -Level INFO
Write-Log (" 目标卷可用空间:{0} GB" -f $freeNowGB)
Write-Log (" 本次要重打 {0} 个条目(另有 {1} 个源未更新会跳过、{2} 个源不存在)" -f $spacePlan.Count, $spaceSkipped, $spaceNoSource)
Write-Log (" 新归档合计约 {0} GB;其中会替换掉的旧归档 {1} GB" -f [math]::Round($estimateGB, 2), [math]::Round($existingGB, 2))
Write-BakNRetLog '==== 备份前空间预估(只读)====' -Level INFO
Write-BakNRetLog (" 目标卷可用空间:{0} GB" -f $freeNowGB)
Write-BakNRetLog (" 本次要重打 {0} 个条目(另有 {1} 个源未更新会跳过、{2} 个源不存在)" -f $spacePlan.Count, $spaceSkipped, $spaceNoSource)
Write-BakNRetLog (" 新归档合计约 {0} GB;其中会替换掉的旧归档 {1} GB" -f [math]::Round($estimateGB, 2), [math]::Round($existingGB, 2))
foreach ($plan in ($spacePlan | Sort-Object Estimate -Descending | Select-Object -First 15)) {
Write-Log (" - {0,-22} 源 {1,8:N1} MB / {2,6} 文件 现有 {3,7:N1} MB 预估 {4,7:N1} MB" -f `
Write-BakNRetLog (" - {0,-22} 源 {1,8:N1} MB / {2,6} 文件 现有 {3,7:N1} MB 预估 {4,7:N1} MB" -f `
$plan.Name, ($plan.SourceBytes / 1MB), $plan.Files, ($plan.Existing / 1MB), ($plan.Estimate / 1MB))
}
if ($spacePlan.Count -gt 15) {
Write-Log (" …… 另有 {0} 个条目未逐条列出" -f ($spacePlan.Count - 15))
Write-BakNRetLog (" …… 另有 {0} 个条目未逐条列出" -f ($spacePlan.Count - 15))
}
Write-Log (" 预计峰值新增占用:{0} GB(全程净增量 {1} GB)" -f [math]::Round($peakGB, 2), [math]::Round($netGB, 2))
Write-BakNRetLog (" 预计峰值新增占用:{0} GB(全程净增量 {1} GB)" -f [math]::Round($peakGB, 2), [math]::Round($netGB, 2))
if ($freeNowGB -lt 0) {
Write-Log ' 结论:读不到目标卷可用空间,请自行确认是否够用' -Level WARN
Write-BakNRetLog ' 结论:读不到目标卷可用空间,请自行确认是否够用' -Level WARN
}
elseif ($peakGB -le $freeNowGB) {
Write-Log (" 结论:空间足够(预计用 {0} GB / 可用 {1} GB)" -f [math]::Round($peakGB, 2), $freeNowGB) -Level INFO
Write-BakNRetLog (" 结论:空间足够(预计用 {0} GB / 可用 {1} GB)" -f [math]::Round($peakGB, 2), $freeNowGB) -Level INFO
}
else {
Write-Log (" 结论:空间可能不够!预计需要 {0} GB,可用只有 {1} GB,差 {2} GB" -f `
Write-BakNRetLog (" 结论:空间可能不够!预计需要 {0} GB,可用只有 {1} GB,差 {2} GB" -f `
[math]::Round($peakGB, 2), $freeNowGB, [math]::Round($peakGB - $freeNowGB, 2)) -Level WARN
Write-Log ' 仍会继续执行:真正放不下的条目会被逐条目守卫跳过。建议先腾空间,或用 -Only / -Skip 分批备份。' -Level WARN
Write-BakNRetLog ' 仍会继续执行:真正放不下的条目会被逐条目守卫跳过。建议先腾空间,或用 -Only / -Skip 分批备份。' -Level WARN
}
Write-Log '============================' -Level INFO
Write-BakNRetLog '============================' -Level INFO
}
# ============================================================================
@@ -542,7 +542,7 @@ foreach ($line in $lines) {
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
if (-not $resolved.BaseName) {
$record = New-ItemRecord -BaseName ('raw:' + $displayPath) -Source $displayPath -ResolvedSource $displayPath -Phase 'parse'
@@ -555,7 +555,7 @@ foreach ($line in $lines) {
$sourcePath = [Environment]::ExpandEnvironmentVariables($displayPath)
if (-not (Test-ItemSelected -DisplayPath $displayPath -BaseName $baseName)) {
Write-Log "跳过(未选中): $displayPath" -Level DEBUG
Write-BakNRetLog "跳过(未选中): $displayPath" -Level DEBUG
continue
}
@@ -564,7 +564,7 @@ foreach ($line in $lines) {
# 备份列表里写重了会生成两个同名归档、互相覆盖 —— 直接报错,不猜。
if ($seenBaseNames.ContainsKey($baseName)) {
$reason = "归档名 '$baseName' 与清单中的 '$($seenBaseNames[$baseName])' 重复(由 '$displayPath' 生成),两者会互相覆盖"
Write-Log "失败: $displayPath,$reason" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR
$record = New-ItemRecord -BaseName $baseName -Source $displayPath -ResolvedSource $sourcePath -Phase 'backup'
Save-ItemRecord -Record $record -Action 'failed' -Reason $reason | Out-Null
$failed++; $failures += $displayPath
@@ -573,7 +573,7 @@ foreach ($line in $lines) {
$seenBaseNames[$baseName] = $displayPath
if ($resolved.Direction -eq 'restore') {
Write-Log "跳过(行首 -,仅恢复): $displayPath" -Level INFO
Write-BakNRetLog "跳过(行首 -,仅恢复): $displayPath" -Level INFO
continue
}
@@ -584,12 +584,12 @@ foreach ($line in $lines) {
# root= 是历史标记:包内的一层目录现在由名录里的 Slot 决定,这里只告警不静默忽略。
if (@($item.Flags | Where-Object { $_ -like 'root=*' }).Count -gt 0) {
Write-Log "警告: $displayPath 使用了 root= 标记。归档内的一层目录现在由 Slot 决定,该标记已废弃,本次忽略" -Level WARN
Write-BakNRetLog "警告: $displayPath 使用了 root= 标记。归档内的一层目录现在由 Slot 决定,该标记已废弃,本次忽略" -Level WARN
}
# 归档内路径冲突:明确失败,绝不把两块内容静默搅进同一棵树
if ($resolved.Blocking) {
Write-Log "失败: $displayPath,$($resolved.Blocking)" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$($resolved.Blocking)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $resolved.Blocking | Out-Null
$failed++; $failures += $displayPath
continue
@@ -604,7 +604,7 @@ foreach ($line in $lines) {
else {
$planCatalogExcludes = @($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
}
Write-BackupEntryPlan -Resolved $resolved -DisplayPath $displayPath `
Write-BakNRetBackupEntryPlan -Resolved $resolved -DisplayPath $displayPath `
-ListExcludes $planListExcludes -CatalogExcludes $planCatalogExcludes `
-ConfigExcludes @($script:Config.DefaultExcludes) -Comment $item.Comment
@@ -613,26 +613,26 @@ foreach ($line in $lines) {
# 但 Items 是给出的(恢复端要靠它把内容还原回原位),备份端由下面的存在性检查统一处理。
if ($resolved.Items.Count -eq 0) {
$reason = if ($resolved.Error) { $resolved.Error } else { '解析不出任何源路径' }
Write-Log "跳过: $displayPath,$reason" -Level WARN
Write-BakNRetLog "跳过: $displayPath,$reason" -Level WARN
Save-ItemRecord -Record $record -Action 'missing-source' -Reason $reason | Out-Null
$skipped++
continue
}
# 源存在性检查必须在 Get-FolderSummary / Get-Item 之前:
# 源存在性检查必须在 Get-BakNRetFolderSummary / Get-Item 之前:
# 两者对不存在的路径要么抛异常、要么返回会误导判断的空摘要。
$missingItems = @($resolved.Items | Where-Object { -not (Test-Path -LiteralPath $_.RealPath) })
if ($missingItems.Count -ge $resolved.Items.Count) {
$missingText = @($missingItems | ForEach-Object { $_.RealPath }) -join ';'
Write-Log "跳过: $displayPath,源路径不存在" -Level WARN
Write-BakNRetLog "跳过: $displayPath,源路径不存在" -Level WARN
Save-ItemRecord -Record $record -Action 'missing-source' -Reason ('源路径不存在:' + $missingText) | Out-Null
$skipped++
continue
}
if ($missingItems.Count -gt 0) {
Write-Log ("警告: {0} 有 {1} 个归档项的源路径不存在,本次只打包存在的部分:{2}" -f `
Write-BakNRetLog ("警告: {0} 有 {1} 个归档项的源路径不存在,本次只打包存在的部分:{2}" -f `
$displayPath, $missingItems.Count, (@($missingItems | ForEach-Object { $_.RealPath }) -join ';')) -Level WARN
}
@@ -640,7 +640,7 @@ foreach ($line in $lines) {
$liveItems = @($resolved.Items | Where-Object { Test-Path -LiteralPath $_.RealPath })
# 归档内的顶层条目名 = 真实存在的归档项在包内的第一层名字(Slot 名 / 源目录名)。
# 这里记录可核对的事实,备份成功后还会用 Get-ArchiveTopLevelNames 与归档内容对账。
# 这里记录可核对的事实,备份成功后还会用 Get-BakNRetArchiveTopLevelNames 与归档内容对账。
$record.roots = @($liveItems | ForEach-Object { $_.TopName } | Select-Object -Unique)
# 恢复端要知道每个项在归档里是目录还是文件:记在 manifest 里,
@@ -654,17 +654,17 @@ foreach ($line in $lines) {
$primarySource = $liveItems[0].RealPath
if ([string]::IsNullOrWhiteSpace($primarySource)) {
Write-Log "跳过: $displayPath,无法确定主源路径" -Level WARN
Write-BakNRetLog "跳过: $displayPath,无法确定主源路径" -Level WARN
Save-ItemRecord -Record $record -Action 'invalid-path' -Reason '归档项没有可用路径' | Out-Null
$skipped++
continue
}
$summary = Get-FolderSummary -FolderPath $primarySource
$summary = Get-BakNRetFolderSummary -FolderPath $primarySource
# 从第二个归档项开始累加。刻意不用 `$liveItems[1..($liveItems.Count-1)]`:
# 只有一项时 `1..0` 会退化成 `1,0`,把同一份源数两遍(旧实现的隐蔽 bug)。
for ($index = 1; $index -lt $liveItems.Count; $index++) {
$extra = Get-FolderSummary -FolderPath $liveItems[$index].RealPath
$extra = Get-BakNRetFolderSummary -FolderPath $liveItems[$index].RealPath
$summary.FileCount += $extra.FileCount
$summary.TotalSize += $extra.TotalSize
if ($extra.LatestModifiedTime -and $extra.LatestModifiedTime -gt $summary.LatestModifiedTime) {
@@ -677,9 +677,9 @@ foreach ($line in $lines) {
$archiveExists = Test-Path -LiteralPath $finalPath
$archiveItem = if ($archiveExists) { Get-Item -LiteralPath $finalPath } else { $null }
Write-Log ("开始备份: {0} -> {1}({2} 个文件,{3} MB)" -f $displayPath, $record.archive, $summary.FileCount, [math]::Round(($summary.TotalSize / 1MB), 2))
Write-BakNRetLog ("开始备份: {0} -> {1}({2} 个文件,{3} MB)" -f $displayPath, $record.archive, $summary.FileCount, [math]::Round(($summary.TotalSize / 1MB), 2))
# 空目录时 Get-FolderSummary 拿不到任何条目,回退到源自身的修改时间
# 空目录时 Get-BakNRetFolderSummary 拿不到任何条目,回退到源自身的修改时间
# (源路径上面已经确认存在,这里的 Get-Item 不会再抛异常)
$sourceLatest = $summary.LatestModifiedTime
if (-not $sourceLatest) {
@@ -687,7 +687,7 @@ foreach ($line in $lines) {
}
if (-not $Force -and $archiveItem -and $sourceLatest -and $sourceLatest -le $archiveItem.LastWriteTime) {
Write-Log "跳过: $displayPath,源目录未更新" -Level INFO
Write-BakNRetLog "跳过: $displayPath,源目录未更新" -Level INFO
$record.archiveBytes = $archiveItem.Length
Save-ItemRecord -Record $record -Action 'skip-unchanged' -Reason ('源最新修改时间 {0} 不晚于归档时间 {1}' -f $sourceLatest, $archiveItem.LastWriteTime) | Out-Null
$skipped++
@@ -700,17 +700,17 @@ foreach ($line in $lines) {
$archiveGB = $archiveItem.Length / 1GB
$estimatedGB = [math]::Min($estimatedGB, $archiveGB * 1.3)
}
$freeSpaceGB = Get-BaknretFreeSpaceGB -Path $BackupDir
$freeSpaceGB = Get-BakNRetFreeSpaceGB -Path $BackupDir
if ($freeSpaceGB -ge 0 -and $estimatedGB -gt 0 -and $freeSpaceGB -lt $estimatedGB) {
$reason = ('剩余空间 {0} GB 不足以写入预估 {1} GB 的新归档' -f $freeSpaceGB, [math]::Round($estimatedGB, 2))
Write-Log "失败: $displayPath,$reason" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$reason" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $reason | Out-Null
$failed++; $failures += $displayPath
continue
}
if ($DryRun) {
Write-Log ("[试运行] 将打包 {0} -> {1}" -f $sourcePath, $finalPath) -Level INFO
Write-BakNRetLog ("[试运行] 将打包 {0} -> {1}" -f $sourcePath, $finalPath) -Level INFO
$record.reason = '试运行,未执行压缩'
Save-ItemRecord -Record $record -Action 'planned' -Reason '试运行,未执行压缩' | Out-Null
$planned++
@@ -733,19 +733,19 @@ foreach ($line in $lines) {
@($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
}
$allPatterns = @($script:Config.DefaultExcludes) + $patternSource
$scopeMap = Split-BaknretPatternScope -Items $liveItems -Patterns $allPatterns
$scopeMap = Split-BakNRetPatternScope -Items $liveItems -Patterns $allPatterns
$excludeLists = @()
$excludeError = $null
for ($index = 0; $index -lt $liveItems.Count; $index++) {
$expanded = Get-BaknretExcludeArgument -Item $liveItems[$index] -Patterns @($scopeMap[$index])
$expanded = Get-BakNRetExcludeArgument -Item $liveItems[$index] -Patterns @($scopeMap[$index])
if ($expanded.Error) { $excludeError = $expanded.Error }
$excludeLists += , @($expanded.Arguments)
}
$effectiveExcludes = @(Merge-BaknretExcludeArgument -ArgumentLists $excludeLists)
$effectiveExcludes = @(Merge-BakNRetExcludeArgument -ArgumentLists $excludeLists)
if ($excludeError) {
Write-Log "失败: $displayPath,$excludeError" -Level ERROR
Write-BakNRetLog "失败: $displayPath,$excludeError" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $excludeError | Out-Null
$failed++; $failures += $displayPath
continue
@@ -765,7 +765,7 @@ foreach ($line in $lines) {
# 挂进暂存目录(junction / 硬链接),再让压缩工具以暂存目录为工作目录打包。
$stagingRoot = $null
try {
$stagingRoot = New-BaknretArchiveStaging -Items $liveItems
$stagingRoot = New-BakNRetArchiveStaging -Items $liveItems
$result = Invoke-BackupItem -SourceItems $liveItems -StagingRoot $stagingRoot `
-FinalPath $finalPath -ExcludePatterns $effectiveExcludes -UseEncryption:$useEncryption `
-ProtectPrevious:$protectPrevious -AcceptWarnings:$AcceptWarnings
@@ -774,7 +774,7 @@ foreach ($line in $lines) {
$result = [pscustomobject]@{ Ok = $false; ExitCode = $null; Warnings = $false; Reason = "准备归档内容失败:$_" }
}
finally {
Remove-BaknretArchiveStaging -Root $stagingRoot
Remove-BakNRetArchiveStaging -Root $stagingRoot
}
$record.exitCode = $result.ExitCode
@@ -783,7 +783,7 @@ foreach ($line in $lines) {
$record.durationSec = [math]::Round(((Get-Date) - $startedAt).TotalSeconds, 1)
if (-not $result.Ok) {
Write-Log "备份失败: $displayPath,$($result.Reason)" -Level ERROR
Write-BakNRetLog "备份失败: $displayPath,$($result.Reason)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason $result.Reason | Out-Null
$failed++; $failures += $displayPath
continue
@@ -792,11 +792,11 @@ foreach ($line in $lines) {
$written = Get-Item -LiteralPath $finalPath
$record.archiveBytes = $written.Length
if ($result.Warnings) {
Write-Log "备份成功(压缩工具报告了警告,可能有文件被占用而没打进归档): $displayPath" -Level WARN
Write-Log ' 该归档在 manifest 里标记为 warnings=true;如果以后现有归档是完整的,会拒绝被它覆盖' -Level WARN
Write-BakNRetLog "备份成功(压缩工具报告了警告,可能有文件被占用而没打进归档): $displayPath" -Level WARN
Write-BakNRetLog ' 该归档在 manifest 里标记为 warnings=true;如果以后现有归档是完整的,会拒绝被它覆盖' -Level WARN
}
else {
Write-Log "备份成功: $baseName" -Level INFO
Write-BakNRetLog "备份成功: $baseName" -Level INFO
}
# ------------------------------------------------------------------
@@ -813,9 +813,9 @@ foreach ($line in $lines) {
$sidecarName = "$baseName.acl.json"
$sidecarPath = Join-Path $BackupDir $sidecarName
try {
$capture = Get-BaknretSecurityRecords -Items $liveItems -ScopeMap $scopeMap -Mode $securityMode `
$capture = Get-BakNRetSecurityRecords -Items $liveItems -ScopeMap $scopeMap -Mode $securityMode `
-IncludeSacl:([bool]$script:Config.Security.IncludeSacl)
Save-BaknretSecuritySidecar -Path $sidecarPath -Records $capture.Records -Mode $securityMode `
Save-BakNRetSecuritySidecar -Path $sidecarPath -Records $capture.Records -Mode $securityMode `
-IncludeSacl:([bool]$script:Config.Security.IncludeSacl) `
-Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
@@ -827,25 +827,25 @@ foreach ($line in $lines) {
errors = $capture.Errors
capturedAt = (Get-Date).ToString('o')
}
Write-Log ("安全描述符:{0} 个对象写进 {1}(扫描 {2} 个,读不到 {3} 个)" -f `
Write-BakNRetLog ("安全描述符:{0} 个对象写进 {1}(扫描 {2} 个,读不到 {3} 个)" -f `
$capture.Kept, $sidecarName, $capture.Scanned, $capture.Errors) -Level INFO
if ($capture.Errors -gt 0) {
$securityErrorCount++
$unreadable = @($capture.Records | Where-Object { $_.e } | Select-Object -First 3 -ExpandProperty p)
Write-Log (" {0} 个对象的安全描述符读不到(恢复后它们的属主/ACL 会是新建对象的默认值),例如:{1}" -f `
Write-BakNRetLog (" {0} 个对象的安全描述符读不到(恢复后它们的属主/ACL 会是新建对象的默认值),例如:{1}" -f `
$capture.Errors, ($unreadable -join '、')) -Level WARN
}
}
catch {
$securityFailed++
Write-Log "安全描述符采集/写盘失败:$displayPath —— $_" -Level WARN
Write-BakNRetLog "安全描述符采集/写盘失败:$displayPath —— $_" -Level WARN
$record.security = [ordered]@{ file = $sidecarName; error = "$_" }
if ([bool]$script:Config.Security.FailOnError) { $securityFatal = $true }
}
if ($securityFatal) {
Write-Log "失败: $displayPath,归档已替换,但安全描述符没能存下来(Security.FailOnError = \$true)" -Level ERROR
Write-BakNRetLog "失败: $displayPath,归档已替换,但安全描述符没能存下来(Security.FailOnError = \$true)" -Level ERROR
Save-ItemRecord -Record $record -Action 'failed' -Reason '归档已替换,但安全描述符没能存下来' | Out-Null
$failed++; $failures += $displayPath
continue
@@ -854,7 +854,7 @@ foreach ($line in $lines) {
if ($Hash -or $script:Config.ComputeHash) {
$record.sha256 = (Get-FileHash -LiteralPath $finalPath -Algorithm SHA256).Hash
Write-Log "SHA256: $($record.sha256)" -Level DEBUG
Write-BakNRetLog "SHA256: $($record.sha256)" -Level DEBUG
}
if ($Snapshot -or $script:Config.Snapshot.Enabled) {
@@ -863,7 +863,7 @@ foreach ($line in $lines) {
$targetDir = Split-Path -Parent $target
if (-not (Test-Path -LiteralPath $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
Copy-Item -LiteralPath $finalPath -Destination $target -Force
Write-Log "已留存快照: $target" -Level INFO
Write-BakNRetLog "已留存快照: $target" -Level INFO
}
Save-ItemRecord -Record $record -Action 'backed-up' -Reason $null -ArchiveWarnings $result.Warnings | Out-Null
@@ -875,17 +875,17 @@ foreach ($line in $lines) {
# ============================================================================
if ($DryRun) {
Write-Log '试运行:manifest 与归档都不会被写入' -Level INFO
Write-BakNRetLog '试运行:manifest 与归档都不会被写入' -Level INFO
}
else {
# manifest 里写了 archive 的记录,磁盘上就必须真有那个文件
$clearedArchiveFields = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
$clearedArchiveFields = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $BackupDir
if ($clearedArchiveFields.Count -gt 0) {
Write-Log ("已清空 {0} 条记录里指向不存在归档的 archive 字段:{1}" -f $clearedArchiveFields.Count, ($clearedArchiveFields -join '、')) -Level WARN
Write-BakNRetLog ("已清空 {0} 条记录里指向不存在归档的 archive 字段:{1}" -f $clearedArchiveFields.Count, ($clearedArchiveFields -join '、')) -Level WARN
}
Write-BaknretManifest -Path $manifestPath -Manifest $manifest | Out-Null
Write-Log "manifest 已更新:$manifestPath" -Level DEBUG
Write-BakNRetManifest -Path $manifestPath -Manifest $manifest | Out-Null
Write-BakNRetLog "manifest 已更新:$manifestPath" -Level DEBUG
}
# 孤儿归档审计:磁盘上有、但**当前清单里任何条目都不指向**的归档。
@@ -906,37 +906,37 @@ if (-not $DryRun -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') -and -not $known.ContainsKey($_.BaseName) })
if ($orphanArchives.Count -gt 0) {
Write-Log ("发现 {0} 个孤儿归档(当前清单里没有任何条目指向,恢复不到,注意别误删):" -f $orphanArchives.Count) -Level WARN
Write-BakNRetLog ("发现 {0} 个孤儿归档(当前清单里没有任何条目指向,恢复不到,注意别误删):" -f $orphanArchives.Count) -Level WARN
foreach ($orphan in $orphanArchives) {
$inManifest = $manifest.items.Contains($orphan.BaseName)
Write-Log (" - {0}({1:N1} MB,{2}){3}" -f $orphan.Name, ($orphan.Length / 1MB), $orphan.LastWriteTime, $(if ($inManifest) { ';manifest 里还留着它的历史记录,但清单里已经没有了' } else { '' })) -Level WARN
Write-BakNRetLog (" - {0}({1:N1} MB,{2}){3}" -f $orphan.Name, ($orphan.Length / 1MB), $orphan.LastWriteTime, $(if ($inManifest) { ';manifest 里还留着它的历史记录,但清单里已经没有了' } else { '' })) -Level WARN
}
}
else {
Write-Log '孤儿归档审计:没有发现(所有归档都有清单条目指向)' -Level DEBUG
Write-BakNRetLog '孤儿归档审计:没有发现(所有归档都有清单条目指向)' -Level DEBUG
}
}
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 }
}
if ($securityFailed -gt 0) {
Write-Log ("有 {0} 个条目的安全描述符完全没能存下来(manifest 的 security.error 里有原文)" -f $securityFailed) -Level WARN
Write-BakNRetLog ("有 {0} 个条目的安全描述符完全没能存下来(manifest 的 security.error 里有原文)" -f $securityFailed) -Level WARN
}
if ($securityErrorCount -gt 0) {
Write-Log ("有 {0} 个条目存在'读不到安全描述符'的对象;恢复后这些对象的属主/ACL 是新建对象的默认值,可查 manifest 的 security.errors" -f $securityErrorCount) -Level WARN
Write-BakNRetLog ("有 {0} 个条目存在'读不到安全描述符'的对象;恢复后这些对象的属主/ACL 是新建对象的默认值,可查 manifest 的 security.errors" -f $securityErrorCount) -Level WARN
}
$summaryText = "备份完成。成功: $processed, 跳过: $skipped, 失败: $failed"
if ($DryRun) { $summaryText += ", 试运行计划: $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 ($failed -gt 0) { exit 1 }
exit 0
+58 -58
View File
@@ -19,68 +19,68 @@
CompatiblePSEditions = @('Desktop', 'Core')
FunctionsToExport = @(
'Enter-BaknretRunLock',
'Exit-BaknretRunLock',
'Get-BaknretRunLockPath',
'Set-BaknretDebug',
'Start-BaknretLog',
'Stop-BaknretLog',
'Get-BaknretLogPath',
'Write-Log',
'Test-Administrator',
'Get-BaknretFreeSpaceGB',
'ConvertTo-NativeArgumentString',
'Enter-BakNRetRunLock',
'Exit-BakNRetRunLock',
'Get-BakNRetRunLockPath',
'Set-BakNRetDebug',
'Start-BakNRetLog',
'Stop-BakNRetLog',
'Get-BakNRetLogPath',
'Write-BakNRetLog',
'Test-BakNRetAdministrator',
'Get-BakNRetFreeSpaceGB',
'ConvertTo-BakNRetNativeArgumentString',
'Invoke-ExternalCommand',
'Resolve-CompressionTool',
'Resolve-BakNRetCompressionTool',
'Get-Optimized7zArgument',
'Split-BaknretToken',
'Remove-BaknretQuote',
'Test-BaknretMarker',
'ConvertFrom-BaknretPatternList',
'Split-BakNRetToken',
'Remove-BakNRetQuote',
'Test-BakNRetMarker',
'ConvertFrom-BakNRetPatternList',
'ConvertFrom-BackupListLine',
'Test-LiteralPath',
'Get-BaknretRegexExclude',
'Get-BaknretExcludeArgument',
'Split-BaknretPatternScope',
'Merge-BaknretExcludeArgument',
'Resolve-CatalogPath',
'Get-SoftwareCatalog',
'Find-ChildDirectoryByName',
'Format-CatalogName',
'Test-BakNRetLiteralPath',
'Get-BakNRetRegexExclude',
'Get-BakNRetExcludeArgument',
'Split-BakNRetPatternScope',
'Merge-BakNRetExcludeArgument',
'Resolve-BakNRetCatalogPath',
'Get-BakNRetSoftwareCatalog',
'Find-BakNRetChildDirectoryByName',
'Format-BakNRetCatalogName',
'Expand-CatalogPathText',
'Get-ArchiveTopLevelNames',
'Get-BaknretArchiveTopName',
'New-BaknretArchiveItem',
'New-BaknretJunction',
'Remove-BaknretJunction',
'New-BaknretArchiveStaging',
'Remove-BaknretArchiveStaging',
'Get-ItemArchiveName',
'Resolve-BackupEntry',
'Write-BackupEntryPlan',
'Get-BackupBaseName',
'Convert-BackupFileNameToPath',
'Get-FolderSummary',
'Read-BaknretManifest',
'Write-BaknretManifest',
'Sync-BaknretManifestArchive',
'Move-BaknretArchiveIntoPlace',
'Enable-BaknretPrivilege',
'ConvertTo-BaknretWildcardPattern',
'Test-BaknretPathExcluded',
'Get-BaknretAceSignatureList',
'Get-BaknretSecuritySddlWithStale',
'Get-BaknretSecurityRecord',
'Test-BaknretSecurityRecordNeeded',
'Get-BaknretSecurityRecords',
'Write-BaknretAtomicText',
'Save-BaknretSecuritySidecar',
'Read-BaknretSecuritySidecar',
'Convert-BaknretSidMap',
'Set-BaknretObjectSecurity',
'Restore-BaknretSecurity',
'Get-BaknretConfig',
'Get-BaknretPassword'
'Get-BakNRetArchiveTopLevelNames',
'Get-BakNRetArchiveTopName',
'New-BakNRetArchiveItem',
'New-BakNRetJunction',
'Remove-BakNRetJunction',
'New-BakNRetArchiveStaging',
'Remove-BakNRetArchiveStaging',
'Get-BakNRetItemArchiveName',
'Resolve-BakNRetBackupEntry',
'Write-BakNRetBackupEntryPlan',
'Get-BakNRetBackupBaseName',
'Convert-BakNRetBackupFileNameToPath',
'Get-BakNRetFolderSummary',
'Read-BakNRetManifest',
'Write-BakNRetManifest',
'Sync-BakNRetManifestArchive',
'Move-BakNRetArchiveIntoPlace',
'Enable-BakNRetPrivilege',
'ConvertTo-BakNRetWildcardPattern',
'Test-BakNRetPathExcluded',
'Get-BakNRetAceSignatureList',
'Get-BakNRetSecuritySddlWithStale',
'Get-BakNRetSecurityRecord',
'Test-BakNRetSecurityRecordNeeded',
'Get-BakNRetSecurityRecords',
'Write-BakNRetAtomicText',
'Save-BakNRetSecuritySidecar',
'Read-BakNRetSecuritySidecar',
'Convert-BakNRetSidMap',
'Set-BakNRetObjectSecurity',
'Restore-BakNRetSecurity',
'Get-BakNRetConfig',
'Get-BakNRetPassword'
)
CmdletsToExport = @()
VariablesToExport = @()
+120 -120
View File
@@ -46,155 +46,155 @@
. (Join-Path $PSScriptRoot 'Private\State.ps1')
# ---- 日志 ----
. (Join-Path $PSScriptRoot 'Public\Set-BaknretDebug.ps1')
. (Join-Path $PSScriptRoot 'Public\Start-BaknretLog.ps1')
. (Join-Path $PSScriptRoot 'Public\Stop-BaknretLog.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretLogPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-Log.ps1')
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetDebug.ps1')
. (Join-Path $PSScriptRoot 'Public\Start-BakNRetLog.ps1')
. (Join-Path $PSScriptRoot 'Public\Stop-BakNRetLog.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetLogPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BakNRetLog.ps1')
# ---- 运行锁 ----
. (Join-Path $PSScriptRoot 'Public\Get-BaknretRunLockPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Enter-BaknretRunLock.ps1')
. (Join-Path $PSScriptRoot 'Public\Exit-BaknretRunLock.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetRunLockPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Enter-BakNRetRunLock.ps1')
. (Join-Path $PSScriptRoot 'Public\Exit-BakNRetRunLock.ps1')
# ---- 环境 ----
. (Join-Path $PSScriptRoot 'Public\Test-Administrator.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretFreeSpaceGB.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BakNRetAdministrator.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetFreeSpaceGB.ps1')
# ---- 外部命令 ----
. (Join-Path $PSScriptRoot 'Public\ConvertTo-NativeArgumentString.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertTo-BakNRetNativeArgumentString.ps1')
. (Join-Path $PSScriptRoot 'Public\Invoke-ExternalCommand.ps1')
. (Join-Path $PSScriptRoot 'Public\Resolve-CompressionTool.ps1')
. (Join-Path $PSScriptRoot 'Public\Resolve-BakNRetCompressionTool.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-Optimized7zArgument.ps1')
# ---- BackupList.txt 解析 ----
. (Join-Path $PSScriptRoot 'Public\Split-BaknretToken.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BaknretQuote.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BaknretMarker.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertFrom-BaknretPatternList.ps1')
. (Join-Path $PSScriptRoot 'Public\Split-BakNRetToken.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BakNRetQuote.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BakNRetMarker.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertFrom-BakNRetPatternList.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertFrom-BackupListLine.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-LiteralPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretRegexExclude.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretExcludeArgument.ps1')
. (Join-Path $PSScriptRoot 'Public\Split-BaknretPatternScope.ps1')
. (Join-Path $PSScriptRoot 'Public\Merge-BaknretExcludeArgument.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BakNRetLiteralPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetRegexExclude.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetExcludeArgument.ps1')
. (Join-Path $PSScriptRoot 'Public\Split-BakNRetPatternScope.ps1')
. (Join-Path $PSScriptRoot 'Public\Merge-BakNRetExcludeArgument.ps1')
# ---- 软件名录(SoftwareCatalog.psd1) ----
. (Join-Path $PSScriptRoot 'Public\Resolve-CatalogPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Format-CatalogName.ps1')
. (Join-Path $PSScriptRoot 'Private\Test-BaknretMapKey.ps1')
. (Join-Path $PSScriptRoot 'Private\Get-BaknretMapValue.ps1')
. (Join-Path $PSScriptRoot 'Private\Get-BaknretMapKeys.ps1')
. (Join-Path $PSScriptRoot 'Public\Resolve-BakNRetCatalogPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Format-BakNRetCatalogName.ps1')
. (Join-Path $PSScriptRoot 'Private\Test-BakNRetMapKey.ps1')
. (Join-Path $PSScriptRoot 'Private\Get-BakNRetMapValue.ps1')
. (Join-Path $PSScriptRoot 'Private\Get-BakNRetMapKeys.ps1')
. (Join-Path $PSScriptRoot 'Public\Expand-CatalogPathText.ps1')
. (Join-Path $PSScriptRoot 'Private\Import-BaknretDataFile.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-SoftwareCatalog.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-ArchiveTopLevelNames.ps1')
. (Join-Path $PSScriptRoot 'Public\Find-ChildDirectoryByName.ps1')
. (Join-Path $PSScriptRoot 'Private\Import-BakNRetDataFile.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetSoftwareCatalog.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetArchiveTopLevelNames.ps1')
. (Join-Path $PSScriptRoot 'Public\Find-BakNRetChildDirectoryByName.ps1')
# ---- 归档命名与路径还原 ----
. (Join-Path $PSScriptRoot 'Public\Get-ItemArchiveName.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretArchiveTopName.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BaknretArchiveItem.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BaknretJunction.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BaknretJunction.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BaknretArchiveStaging.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BaknretArchiveStaging.ps1')
. (Join-Path $PSScriptRoot 'Public\Resolve-BackupEntry.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BackupEntryPlan.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BackupBaseName.ps1')
. (Join-Path $PSScriptRoot 'Public\Convert-BackupFileNameToPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-FolderSummary.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetItemArchiveName.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetArchiveTopName.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BakNRetArchiveItem.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BakNRetJunction.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BakNRetJunction.ps1')
. (Join-Path $PSScriptRoot 'Public\New-BakNRetArchiveStaging.ps1')
. (Join-Path $PSScriptRoot 'Public\Remove-BakNRetArchiveStaging.ps1')
. (Join-Path $PSScriptRoot 'Public\Resolve-BakNRetBackupEntry.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BakNRetBackupEntryPlan.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetBackupBaseName.ps1')
. (Join-Path $PSScriptRoot 'Public\Convert-BakNRetBackupFileNameToPath.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetFolderSummary.ps1')
# ---- manifest.json ----
. (Join-Path $PSScriptRoot 'Public\Read-BaknretManifest.ps1')
. (Join-Path $PSScriptRoot 'Public\Sync-BaknretManifestArchive.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BaknretManifest.ps1')
. (Join-Path $PSScriptRoot 'Public\Read-BakNRetManifest.ps1')
. (Join-Path $PSScriptRoot 'Public\Sync-BakNRetManifestArchive.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BakNRetManifest.ps1')
# ---- 归档原子替换 ----
. (Join-Path $PSScriptRoot 'Public\Move-BaknretArchiveIntoPlace.ps1')
. (Join-Path $PSScriptRoot 'Public\Move-BakNRetArchiveIntoPlace.ps1')
# ---- 安全描述符(NTFS 属主 / ACL) ----
. (Join-Path $PSScriptRoot 'Public\Enable-BaknretPrivilege.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertTo-BaknretWildcardPattern.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BaknretPathExcluded.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretAceSignatureList.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretSecuritySddlWithStale.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretSecurityRecord.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BaknretSecurityRecordNeeded.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretSecurityRecords.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BaknretAtomicText.ps1')
. (Join-Path $PSScriptRoot 'Public\Save-BaknretSecuritySidecar.ps1')
. (Join-Path $PSScriptRoot 'Public\Read-BaknretSecuritySidecar.ps1')
. (Join-Path $PSScriptRoot 'Public\Convert-BaknretSidMap.ps1')
. (Join-Path $PSScriptRoot 'Public\Set-BaknretObjectSecurity.ps1')
. (Join-Path $PSScriptRoot 'Public\Restore-BaknretSecurity.ps1')
. (Join-Path $PSScriptRoot 'Public\Enable-BakNRetPrivilege.ps1')
. (Join-Path $PSScriptRoot 'Public\ConvertTo-BakNRetWildcardPattern.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BakNRetPathExcluded.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetAceSignatureList.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetSecuritySddlWithStale.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetSecurityRecord.ps1')
. (Join-Path $PSScriptRoot 'Public\Test-BakNRetSecurityRecordNeeded.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetSecurityRecords.ps1')
. (Join-Path $PSScriptRoot 'Public\Write-BakNRetAtomicText.ps1')
. (Join-Path $PSScriptRoot 'Public\Save-BakNRetSecuritySidecar.ps1')
. (Join-Path $PSScriptRoot 'Public\Read-BakNRetSecuritySidecar.ps1')
. (Join-Path $PSScriptRoot 'Public\Convert-BakNRetSidMap.ps1')
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetObjectSecurity.ps1')
. (Join-Path $PSScriptRoot 'Public\Restore-BakNRetSecurity.ps1')
# ---- 配置 ----
. (Join-Path $PSScriptRoot 'Public\Get-BaknretConfig.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BaknretPassword.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetConfig.ps1')
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetPassword.ps1')
Export-ModuleMember -Function @(
'Enter-BaknretRunLock',
'Exit-BaknretRunLock',
'Get-BaknretRunLockPath',
'Set-BaknretDebug',
'Start-BaknretLog',
'Stop-BaknretLog',
'Get-BaknretLogPath',
'Write-Log',
'Test-Administrator',
'Get-BaknretFreeSpaceGB',
'ConvertTo-NativeArgumentString',
'Enter-BakNRetRunLock',
'Exit-BakNRetRunLock',
'Get-BakNRetRunLockPath',
'Set-BakNRetDebug',
'Start-BakNRetLog',
'Stop-BakNRetLog',
'Get-BakNRetLogPath',
'Write-BakNRetLog',
'Test-BakNRetAdministrator',
'Get-BakNRetFreeSpaceGB',
'ConvertTo-BakNRetNativeArgumentString',
'Invoke-ExternalCommand',
'Resolve-CompressionTool',
'Resolve-BakNRetCompressionTool',
'Get-Optimized7zArgument',
'Split-BaknretToken',
'Remove-BaknretQuote',
'Test-BaknretMarker',
'ConvertFrom-BaknretPatternList',
'Split-BakNRetToken',
'Remove-BakNRetQuote',
'Test-BakNRetMarker',
'ConvertFrom-BakNRetPatternList',
'ConvertFrom-BackupListLine',
'Test-LiteralPath',
'Get-BaknretRegexExclude',
'Get-BaknretExcludeArgument',
'Split-BaknretPatternScope',
'Merge-BaknretExcludeArgument',
'Resolve-CatalogPath',
'Get-SoftwareCatalog',
'Find-ChildDirectoryByName',
'Format-CatalogName',
'Test-BakNRetLiteralPath',
'Get-BakNRetRegexExclude',
'Get-BakNRetExcludeArgument',
'Split-BakNRetPatternScope',
'Merge-BakNRetExcludeArgument',
'Resolve-BakNRetCatalogPath',
'Get-BakNRetSoftwareCatalog',
'Find-BakNRetChildDirectoryByName',
'Format-BakNRetCatalogName',
'Expand-CatalogPathText',
'Get-ArchiveTopLevelNames',
'Get-BaknretArchiveTopName',
'New-BaknretArchiveItem',
'New-BaknretJunction',
'Remove-BaknretJunction',
'New-BaknretArchiveStaging',
'Remove-BaknretArchiveStaging',
'Get-ItemArchiveName',
'Resolve-BackupEntry',
'Write-BackupEntryPlan',
'Get-BackupBaseName',
'Convert-BackupFileNameToPath',
'Get-FolderSummary',
'Read-BaknretManifest',
'Write-BaknretManifest',
'Sync-BaknretManifestArchive',
'Move-BaknretArchiveIntoPlace',
'Enable-BaknretPrivilege',
'ConvertTo-BaknretWildcardPattern',
'Test-BaknretPathExcluded',
'Get-BaknretAceSignatureList',
'Get-BaknretSecuritySddlWithStale',
'Get-BaknretSecurityRecord',
'Test-BaknretSecurityRecordNeeded',
'Get-BaknretSecurityRecords',
'Write-BaknretAtomicText',
'Save-BaknretSecuritySidecar',
'Read-BaknretSecuritySidecar',
'Convert-BaknretSidMap',
'Set-BaknretObjectSecurity',
'Restore-BaknretSecurity',
'Get-BaknretConfig',
'Get-BaknretPassword'
'Get-BakNRetArchiveTopLevelNames',
'Get-BakNRetArchiveTopName',
'New-BakNRetArchiveItem',
'New-BakNRetJunction',
'Remove-BakNRetJunction',
'New-BakNRetArchiveStaging',
'Remove-BakNRetArchiveStaging',
'Get-BakNRetItemArchiveName',
'Resolve-BakNRetBackupEntry',
'Write-BakNRetBackupEntryPlan',
'Get-BakNRetBackupBaseName',
'Convert-BakNRetBackupFileNameToPath',
'Get-BakNRetFolderSummary',
'Read-BakNRetManifest',
'Write-BakNRetManifest',
'Sync-BakNRetManifestArchive',
'Move-BakNRetArchiveIntoPlace',
'Enable-BakNRetPrivilege',
'ConvertTo-BakNRetWildcardPattern',
'Test-BakNRetPathExcluded',
'Get-BakNRetAceSignatureList',
'Get-BakNRetSecuritySddlWithStale',
'Get-BakNRetSecurityRecord',
'Test-BakNRetSecurityRecordNeeded',
'Get-BakNRetSecurityRecords',
'Write-BakNRetAtomicText',
'Save-BakNRetSecuritySidecar',
'Read-BakNRetSecuritySidecar',
'Convert-BakNRetSidMap',
'Set-BakNRetObjectSecurity',
'Restore-BakNRetSecurity',
'Get-BakNRetConfig',
'Get-BakNRetPassword'
)
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretMapKeys {
function Get-BakNRetMapKeys {
<# .SYNOPSIS 列出哈希表或 JSON 对象的全部键。 #>
param($Map)
if ($null -eq $Map) { return @() }
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretMapValue {
function Get-BakNRetMapValue {
<# .SYNOPSIS 从哈希表或 JSON 对象里按键取值。 #>
param($Map, [string]$Key)
if ($null -eq $Map) { return $null }
+2 -2
View File
@@ -1,4 +1,4 @@
function Import-BaknretDataFile {
function Import-BakNRetDataFile {
<#
.SYNOPSIS
读取 .psd1 / .json 配置数据。
@@ -23,7 +23,7 @@
}
catch {
$firstLine = ([string]$_.Exception.Message) -split "`r?`n" | Select-Object -First 1
Write-Log "psd1 里有 Import-PowerShellDataFile 不接受的表达式($firstLine),改用 PowerShell 求值:$Path" -Level DEBUG
Write-BakNRetLog "psd1 里有 Import-PowerShellDataFile 不接受的表达式($firstLine),改用 PowerShell 求值:$Path" -Level DEBUG
$raw = [System.IO.File]::ReadAllText($Path)
return [scriptblock]::Create($raw).Invoke()
}
+1 -1
View File
@@ -18,5 +18,5 @@ $script:CatalogCache = @{}
$script:CatalogExpressionCache = @{}
$script:BaknretPrivilegeState = @{}
$script:BakNRetPrivilegeState = @{}
+1 -1
View File
@@ -1,4 +1,4 @@
function Test-BaknretMapKey {
function Test-BakNRetMapKey {
<# .SYNOPSIS 判断一个数据对象(哈希表或 JSON 对象)里有没有某个键。 #>
param($Map, [string]$Key)
if ($null -eq $Map) { return $false }
@@ -1,4 +1,4 @@
function Convert-BackupFileNameToPath {
function Convert-BakNRetBackupFileNameToPath {
<#
.SYNOPSIS
把归档文件名还原成原始路径(用于没有 manifest 时的兜底)。
@@ -22,11 +22,11 @@
})
$reconstructed = ($parts -join '\') + '\' + $folderPart
Write-Log "逆向解析:$FileName -> $reconstructed" -Level DEBUG
Write-BakNRetLog "逆向解析:$FileName -> $reconstructed" -Level DEBUG
return $reconstructed
}
catch {
Write-Log "无法解析备份文件名:$FileName" -Level WARN
Write-BakNRetLog "无法解析备份文件名:$FileName" -Level WARN
return $null
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
function Convert-BaknretSidMap {
function Convert-BakNRetSidMap {
<#
.SYNOPSIS
按 SID 映射表改写 SDDL 里的 SID(跨机恢复用)。
+22 -22
View File
@@ -42,7 +42,7 @@
if ([string]::IsNullOrEmpty($content)) { return $null }
}
$tokens = @(Split-BaknretToken -Text $content)
$tokens = @(Split-BakNRetToken -Text $content)
if ($tokens.Count -eq 0) { return $null }
# 整行被一对引号包住是**历史写法**(`"C:\a b\CodeSpace :: X\"`)。
@@ -56,9 +56,9 @@
$last = $raw[$raw.Length - 1]
if ($first -eq $last -and ($first -eq '"' -or $first -eq "'")) {
$inner = $raw.Substring(1, $raw.Length - 2)
foreach ($innerToken in @(Split-BaknretToken -Text $inner)) {
if (Test-BaknretMarker -Token $innerToken) {
Write-Log "整行被引号包住,引号里的修饰符不会被识别(历史写法)。请去掉外层引号,并注意现在 `:-` 才是排除、`::` 是覆盖 Path:$Line" -Level WARN
foreach ($innerToken in @(Split-BakNRetToken -Text $inner)) {
if (Test-BakNRetMarker -Token $innerToken) {
Write-BakNRetLog "整行被引号包住,引号里的修饰符不会被识别(历史写法)。请去掉外层引号,并注意现在 `:-` 才是排除、`::` 是覆盖 Path:$Line" -Level WARN
break
}
}
@@ -98,11 +98,11 @@
# 所以这里取"第一个修饰符记号之前的全部记号",而不是只取第一个记号。
$firstMarker = -1
for ($index = 0; $index -lt $tokens.Count; $index++) {
if (Test-BaknretMarker -Token $tokens[$index]) { $firstMarker = $index; break }
if (Test-BakNRetMarker -Token $tokens[$index]) { $firstMarker = $index; break }
}
if ($firstMarker -eq 0) {
Write-Log "清单行缺少目标,已忽略:$Line" -Level WARN
Write-BakNRetLog "清单行缺少目标,已忽略:$Line" -Level WARN
return $null
}
@@ -115,7 +115,7 @@
$markerTokens = @($tokens[$firstMarker..($tokens.Count - 1)])
}
$target = Remove-BaknretQuote -Text $targetText
$target = Remove-BakNRetQuote -Text $targetText
if ([string]::IsNullOrWhiteSpace($target)) { return $null }
$overrides = @{}
@@ -124,24 +124,24 @@
$index = 0
while ($index -lt $markerTokens.Count) {
$kind = Test-BaknretMarker -Token $markerTokens[$index]
$kind = Test-BakNRetMarker -Token $markerTokens[$index]
$inline = $null
if ($kind -eq 'at') { $inline = $markerTokens[$index].Substring(1) }
$index++
$values = @()
if (-not [string]::IsNullOrWhiteSpace($inline)) { $values += $inline }
while ($index -lt $markerTokens.Count -and -not (Test-BaknretMarker -Token $markerTokens[$index])) {
while ($index -lt $markerTokens.Count -and -not (Test-BakNRetMarker -Token $markerTokens[$index])) {
$values += $markerTokens[$index]
$index++
}
switch ($kind) {
'path' {
$value = Remove-BaknretQuote -Text ($values -join ' ')
$value = Remove-BakNRetQuote -Text ($values -join ' ')
if (-not [string]::IsNullOrWhiteSpace($value)) {
if ($overrides.ContainsKey('Path')) {
Write-Log "同一条目里给了多次路径覆盖,用最后一个:$Line" -Level WARN
Write-BakNRetLog "同一条目里给了多次路径覆盖,用最后一个:$Line" -Level WARN
}
$overrides['Path'] = $value
}
@@ -149,31 +149,31 @@
# 同类记号可以出现多次(`Foo :- a :- b`),**累积**而不是后者覆盖前者:
# 静默丢掉前一条排除规则正是这个工具最不该犯的错。
'exclude' {
$parsed = @(ConvertFrom-BaknretPatternList -Values $values)
$parsed = @(ConvertFrom-BakNRetPatternList -Values $values)
if ($overrides.ContainsKey('Exclude')) { $overrides['Exclude'] = @($overrides['Exclude']) + $parsed }
else { $overrides['Exclude'] = $parsed }
}
'include' {
$parsed = @(ConvertFrom-BaknretPatternList -Values $values)
$parsed = @(ConvertFrom-BakNRetPatternList -Values $values)
if ($overrides.ContainsKey('Include')) { $overrides['Include'] = @($overrides['Include']) + $parsed }
else { $overrides['Include'] = $parsed }
}
'encrypt' {
if ($overrides.ContainsKey('Encrypt')) { Write-Log "同一条目里给了多次加密开关,用最后一个:$Line" -Level WARN }
if ($overrides.ContainsKey('Encrypt')) { Write-BakNRetLog "同一条目里给了多次加密开关,用最后一个:$Line" -Level WARN }
$overrides['Encrypt'] = $true
}
'noencrypt' {
if ($overrides.ContainsKey('Encrypt')) { Write-Log "同一条目里给了多次加密开关,用最后一个:$Line" -Level WARN }
if ($overrides.ContainsKey('Encrypt')) { Write-BakNRetLog "同一条目里给了多次加密开关,用最后一个:$Line" -Level WARN }
$overrides['Encrypt'] = $false
}
'at' {
$text = Remove-BaknretQuote -Text ($values -join ' ')
$text = Remove-BakNRetQuote -Text ($values -join ' ')
if ([string]::IsNullOrWhiteSpace($text)) { continue }
$equals = $text.IndexOf('=')
if ($equals -lt 0) {
# 兼容历史写法:`@encrypt` / `@!encrypt` / `@pathname` / `@root=名`
foreach ($legacy in @(ConvertFrom-BaknretPatternList -Values @($text))) {
foreach ($legacy in @(ConvertFrom-BakNRetPatternList -Values @($text))) {
$name = $legacy.Trim().TrimStart('@')
if ($name -ieq 'encrypt') { $overrides['Encrypt'] = $true }
elseif ($name -ieq '!encrypt') { $overrides['Encrypt'] = $false }
@@ -183,16 +183,16 @@
}
$key = $text.Substring(0, $equals).Trim()
$value = Remove-BaknretQuote -Text $text.Substring($equals + 1)
$value = Remove-BakNRetQuote -Text $text.Substring($equals + 1)
switch -Regex ($key) {
'(?i)^path$' { $overrides['Path'] = $value }
'(?i)^exclude$' {
$parsed = @(ConvertFrom-BaknretPatternList -Values @($value))
$parsed = @(ConvertFrom-BakNRetPatternList -Values @($value))
if ($overrides.ContainsKey('Exclude')) { $overrides['Exclude'] = @($overrides['Exclude']) + $parsed }
else { $overrides['Exclude'] = $parsed }
}
'(?i)^include$' {
$parsed = @(ConvertFrom-BaknretPatternList -Values @($value))
$parsed = @(ConvertFrom-BakNRetPatternList -Values @($value))
if ($overrides.ContainsKey('Include')) { $overrides['Include'] = @($overrides['Include']) + $parsed }
else { $overrides['Include'] = $parsed }
}
@@ -205,7 +205,7 @@
}
foreach ($unknown in $unknownKeys) {
Write-Log "清单里的 @ 字段 '$unknown' 不是已知字段(Path / Exclude / Include / Encrypt),已忽略:$Line" -Level WARN
Write-BakNRetLog "清单里的 @ 字段 '$unknown' 不是已知字段(Path / Exclude / Include / Encrypt),已忽略:$Line" -Level WARN
}
$resolvedExclude = @()
@@ -218,7 +218,7 @@
Path = $target
# 目录名或文件名,需要靠 SoftwareCatalog 换成真实路径;
# 带分隔符或 %变量% 的写法按字面路径处理。
IsName = (-not (Test-LiteralPath -Path $target))
IsName = (-not (Test-BakNRetLiteralPath -Path $target))
Overrides = $overrides
ExcludePatterns = $resolvedExclude
Includes = $resolvedInclude
@@ -1,4 +1,4 @@
function ConvertFrom-BaknretPatternList {
function ConvertFrom-BakNRetPatternList {
<#
.SYNOPSIS
把修饰符的值列表拼成字符串并按 `,` / `;` 拆成多个模式。
@@ -7,7 +7,7 @@
$parts = @()
foreach ($value in @($Values)) {
$text = Remove-BaknretQuote -Text ([string]$value)
$text = Remove-BakNRetQuote -Text ([string]$value)
if ([string]::IsNullOrWhiteSpace($text)) { continue }
$parts += @($text -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
@@ -1,4 +1,4 @@
function ConvertTo-NativeArgumentString {
function ConvertTo-BakNRetNativeArgumentString {
<#
.SYNOPSIS
按 Windows 的命令行引用规则,把参数数组拼成单个命令行字符串。
@@ -1,10 +1,10 @@
function ConvertTo-BaknretWildcardPattern {
function ConvertTo-BakNRetWildcardPattern {
<#
.SYNOPSIS
把 7z 风格的通配符(* 与 ?)转成正则片段。
.DESCRIPTION
与 Get-BaknretExcludeArgument 保持一致:模式里的空格先转成 `?`(7z 的
与 Get-BakNRetExcludeArgument 保持一致:模式里的空格先转成 `?`(7z 的
`-x!` 不接受带空格的模式)。`*` 转 `.*`,跨过路径分隔符,
这样锚定模式 `Default\*` 才能命中 `Default\a\b`。
#>
+10 -10
View File
@@ -1,4 +1,4 @@
function Enable-BaknretPrivilege {
function Enable-BakNRetPrivilege {
<#
.SYNOPSIS
在当前进程令牌里启用指定特权,返回哪些没能启用。
@@ -26,9 +26,9 @@
Failed = @()
}
if (-not ('Baknret.Privileges' -as [type])) {
if (-not ('BakNRet.Privileges' -as [type])) {
try {
Add-Type -Namespace Baknret -Name Privileges -MemberDefinition @'
Add-Type -Namespace BakNRet -Name Privileges -MemberDefinition @'
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool OpenProcessToken(IntPtr h, int acc, out IntPtr phtok);
[DllImport("advapi32.dll", SetLastError = true)]
@@ -61,7 +61,7 @@ public static int Enable(string name) {
'@
}
catch {
Write-Log "特权启用代码编译失败(本次不启用任何特权):$($_.Exception.Message)" -Level WARN
Write-BakNRetLog "特权启用代码编译失败(本次不启用任何特权):$($_.Exception.Message)" -Level WARN
$result.Failed = @($Name)
return $result
}
@@ -70,12 +70,12 @@ public static int Enable(string name) {
$enabled = @(); $missing = @(); $failed = @()
foreach ($privilege in @($Name)) {
$cacheKey = $privilege
if ($script:BaknretPrivilegeState.ContainsKey($cacheKey)) {
$state = $script:BaknretPrivilegeState[$cacheKey]
if ($script:BakNRetPrivilegeState.ContainsKey($cacheKey)) {
$state = $script:BakNRetPrivilegeState[$cacheKey]
}
else {
$state = [Baknret.Privileges]::Enable($privilege)
$script:BaknretPrivilegeState[$cacheKey] = $state
$state = [BakNRet.Privileges]::Enable($privilege)
$script:BakNRetPrivilegeState[$cacheKey] = $state
}
switch ($state) {
0 { $enabled += $privilege }
@@ -85,10 +85,10 @@ public static int Enable(string name) {
}
if ($missing.Count -gt 0) {
Write-Log ("这些特权不在当前令牌里(需要管理员或 SYSTEM):{0} —— 属主将无法改成别的账户,只能恢复 DACL" -f ($missing -join '、')) -Level WARN
Write-BakNRetLog ("这些特权不在当前令牌里(需要管理员或 SYSTEM):{0} —— 属主将无法改成别的账户,只能恢复 DACL" -f ($missing -join '、')) -Level WARN
}
if ($failed.Count -gt 0) {
Write-Log ("这些特权启用失败:{0}" -f ($failed -join '、')) -Level WARN
Write-BakNRetLog ("这些特权启用失败:{0}" -f ($failed -join '、')) -Level WARN
}
$result.Enabled = @($enabled)
+4 -4
View File
@@ -1,4 +1,4 @@
function Enter-BaknretRunLock {
function Enter-BakNRetRunLock {
<#
.SYNOPSIS
取得"同一份备份目录同一时间只允许一个进程操作"的锁;拿不到时返回 $null。
@@ -8,7 +8,7 @@
"等它跑完"对用户来说和挂住没区别,不如直接说清楚是谁占着。
返回的是一个已打开的文件流。**不需要刻意释放**:进程退出(含 exit)时句柄由系统
关闭,锁随之释放。调用方仍应显式调 Exit-BaknretRunLock,让锁的覆盖范围一眼可见。
关闭,锁随之释放。调用方仍应显式调 Exit-BakNRetRunLock,让锁的覆盖范围一眼可见。
#>
param([Parameter(Mandatory = $true)][string]$Directory)
@@ -16,7 +16,7 @@
New-Item -ItemType Directory -Path $Directory -Force | Out-Null
}
$path = Get-BaknretRunLockPath -Directory $Directory
$path = Get-BakNRetRunLockPath -Directory $Directory
try {
$stream = [System.IO.File]::Open(
$path,
@@ -34,7 +34,7 @@
while ($inner -and $inner -isnot [System.IO.IOException]) { $inner = $inner.InnerException }
if (-not $inner) { throw }
Write-Log ("运行锁不可用({0}):{1}" -f $inner.GetType().Name, $inner.Message) -Level DEBUG
Write-BakNRetLog ("运行锁不可用({0}):{1}" -f $inner.GetType().Name, $inner.Message) -Level DEBUG
return $null
}
+2 -2
View File
@@ -1,4 +1,4 @@
function Exit-BaknretRunLock {
function Exit-BakNRetRunLock {
<#
.SYNOPSIS
释放运行锁。锁文件本身留着 —— 它的内容是最后一次持有者的线索,删不删都无所谓。
@@ -10,6 +10,6 @@
$Lock.Dispose()
}
catch {
Write-Log "释放运行锁失败(进程退出时会自动释放):$_" -Level WARN
Write-BakNRetLog "释放运行锁失败(进程退出时会自动释放):$_" -Level WARN
}
}
+1 -1
View File
@@ -48,7 +48,7 @@
if ($null -ne $evaluated) { $replacement = ([string]($evaluated)).Trim() }
}
catch {
Write-Log "名录路径里的表达式求值失败:$expression —— $($_.Exception.Message)" -Level WARN
Write-BakNRetLog "名录路径里的表达式求值失败:$expression —— $($_.Exception.Message)" -Level WARN
}
$value = $value.Substring(0, $start) + $replacement + $value.Substring($end + 1)
}
@@ -1,4 +1,4 @@
function Find-ChildDirectoryByName {
function Find-BakNRetChildDirectoryByName {
<#
.SYNOPSIS
在 $Parent 下按精确名或"<名>_<后缀>"/"<名>-<后缀>"形式找目录。
@@ -1,4 +1,4 @@
function Format-CatalogName {
function Format-BakNRetCatalogName {
<#
.SYNOPSIS
把软件名规范化成合法的归档基础名。
@@ -1,4 +1,4 @@
function Get-ArchiveTopLevelNames {
function Get-BakNRetArchiveTopLevelNames {
<#
.SYNOPSIS
列出归档内的顶层条目名(用于确认多目录打包时每个目录都真的进去了)。
@@ -36,7 +36,7 @@
Select-Object -ExpandProperty Name)
}
catch {
Write-Log "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG
Write-BakNRetLog "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG
$names = @()
}
finally {
@@ -1,4 +1,4 @@
function Get-BackupBaseName {
function Get-BakNRetBackupBaseName {
<#
.SYNOPSIS
由清单中的原始路径生成归档基础名。
@@ -16,7 +16,7 @@
$parts = @($normalized -split '\\' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($parts.Count -eq 0) {
Write-Log "无法解析路径:$RawPath" -Level ERROR
Write-BakNRetLog "无法解析路径:$RawPath" -Level ERROR
return $null
}
@@ -39,6 +39,6 @@
})
$baseName = $baseName -replace ':', '_'
Write-Log "生成文件基础名:$baseName" -Level DEBUG
Write-BakNRetLog "生成文件基础名:$baseName" -Level DEBUG
return $baseName
}
@@ -1,4 +1,4 @@
function Get-FolderSummary {
function Get-BakNRetFolderSummary {
<#
.SYNOPSIS
统计目录/文件的文件数、总大小与最新修改时间。
@@ -26,7 +26,7 @@
}
}
catch {
Write-Log "无法读取文件夹摘要:$FolderPath" -Level WARN
Write-BakNRetLog "无法读取文件夹摘要:$FolderPath" -Level WARN
return [pscustomobject]@{
FileCount = 0
TotalSize = 0
@@ -1,4 +1,4 @@
function Get-ItemArchiveName {
function Get-BakNRetItemArchiveName {
<#
.SYNOPSIS
决定一个条目的归档基础名(不含扩展名)。
@@ -14,27 +14,27 @@
# @pathname 时用"真实路径"跑路径命名算法。
# 清单里写的可能是软件名,必须先经名录换成真实路径,
# 否则 Get-BackupBaseName 会对软件名本身运算,得出错误的名字。
# 否则 Get-BakNRetBackupBaseName 会对软件名本身运算,得出错误的名字。
if ($Entry.Flags -contains 'pathname') {
$nameSource = $Entry.Path
if (-not (Test-LiteralPath -Path $Entry.Path)) {
$catalogForPath = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
if (-not (Test-BakNRetLiteralPath -Path $Entry.Path)) {
$catalogForPath = Get-BakNRetSoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
if ($catalogForPath.ContainsKey($Entry.Path)) {
$nameSource = $catalogForPath[$Entry.Path].Path
}
}
return Get-BackupBaseName -RawPath $nameSource
return Get-BakNRetBackupBaseName -RawPath $nameSource
}
$looksLikePath = Test-LiteralPath -Path $Entry.Path
$looksLikePath = Test-BakNRetLiteralPath -Path $Entry.Path
if (-not $looksLikePath) {
$catalog = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
$catalog = Get-BakNRetSoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
if ($catalog.ContainsKey($Entry.Path)) {
return $catalog[$Entry.Path].Name
}
Write-Log "名录里没有 '$($Entry.Path)',按目录名处理" -Level WARN
return (Format-CatalogName -Name $Entry.Path)
Write-BakNRetLog "名录里没有 '$($Entry.Path)',按目录名处理" -Level WARN
return (Format-BakNRetCatalogName -Name $Entry.Path)
}
return Get-BackupBaseName -RawPath $Entry.Path
return Get-BakNRetBackupBaseName -RawPath $Entry.Path
}
@@ -1,4 +1,4 @@
function Get-SoftwareCatalog {
function Get-BakNRetSoftwareCatalog {
<#
.SYNOPSIS
载入"软件名 -> Slot 组"名录。
@@ -44,7 +44,7 @@
if (-not $NoCache) {
try {
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
$hash = (Get-FileHash -LiteralPath $Path -Algorithm MD5 -ErrorAction Stop).Hash
$hash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction Stop).Hash
$stamp = '{0}-{1}-{2}' -f $item.LastWriteTimeUtc.Ticks, $item.Length, $hash
if ($script:CatalogCache.ContainsKey($Path) -and $script:CatalogCache[$Path].Stamp -eq $stamp) {
return $script:CatalogCache[$Path].Data
@@ -57,22 +57,22 @@
$data = $null
try {
$data = Import-BaknretDataFile -Path $Path
$data = Import-BakNRetDataFile -Path $Path
}
catch {
Write-Log "软件名录读取失败:$Path —— $_" -Level ERROR
Write-BakNRetLog "软件名录读取失败:$Path —— $_" -Level ERROR
return $result
}
# 递归引入其它名录文件(路径相对本文件)
$includeValue = Get-BaknretMapValue -Map $data -Key 'Includes'
$includeValue = Get-BakNRetMapValue -Map $data -Key 'Includes'
if ($includeValue) {
$baseDir = Split-Path -Parent $Path
foreach ($include in @($includeValue)) {
if (-not $include) { continue }
$includePath = [string]$include
if (-not [System.IO.Path]::IsPathRooted($includePath)) { $includePath = Join-Path $baseDir $includePath }
$included = Get-SoftwareCatalog -Path $includePath -MaxDepth $MaxDepth
$included = Get-BakNRetSoftwareCatalog -Path $includePath -MaxDepth $MaxDepth
foreach ($includedName in $included.Keys) {
if ($result.ContainsKey($includedName)) { continue }
$result[$includedName] = $included[$includedName]
@@ -80,30 +80,30 @@
}
}
foreach ($key in @(Get-BaknretMapKeys -Map $data | Where-Object { $_ -ne 'Includes' })) {
$name = Format-CatalogName -Name ([string]$key)
foreach ($key in @(Get-BakNRetMapKeys -Map $data | Where-Object { $_ -ne 'Includes' })) {
$name = Format-BakNRetCatalogName -Name ([string]$key)
if (-not $name) { continue }
$raw = Get-BaknretMapValue -Map $data -Key $key
$raw = Get-BakNRetMapValue -Map $data -Key $key
if ($raw -isnot [System.Collections.IDictionary] -and $null -ne $raw -and -not ($raw -is [psobject] -and @($raw.PSObject.Properties.Name).Count -gt 0)) {
Write-Log "名录条目 '$key' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }" -Level ERROR
Write-BakNRetLog "名录条目 '$key' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }" -Level ERROR
continue
}
$slots = @()
$errors = @()
foreach ($slotKey in @(Get-BaknretMapKeys -Map $raw)) {
foreach ($slotKey in @(Get-BakNRetMapKeys -Map $raw)) {
$slotName = ([string]$slotKey).Trim()
if (-not $slotName) { continue }
$slotRaw = Get-BaknretMapValue -Map $raw -Key $slotKey
$slotRaw = Get-BakNRetMapValue -Map $raw -Key $slotKey
if ($slotRaw -isnot [System.Collections.IDictionary] -and -not ($slotRaw -is [psobject])) {
$errors += "Slot $slotName 的写法不对,应写成 @{ Path = '...' }"
continue
}
$declaredRaw = [string](Get-BaknretMapValue -Map $slotRaw -Key 'Path')
$declaredRaw = [string](Get-BakNRetMapValue -Map $slotRaw -Key 'Path')
if ([string]::IsNullOrWhiteSpace($declaredRaw)) {
$errors += "Slot $slotName 缺少 Path"
continue
@@ -125,7 +125,7 @@
$parent = Split-Path -Path $declared -Parent
$leafName = Split-Path -Path $declared -Leaf
if ($parent -and $leafName -and (Test-Path -LiteralPath $parent)) {
$candidates = @(Find-ChildDirectoryByName -Parent $parent -Name $leafName -MaxDepth $MaxDepth)
$candidates = @(Find-BakNRetChildDirectoryByName -Parent $parent -Name $leafName -MaxDepth $MaxDepth)
}
}
@@ -144,10 +144,10 @@
if ($resolvedItem) { $isFile = -not $resolvedItem.PSIsContainer }
}
$excludeText = [string](Get-BaknretMapValue -Map $slotRaw -Key 'Exclude')
$includeText = [string](Get-BaknretMapValue -Map $slotRaw -Key 'Include')
$encryptValue = Get-BaknretMapValue -Map $slotRaw -Key 'Encrypt'
$description = [string](Get-BaknretMapValue -Map $slotRaw -Key 'Description')
$excludeText = [string](Get-BakNRetMapValue -Map $slotRaw -Key 'Exclude')
$includeText = [string](Get-BakNRetMapValue -Map $slotRaw -Key 'Include')
$encryptValue = Get-BakNRetMapValue -Map $slotRaw -Key 'Encrypt'
$description = [string](Get-BakNRetMapValue -Map $slotRaw -Key 'Description')
$slots += [pscustomobject]@{
Name = $slotName
@@ -157,8 +157,8 @@
IsFile = $isFile
Suffixed = $suffixed
Description = $description
Exclude = @(ConvertFrom-BaknretPatternList -Values @($excludeText))
Include = @(ConvertFrom-BaknretPatternList -Values @($includeText))
Exclude = @(ConvertFrom-BakNRetPatternList -Values @($excludeText))
Include = @(ConvertFrom-BakNRetPatternList -Values @($includeText))
Encrypt = [bool]$encryptValue
}
}
@@ -178,18 +178,18 @@
else { 'Single' }
if ($errors.Count -gt 0) {
Write-Log ("名录条目 {0} 有问题:{1}" -f $name, ($errors -join ';')) -Level ERROR
Write-BakNRetLog ("名录条目 {0} 有问题:{1}" -f $name, ($errors -join ';')) -Level ERROR
}
elseif ($missing.Count -gt 0) {
Write-Log ("名录:{0} 有 {1} 个 Slot 的路径当前不存在:{2}" -f $name, $missing.Count, (($missing | ForEach-Object { $_.Declared }) -join ';')) -Level DEBUG
Write-BakNRetLog ("名录:{0} 有 {1} 个 Slot 的路径当前不存在:{2}" -f $name, $missing.Count, (($missing | ForEach-Object { $_.Declared }) -join ';')) -Level DEBUG
}
if ($slots.Count -gt 0) {
Write-Log ("名录:{0} -> {1} 个 Slot,其中存在 {2} 个" -f $name, $slots.Count, $existing.Count) -Level DEBUG
Write-BakNRetLog ("名录:{0} -> {1} 个 Slot,其中存在 {2} 个" -f $name, $slots.Count, $existing.Count) -Level DEBUG
}
if ($result.ContainsKey($name)) {
Write-Log ("名录里有两条规范化之后同名的条目:{0}(后者覆盖前者)" -f $name) -Level WARN
Write-BakNRetLog ("名录里有两条规范化之后同名的条目:{0}(后者覆盖前者)" -f $name) -Level WARN
}
$result[$name] = [pscustomobject]@{
@@ -1,4 +1,4 @@
function Get-BaknretAceSignatureList {
function Get-BakNRetAceSignatureList {
<#
.SYNOPSIS
把 ACE 列表压成可比对的"签名"集合(`类型|SID|掩码`)。
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretArchiveTopName {
function Get-BakNRetArchiveTopName {
<# .SYNOPSIS 取归档内相对路径的第一段(顶层名字)。 #>
param([AllowEmptyString()][string]$ArchivePath)
+4 -4
View File
@@ -1,4 +1,4 @@
function Get-BaknretConfig {
function Get-BakNRetConfig {
<#
.SYNOPSIS
读取 BackupConfig.psd1 并与内置默认值合并。
@@ -22,7 +22,7 @@
Snapshot = @{ Enabled = $false; KeepCount = 3; KeepDays = 30 }
Encryption = @{ Enabled = $false; PasswordFile = ''; EncryptHeaders = $true }
# 安全描述符(属主 / ACL)的采集与回放。
# Mode Off | Roots | Smart | Full(语义见 Get-BaknretSecurityRecords)
# Mode Off | Roots | Smart | Full(语义见 Get-BakNRetSecurityRecords)
# **默认 Full**:这个功能存在的意义就是不丢权限,正确性优先于体积;
# Smart 是体积优化(靠继承复现的对象不落盘),已在真机上见过
# 它需要处理的"陈旧继承 ACE",判据偏保守,但终究是启发式。
@@ -43,10 +43,10 @@
}
try {
$loaded = Import-BaknretDataFile -Path $Path
$loaded = Import-BakNRetDataFile -Path $Path
}
catch {
Write-Log "配置文件读取失败(改用默认值):$Path —— $_" -Level WARN
Write-BakNRetLog "配置文件读取失败(改用默认值):$Path —— $_" -Level WARN
return $defaults
}
@@ -1,15 +1,15 @@
function Get-BaknretExcludeArgument {
function Get-BakNRetExcludeArgument {
<#
.SYNOPSIS
把一个归档项的模式列表翻译成 7z 的 `-x!` / `-xr!` 参数。
.DESCRIPTION
传进来的模式**已经按项分配好**(见 Split-BaknretPatternScope),因此这里
传进来的模式**已经按项分配好**(见 Split-BakNRetPatternScope),因此这里
拿到的模式一律是"相对该项归档根"的:
* `<相对路径>` -> `-x!<ArchivePath>\<相对路径>`(锚定在归档根)
* `!<通配>` -> `-xr!<通配>`(任意层级,模式里的空格自动转 `?`)
* `!re:<正则>` -> 遍历源目录翻译成若干 `-x!<完整路径>`(见 Get-BaknretRegexExclude)
* `!re:<正则>` -> 遍历源目录翻译成若干 `-x!<完整路径>`(见 Get-BakNRetRegexExclude)
7z 排除语义(已实测确认):
* `-x!<完整归档内路径>` 匹配对象的完整路径,所以要带上项自己的归档根名;
@@ -33,7 +33,7 @@
if ($text.StartsWith('!re:')) {
$regexText = $text.Substring(4).Trim()
if (-not $regexText) { continue }
$expanded = Get-BaknretRegexExclude -Item $Item -Pattern $regexText -MaxMatches $MaxRegexMatches
$expanded = Get-BakNRetRegexExclude -Item $Item -Pattern $regexText -MaxMatches $MaxRegexMatches
if ($expanded.Error) { $errorText = $expanded.Error; continue }
$arguments += @($expanded.Arguments)
continue
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretFreeSpaceGB {
function Get-BakNRetFreeSpaceGB {
<#
.SYNOPSIS
返回 $Path 所在卷的剩余空间(GB);无法确定时返回 -1。
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretLogPath {
function Get-BakNRetLogPath {
<# .SYNOPSIS 返回当前日志文件路径(未启用时返回 $null)。 #>
return $script:LogConfig.FilePath
}
+3 -3
View File
@@ -1,4 +1,4 @@
function Get-BaknretPassword {
function Get-BakNRetPassword {
<#
.SYNOPSIS
取加密口令:命令行参数 > 环境变量 > 密码文件 > 交互式询问。
@@ -34,7 +34,7 @@
try { $interactive = -not [System.Console]::IsInputRedirected } catch { $interactive = $false }
if ($interactive) {
Write-Log '需要加密口令,请在弹出的提示里输入(不会回显、不会落盘)' -Level WARN
Write-BakNRetLog '需要加密口令,请在弹出的提示里输入(不会回显、不会落盘)' -Level WARN
try {
$secure = Read-Host -Prompt '请输入加密口令' -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
@@ -46,7 +46,7 @@
}
}
catch {
Write-Log "口令输入失败:$_" -Level ERROR
Write-BakNRetLog "口令输入失败:$_" -Level ERROR
return $null
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretRegexExclude {
function Get-BakNRetRegexExclude {
<#
.SYNOPSIS
把一条 `!re:<正则>` 展开成若干 `-x!<归档内路径>` 参数。
+1 -1
View File
@@ -1,4 +1,4 @@
function Get-BaknretRunLockPath {
function Get-BakNRetRunLockPath {
<# .SYNOPSIS 运行锁文件的位置(放在备份目录里,与它保护的账本同处)。 #>
param([Parameter(Mandatory = $true)][string]$Directory)
return (Join-Path $Directory '.baknret.lock')
+5 -5
View File
@@ -1,4 +1,4 @@
function Get-BaknretSecurityRecord {
function Get-BakNRetSecurityRecord {
<#
.SYNOPSIS
读一个对象的安全描述符,产出可序列化的一条记录。
@@ -58,8 +58,8 @@
}
try {
# 陈旧继承 ACE 要固化成显式 ACE,否则恢复后会消失(见 Get-BaknretSecuritySddlWithStale)
$record.s = Get-BaknretSecuritySddlWithStale -Acl $acl -ParentSignatures $ParentSignatures
# 陈旧继承 ACE 要固化成显式 ACE,否则恢复后会消失(见 Get-BakNRetSecuritySddlWithStale)
$record.s = Get-BakNRetSecuritySddlWithStale -Acl $acl -ParentSignatures $ParentSignatures
}
catch {
$record.e = $_.Exception.Message
@@ -80,8 +80,8 @@
$inheritedRules = @($acl.GetAccessRules($false, $true, $sid))
$record.Explicit = $explicitRules.Count
$record.Inherited = $inheritedRules.Count
$record.InheritedSignatures = @(Get-BaknretAceSignatureList -Rules $inheritedRules)
$record.AllSignatures = @(Get-BaknretAceSignatureList -Rules @($acl.GetAccessRules($true, $true, $sid)))
$record.InheritedSignatures = @(Get-BakNRetAceSignatureList -Rules $inheritedRules)
$record.AllSignatures = @(Get-BakNRetAceSignatureList -Rules @($acl.GetAccessRules($true, $true, $sid)))
$inheritable = 0
foreach ($rule in @($acl.GetAccessRules($true, $true, $sid))) {
@@ -1,4 +1,4 @@
function Get-BaknretSecurityRecords {
function Get-BakNRetSecurityRecords {
<#
.SYNOPSIS
采集一组归档项的安全描述符,键是**归档内相对路径**(`<Slot>\…`)。
@@ -10,7 +10,7 @@
遍历用显式栈,并且**跳过 reparse point**:PS 5.1 的 Get-ChildItem -Recurse 会
跟着 junction 无限转;scoop 的 `apps\<app>\current` 就是 junction,正撞在这个坑上。
$ScopeMap 由 Split-BaknretPatternScope 产出(项下标 -> 该相对根的模式数组),
$ScopeMap 由 Split-BakNRetPatternScope 产出(项下标 -> 该相对根的模式数组),
所以这里的排除判定与真正交给 7z 的 -x! / -xr! 是同一套规则。
Mode:
@@ -37,7 +37,7 @@
# 读不到的对象会带 e 字段落进 sidecar,而不是被静默当成"没有特殊权限"。
$privileges = @('SeBackupPrivilege')
if ($IncludeSacl) { $privileges += 'SeSecurityPrivilege' }
Enable-BaknretPrivilege -Name $privileges | Out-Null
Enable-BakNRetPrivilege -Name $privileges | Out-Null
for ($index = 0; $index -lt $Items.Count; $index++) {
$item = $Items[$index]
@@ -55,14 +55,14 @@
if (-not $rootItem) { continue }
if (-not $rootItem.PSIsContainer) {
$record = Get-BaknretSecurityRecord -Path $real -Key $archiveRoot -Kind 'f' -IncludeSacl:$IncludeSacl
$record = Get-BakNRetSecurityRecord -Path $real -Key $archiveRoot -Kind 'f' -IncludeSacl:$IncludeSacl
$scanned++
if ($record.e) { $errorCount++ }
$records.Add($record)
continue
}
$rootRecord = Get-BaknretSecurityRecord -Path $real -Key $archiveRoot -Kind 'd' -IncludeSacl:$IncludeSacl
$rootRecord = Get-BakNRetSecurityRecord -Path $real -Key $archiveRoot -Kind 'd' -IncludeSacl:$IncludeSacl
$scanned++
if ($rootRecord.e) { $errorCount++ }
$records.Add($rootRecord)
@@ -85,10 +85,10 @@
if ($child.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
$childRel = if ($frame.Rel) { $frame.Rel + '\' + $child.Name } else { $child.Name }
if (Test-BaknretPathExcluded -RelativePath $childRel -Patterns $patterns) { continue }
if (Test-BakNRetPathExcluded -RelativePath $childRel -Patterns $patterns) { continue }
$kind = if ($child.PSIsContainer) { 'd' } else { 'f' }
$record = Get-BaknretSecurityRecord -Path $child.FullName -Key ($archiveRoot + '\' + $childRel) `
$record = Get-BakNRetSecurityRecord -Path $child.FullName -Key ($archiveRoot + '\' + $childRel) `
-Kind $kind -IncludeSacl:$IncludeSacl -ParentSignatures $frame.Signatures
$scanned++
if ($record.e) { $errorCount++ }
@@ -96,7 +96,7 @@
if ($Mode -eq 'Full') {
$records.Add($record)
}
elseif (Test-BaknretSecurityRecordNeeded -Record $record `
elseif (Test-BakNRetSecurityRecordNeeded -Record $record `
-ParentOwner $frame.Owner -ParentGroup $frame.Group `
-ParentInheritable $frame.Inheritable -ParentSignatures $frame.Signatures) {
$records.Add($record)
@@ -1,4 +1,4 @@
function Get-BaknretSecuritySddlWithStale {
function Get-BakNRetSecuritySddlWithStale {
<#
.SYNOPSIS
对象与父目录的继承链**不自洽**时,把整套 ACE 冻结成显式副本(并置 protected),
@@ -38,7 +38,7 @@
# 自洽 = 继承来的 ACE 每一条都能在父目录的 ACL 里找到出处
$stale = @()
foreach ($rule in $inherited) {
$signature = @(Get-BaknretAceSignatureList -Rules @($rule))[0]
$signature = @(Get-BakNRetAceSignatureList -Rules @($rule))[0]
if ($ParentSignatures -notcontains $signature) { $stale += $rule }
}
if ($stale.Count -eq 0) { return $Acl.Sddl }
@@ -68,6 +68,6 @@
$rebuilt.SetAccessRuleProtection($true, $false)
Write-Log ("{0} 条继承 ACE 已无出处(父目录里找不到),整套 ACE 冻结为显式并置 protected" -f $stale.Count) -Level DEBUG
Write-BakNRetLog ("{0} 条继承 ACE 已无出处(父目录里找不到),整套 ACE 冻结为显式并置 protected" -f $stale.Count) -Level DEBUG
return $rebuilt.GetSecurityDescriptorSddlForm($sections)
}
+3 -3
View File
@@ -27,12 +27,12 @@
$fileCount++
$totalSize += [int64]$item.Length
}
Write-Log ("分析路径 '{0}':已累计 {1} 个文件,{2} MB" -f $path, $fileCount, [math]::Round($totalSize / 1MB, 2)) -Level DEBUG
Write-BakNRetLog ("分析路径 '{0}':已累计 {1} 个文件,{2} MB" -f $path, $fileCount, [math]::Round($totalSize / 1MB, 2)) -Level DEBUG
}
if ($null -eq $totalSize) { $totalSize = 0 }
$totalSizeMB = [math]::Round($totalSize / 1MB, 2)
Write-Log ("合计分析:{0} 个文件,总大小 {1} MB" -f $fileCount, $totalSizeMB) -Level DEBUG
Write-BakNRetLog ("合计分析:{0} 个文件,总大小 {1} MB" -f $fileCount, $totalSizeMB) -Level DEBUG
if ($totalSizeMB -gt 1024) { $dictSize = '1024m' }
elseif ($totalSizeMB -gt 100) { $dictSize = '256m' }
@@ -47,7 +47,7 @@
$threads = 2
}
Write-Log ("参数优化:字典=$dictSize, 线程=$threads, 级别=$Level") -Level DEBUG
Write-BakNRetLog ("参数优化:字典=$dictSize, 线程=$threads, 级别=$Level") -Level DEBUG
return [pscustomobject]@{
# 只放压缩相关开关。输出开关(-bso0/-bsp0 或默认进度)必须由调用方
+2 -2
View File
@@ -20,7 +20,7 @@
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $FilePath
$startInfo.Arguments = ConvertTo-NativeArgumentString -ArgumentList $ArgumentList
$startInfo.Arguments = ConvertTo-BakNRetNativeArgumentString -ArgumentList $ArgumentList
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $false
if ($WorkingDirectory) {
@@ -37,7 +37,7 @@
$loggableArguments = @($ArgumentList | ForEach-Object {
if ($_ -is [string] -and $_ -like '-p*') { '-p<口令已隐藏>' } else { $_ }
})
Write-Log ('执行: {0} {1}' -f $FilePath, (ConvertTo-NativeArgumentString -ArgumentList $loggableArguments)) -Level DEBUG
Write-BakNRetLog ('执行: {0} {1}' -f $FilePath, (ConvertTo-BakNRetNativeArgumentString -ArgumentList $loggableArguments)) -Level DEBUG
$process = [System.Diagnostics.Process]::Start($startInfo)
try {
@@ -1,4 +1,4 @@
function Merge-BaknretExcludeArgument {
function Merge-BakNRetExcludeArgument {
<#
.SYNOPSIS
合并多个归档项展开出来的排除参数并去重(保序)。
@@ -1,4 +1,4 @@
function Move-BaknretArchiveIntoPlace {
function Move-BakNRetArchiveIntoPlace {
<#
.SYNOPSIS
把临时归档原子地替换到最终路径。
@@ -23,7 +23,7 @@
return
}
catch {
Write-Log "File.Move(overwrite) 不可用(5.1 没有这个重载),改用 File.Replace:$_" -Level DEBUG
Write-BakNRetLog "File.Move(overwrite) 不可用(5.1 没有这个重载),改用 File.Replace:$_" -Level DEBUG
}
# 5.1 走的这条。以前是"先删后移"—— 中途失败会让目标文件消失(旧归档没了、新归档还在
+2 -2
View File
@@ -1,4 +1,4 @@
function New-BaknretArchiveItem {
function New-BakNRetArchiveItem {
<#
.SYNOPSIS
构造一个"归档项":宿主机上的一个目录 / 文件,对应归档内的一条路径。
@@ -27,7 +27,7 @@
$clean = ([string]$ArchivePath).Trim().Trim([char[]]@('\', '/'))
return [pscustomobject]@{
ArchivePath = $clean
TopName = (Get-BaknretArchiveTopName -ArchivePath $clean)
TopName = (Get-BakNRetArchiveTopName -ArchivePath $clean)
RealPath = $RealPath
Kind = $Kind
Slot = $Slot
+8 -8
View File
@@ -1,4 +1,4 @@
function New-BaknretArchiveStaging {
function New-BakNRetArchiveStaging {
<#
.SYNOPSIS
建一个暂存目录,把每个归档项按"归档内的名字"挂进去,供压缩工具直接打包。
@@ -10,7 +10,7 @@
* 目录项 -> 建 junction(不复制数据,等于零成本改名);
* 文件项 -> 先试硬链接(同卷),失败再复制(配置文件都很小)。
返回暂存目录路径;调用方用完必须调 Remove-BaknretArchiveStaging 清理。
返回暂存目录路径;调用方用完必须调 Remove-BakNRetArchiveStaging 清理。
建不出连接点时**明确抛错**,绝不悄悄退化成另一种归档布局 —— 布局一变,恢复就对不上。
#>
param(
@@ -26,7 +26,7 @@
# 半途失败必须在这里自己清干净,不能把责任留给调用方。
#
# 原因:调用方拿到的是**返回值**,而抛错时根本没有返回值 —— Backup.ps1 的 finally 里
# `$stagingRoot` 还是 $null,而 Remove-BaknretArchiveStaging 对 $null 是直接 return。
# `$stagingRoot` 还是 $null,而 Remove-BakNRetArchiveStaging 对 $null 是直接 return。
# 结果是已经建好的 junction 与临时目录永久留在 %TEMP%,而那些 junction 指向的是真实
# 数据;临时目录迟早会被某次 Remove-Item -Recurse 扫到,那一下就会走进真实数据。
try {
@@ -40,14 +40,14 @@
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
if (Test-Path -LiteralPath $linkPath) { Remove-BaknretJunction -Path $linkPath }
if (Test-Path -LiteralPath $linkPath) { Remove-BakNRetJunction -Path $linkPath }
if ($item.IsFile) {
try {
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
}
catch {
Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
Write-BakNRetLog ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
}
}
@@ -55,18 +55,18 @@
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
}
Write-Log ("暂存:{0} -> {1}" -f $item.ArchivePath, $item.RealPath) -Level DEBUG
Write-BakNRetLog ("暂存:{0} -> {1}" -f $item.ArchivePath, $item.RealPath) -Level DEBUG
}
return $Root
}
catch {
try {
Remove-BaknretArchiveStaging -Root $Root
Remove-BakNRetArchiveStaging -Root $Root
}
catch {
# 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径
Write-Log ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
Write-BakNRetLog ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
}
throw
}
+1 -1
View File
@@ -1,4 +1,4 @@
function New-BaknretJunction {
function New-BakNRetJunction {
<#
.SYNOPSIS
建一个 junction;失败时抛异常(调用方决定降级还是报错)。
+2 -2
View File
@@ -1,4 +1,4 @@
function Read-BaknretManifest {
function Read-BakNRetManifest {
<#
.SYNOPSIS
读取 manifest.json;不存在或损坏时返回空清单。
@@ -40,7 +40,7 @@
}
}
catch {
Write-Log "manifest 解析失败(将重新建立):$Path —— $_" -Level WARN
Write-BakNRetLog "manifest 解析失败(将重新建立):$Path —— $_" -Level WARN
return $empty
}
}
@@ -1,4 +1,4 @@
function Read-BaknretSecuritySidecar {
function Read-BakNRetSecuritySidecar {
<#
.SYNOPSIS
读 sidecar;不存在或损坏时返回 $null(调用方据此打"该归档不含安全描述符"的告警)。
@@ -27,7 +27,7 @@
}
}
catch {
Write-Log "安全描述符文件解析失败:$Path —— $($_.Exception.Message)" -Level WARN
Write-BakNRetLog "安全描述符文件解析失败:$Path —— $($_.Exception.Message)" -Level WARN
return $null
}
}
@@ -1,4 +1,4 @@
function Remove-BaknretArchiveStaging {
function Remove-BakNRetArchiveStaging {
<#
.SYNOPSIS
安全拆掉暂存目录:先手工摘掉 junction,再删剩下的普通文件 / 目录。
@@ -17,7 +17,7 @@
$current = $pending.Pop()
foreach ($child in @(Get-ChildItem -LiteralPath $current -Force -ErrorAction SilentlyContinue)) {
if ($child.LinkType -eq 'Junction' -or $child.LinkType -eq 'SymbolicLink') {
Remove-BaknretJunction -Path $child.FullName
Remove-BakNRetJunction -Path $child.FullName
continue
}
if ($child.PSIsContainer) { $pending.Push($child.FullName) }
+1 -1
View File
@@ -1,4 +1,4 @@
function Remove-BaknretJunction {
function Remove-BakNRetJunction {
<#
.SYNOPSIS
只删连接点本身,绝不顺着它删到目标目录里去。
+1 -1
View File
@@ -1,4 +1,4 @@
function Remove-BaknretQuote {
function Remove-BakNRetQuote {
<#
.SYNOPSIS
去掉值两端成对的引号(单双都认);不成对时原样返回。
@@ -1,4 +1,4 @@
function Resolve-BackupEntry {
function Resolve-BakNRetBackupEntry {
<#
.SYNOPSIS
把清单条目解析成"实际要打包什么、归档里长什么样"。
@@ -8,7 +8,7 @@
IsName / BaseName / ArchiveFlavor / Direction
CatalogEntry —— 名录条目(软件名写法才有)
Items —— 归档项数组(见 New-BaknretArchiveItem)
Items —— 归档项数组(见 New-BakNRetArchiveItem)
Encrypt —— 该归档是否加密
ExcludePatterns / HasExcludeOverride —— 条目级 `:-` / `@ Exclude` 覆盖
Includes / HasIncludeOverride —— 条目级 `:+` / `@ Include` 覆盖
@@ -28,9 +28,9 @@
[int]$MaxDepth = 5
)
$isName = -not (Test-LiteralPath -Path $Entry.Path)
$isName = -not (Test-BakNRetLiteralPath -Path $Entry.Path)
$forcePathFlavor = ($Entry.Flags -contains 'pathname')
$baseName = Get-ItemArchiveName -Entry $Entry -CatalogPath $CatalogPath -MaxDepth $MaxDepth
$baseName = Get-BakNRetItemArchiveName -Entry $Entry -CatalogPath $CatalogPath -MaxDepth $MaxDepth
$overrides = $Entry.Overrides
if (-not $overrides) { $overrides = @{} }
@@ -70,13 +70,13 @@
$errorText = "无法从路径里拆出末级名:$real"
}
else {
$items += New-BaknretArchiveItem -ArchivePath $leaf -RealPath $real -Kind 'path' `
$items += New-BakNRetArchiveItem -ArchivePath $leaf -RealPath $real -Kind 'path' `
-Origin 'path' -Exists $exists -IsFile $isFile
}
}
else {
# ---- 写法一:软件名录里的软件名 ----
$catalog = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
$catalog = Get-BakNRetSoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
if (-not $catalog.ContainsKey($Entry.Path)) {
$errorText = "软件名录里没有 '$($Entry.Path)'"
}
@@ -110,7 +110,7 @@
}
}
$items += New-BaknretArchiveItem -ArchivePath $slot.Name -RealPath $resolvedPath -Kind 'slot' `
$items += New-BakNRetArchiveItem -ArchivePath $slot.Name -RealPath $resolvedPath -Kind 'slot' `
-Slot $slot.Name -Description $slot.Description -Origin 'catalog' `
-Exists $exists -IsFile $isFile -Exclude $slot.Exclude
}
@@ -147,7 +147,7 @@
if ($archivePart -match '^[A-Za-z]$' -and ($hostPart.StartsWith('\') -or $hostPart.StartsWith('/'))) {
$lastSeparator = $text.LastIndexOf(':')
if ($lastSeparator -gt $separator) {
Write-Log ("包含项写得像'宿主机:归档内':{0} —— 语法应为 <归档内相对路径>:<宿主机绝对路径>,已按后者解释" -f $text) -Level WARN
Write-BakNRetLog ("包含项写得像'宿主机:归档内':{0} —— 语法应为 <归档内相对路径>:<宿主机绝对路径>,已按后者解释" -f $text) -Level WARN
$hostPart = $text.Substring(0, $lastSeparator).Trim()
$archivePart = $text.Substring($lastSeparator + 1).Trim()
}
@@ -156,7 +156,7 @@
$hostPath = [Environment]::ExpandEnvironmentVariables($hostPart).Trim()
if ([string]::IsNullOrWhiteSpace($hostPath)) {
Write-Log "包含项 '$text' 里没有宿主机路径,已忽略" -Level WARN
Write-BakNRetLog "包含项 '$text' 里没有宿主机路径,已忽略" -Level WARN
continue
}
@@ -170,7 +170,7 @@
$archivePath = $archivePart
if ([string]::IsNullOrWhiteSpace($archivePath)) { $archivePath = Split-Path -Path $hostPath -Leaf }
$items += New-BaknretArchiveItem -ArchivePath $archivePath -RealPath $hostPath -Kind 'include' `
$items += New-BakNRetArchiveItem -ArchivePath $archivePath -RealPath $hostPath -Kind 'include' `
-Origin 'include' -Exists $exists -IsFile $isFile
}
@@ -220,7 +220,7 @@
$encryptedSlots = @($slots | Where-Object { $_.Encrypt })
$encrypt = $encryptedSlots.Count -gt 0
if ($encryptedSlots.Count -gt 0 -and $encryptedSlots.Count -lt $slots.Count) {
Write-Log ("{0}:名录里各 Slot 的 Encrypt 不一致,整个归档按加密处理" -f $Entry.Path) -Level WARN
Write-BakNRetLog ("{0}:名录里各 Slot 的 Encrypt 不一致,整个归档按加密处理" -f $Entry.Path) -Level WARN
}
}
@@ -1,4 +1,4 @@
function Resolve-CatalogPath {
function Resolve-BakNRetCatalogPath {
<#
.SYNOPSIS
计算软件名录的绝对路径(优先 .psd1,找不到就退而用 .json)。
@@ -1,4 +1,4 @@
function Resolve-CompressionTool {
function Resolve-BakNRetCompressionTool {
<#
.SYNOPSIS
探测可用的压缩工具,优先 7z,其次 RAR,最后内置 ZIP。
@@ -17,17 +17,17 @@
$sevenZip = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
}
if ($sevenZip) {
Write-Log '检测到 7z 压缩工具' -Level DEBUG
Write-BakNRetLog '检测到 7z 压缩工具' -Level DEBUG
return [pscustomobject]@{ Name = '7z'; Command = $sevenZip; Extension = '.7z' }
}
$rar = Get-Command rar, winrar -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty Source
if ($rar) {
Write-Log '检测到 RAR 压缩工具' -Level DEBUG
Write-BakNRetLog '检测到 RAR 压缩工具' -Level DEBUG
return [pscustomobject]@{ Name = 'RAR'; Command = $rar; Extension = '.rar' }
}
Write-Log '使用内置 ZIP 工具' -Level DEBUG
Write-BakNRetLog '使用内置 ZIP 工具' -Level DEBUG
return [pscustomobject]@{ Name = 'ZIP'; Command = 'Compress-Archive'; Extension = '.zip' }
}
+6 -6
View File
@@ -1,4 +1,4 @@
function Restore-BaknretSecurity {
function Restore-BakNRetSecurity {
<#
.SYNOPSIS
把 sidecar 里属于某个归档项的那部分安全描述符,回放到真实目标路径上。
@@ -33,7 +33,7 @@
# 写属主需要 SeRestorePrivilege,而且必须**显式启用**:管理员的过滤令牌里它默认是
# disabled,Set-Acl / SetAccessControl 都不会替你打开。没有它,属主会写失败并静默
# 退化成"只恢复 DACL" —— 那恰恰丢掉了这个功能存在的理由(CREATOR OWNER 判给谁)。
Enable-BaknretPrivilege -Name @('SeRestorePrivilege', 'SeBackupPrivilege') | Out-Null
Enable-BakNRetPrivilege -Name @('SeRestorePrivilege', 'SeBackupPrivilege') | Out-Null
if (-not $Sidecar -or -not $Sidecar.Records) { return $result }
@@ -79,22 +79,22 @@
if ($WhatIf) { continue }
$sddl = Convert-BaknretSidMap -Sddl ([string]$entry.Record.s) -SidMap $SidMap
$sddl = Convert-BakNRetSidMap -Sddl ([string]$entry.Record.s) -SidMap $SidMap
try {
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope All
Set-BakNRetObjectSecurity -Item $item -Sddl $sddl -Scope All
$result.Applied++
}
catch {
$fullError = $_
try {
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope OwnerAndAccess
Set-BakNRetObjectSecurity -Item $item -Sddl $sddl -Scope OwnerAndAccess
$result.OwnerFailed++
$result.Failures += ("{0}:属组未恢复,属主与 DACL 已恢复({1})" -f $target, $fullError.Exception.Message)
}
catch {
try {
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope AccessOnly
Set-BakNRetObjectSecurity -Item $item -Sddl $sddl -Scope AccessOnly
$result.OwnerFailed++
$result.Failures += ("{0}:属主/属组未恢复({1}),已只恢复 DACL" -f $target, $_.Exception.Message)
}
@@ -1,4 +1,4 @@
function Save-BaknretSecuritySidecar {
function Save-BakNRetSecuritySidecar {
<#
.SYNOPSIS
把采集结果写成 sidecar(`<归档名>.acl.json`)。
@@ -47,5 +47,5 @@
}
$json = $payload | ConvertTo-Json -Depth 5
return (Write-BaknretAtomicText -Path $Path -Text $json)
return (Write-BakNRetAtomicText -Path $Path -Text $json)
}
+1 -1
View File
@@ -1,4 +1,4 @@
function Set-BaknretDebug {
function Set-BakNRetDebug {
<# .SYNOPSIS 打开 DEBUG 级别日志。 #>
param([switch]$Enabled = $true)
$script:LogConfig.EnableDebug = [bool]$Enabled
+1 -1
View File
@@ -1,4 +1,4 @@
function Set-BaknretObjectSecurity {
function Set-BakNRetObjectSecurity {
<#
.SYNOPSIS
把一条 SDDL 落到一个对象上。
+2 -2
View File
@@ -1,4 +1,4 @@
function Split-BaknretPatternScope {
function Split-BakNRetPatternScope {
<#
.SYNOPSIS
把条目级的模式按 `<归档项名>\` 前缀分配到各个归档项上。
@@ -23,7 +23,7 @@
$map = @{}
for ($index = 0; $index -lt $Items.Count; $index++) { $map[$index] = @() }
# 归档项的名字(顶层目录名)。同一个条目里不允许重名,Resolve-BackupEntry 会拦。
# 归档项的名字(顶层目录名)。同一个条目里不允许重名,Resolve-BakNRetBackupEntry 会拦。
$topIndex = @{}
for ($index = 0; $index -lt $Items.Count; $index++) {
$name = [string]$Items[$index].ArchivePath
+1 -1
View File
@@ -1,4 +1,4 @@
function Split-BaknretToken {
function Split-BakNRetToken {
<#
.SYNOPSIS
把清单的一行切成空白分隔的记号;引号内的空白不切分,引号本身留在记号里。
+1 -1
View File
@@ -1,4 +1,4 @@
function Start-BaknretLog {
function Start-BakNRetLog {
<#
.SYNOPSIS
把后续日志同时写入 <Directory>/<Prefix>-<时间戳>.log,返回日志文件路径。
+1 -1
View File
@@ -1,4 +1,4 @@
function Stop-BaknretLog {
function Stop-BakNRetLog {
<# .SYNOPSIS 停止写入日志文件。 #>
$script:LogConfig.FilePath = $null
}
@@ -1,4 +1,4 @@
function Sync-BaknretManifestArchive {
function Sync-BakNRetManifestArchive {
<#
.SYNOPSIS
清空 manifest 里"指向了一个不存在的归档"的 archive 字段,返回被清空的条目名。
@@ -1,4 +1,4 @@
function Test-Administrator {
function Test-BakNRetAdministrator {
<# .SYNOPSIS 当前进程是否以管理员身份运行。 #>
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
@@ -1,4 +1,4 @@
function Test-LiteralPath {
function Test-BakNRetLiteralPath {
<#
.SYNOPSIS
判断清单里的一行是不是"字面路径"(而非软件名)。
+1 -1
View File
@@ -1,4 +1,4 @@
function Test-BaknretMarker {
function Test-BakNRetMarker {
<#
.SYNOPSIS
判断一个记号是不是清单修饰符,返回它的种类;不是则返回 $null。
+4 -4
View File
@@ -1,4 +1,4 @@
function Test-BaknretPathExcluded {
function Test-BakNRetPathExcluded {
<#
.SYNOPSIS
判断归档内的一个相对路径是否命中排除模式。
@@ -38,7 +38,7 @@
}
}
catch {
Write-Log "排除正则非法,安全描述符采集按'不排除'处理:$regexText —— $($_.Exception.Message)" -Level WARN
Write-BakNRetLog "排除正则非法,安全描述符采集按'不排除'处理:$regexText —— $($_.Exception.Message)" -Level WARN
}
continue
}
@@ -46,7 +46,7 @@
if ($text.StartsWith('!')) {
$wildcard = $text.Substring(1).Trim()
if (-not $wildcard) { continue }
$componentPattern = '^(?:' + (ConvertTo-BaknretWildcardPattern -Pattern $wildcard) + ')$'
$componentPattern = '^(?:' + (ConvertTo-BakNRetWildcardPattern -Pattern $wildcard) + ')$'
foreach ($component in $components) {
if ($component -match $componentPattern) { return $true }
}
@@ -55,7 +55,7 @@
$anchored = ([string]$text).Trim([char[]]@('\', '/'))
if (-not $anchored) { continue }
$anchoredPattern = '^(?:' + (ConvertTo-BaknretWildcardPattern -Pattern $anchored) + ')$'
$anchoredPattern = '^(?:' + (ConvertTo-BakNRetWildcardPattern -Pattern $anchored) + ')$'
if ($relative -match $anchoredPattern) { return $true }
}
@@ -1,4 +1,4 @@
function Test-BaknretSecurityRecordNeeded {
function Test-BakNRetSecurityRecordNeeded {
<#
.SYNOPSIS
Smart 模式下判断这条记录是否必须落进 sidecar。
@@ -1,4 +1,4 @@
function Write-BackupEntryPlan {
function Write-BakNRetBackupEntryPlan {
<#
.SYNOPSIS
在动手打包之前,把"这条会打包哪些目录、归档里长什么样、排除了什么、为什么"打印出来。
@@ -27,41 +27,41 @@
'restore' = '仅恢复(行首 -)'
}
Write-Log ("条目:{0}" -f $DisplayPath)
Write-Log (" 归档:{0}.7z;方向:{1};加密:{2}" -f $Resolved.BaseName,
Write-BakNRetLog ("条目:{0}" -f $DisplayPath)
Write-BakNRetLog (" 归档:{0}.7z;方向:{1};加密:{2}" -f $Resolved.BaseName,
$(if ($directionText.ContainsKey($Resolved.Direction)) { $directionText[$Resolved.Direction] } else { $Resolved.Direction }),
$(if ($Resolved.Encrypt) { '是' } else { '否' }))
if ($Comment) { Write-Log (" 说明:{0}" -f $Comment) }
if ($Resolved.Error) { Write-Log (" 提示:{0}" -f $Resolved.Error) -Level WARN }
if ($Comment) { Write-BakNRetLog (" 说明:{0}" -f $Comment) }
if ($Resolved.Error) { Write-BakNRetLog (" 提示:{0}" -f $Resolved.Error) -Level WARN }
$items = @($Resolved.Items)
if ($items.Count -eq 0) { Write-Log ' 归档项:没有解析出任何目录' -Level WARN }
if ($items.Count -eq 0) { Write-BakNRetLog ' 归档项:没有解析出任何目录' -Level WARN }
for ($index = 0; $index -lt $items.Count; $index++) {
$item = $items[$index]
$exists = Test-Path -LiteralPath $item.RealPath
$origin = if ($item.Origin -and $originText.ContainsKey($item.Origin)) { $originText[$item.Origin] } else { $item.Origin }
Write-Log (" 归档项 {0}/{1}:{2} <- {3}" -f ($index + 1), $items.Count, $item.ArchivePath, $item.RealPath)
Write-Log (" 来源:{0};{1};{2}" -f $origin,
Write-BakNRetLog (" 归档项 {0}/{1}:{2} <- {3}" -f ($index + 1), $items.Count, $item.ArchivePath, $item.RealPath)
Write-BakNRetLog (" 来源:{0};{1};{2}" -f $origin,
$(if ($exists) { '存在,会打包' } else { '当前不存在,本次跳过' }),
$(if ($item.IsFile) { '文件' } else { '目录' }))
if ($item.Description) { Write-Log (" 介绍:{0}" -f $item.Description) }
if ($item.Description) { Write-BakNRetLog (" 介绍:{0}" -f $item.Description) }
if (@($item.Exclude).Count -gt 0) {
Write-Log (" 名录里的排除:{0}" -f (@($item.Exclude) -join '、'))
Write-BakNRetLog (" 名录里的排除:{0}" -f (@($item.Exclude) -join '、'))
}
}
if ($ListExcludes.Count -gt 0) {
Write-Log (" 排除 {0} 条(来自清单的 :- / @ Exclude):{1}" -f $ListExcludes.Count, ($ListExcludes -join '、'))
Write-BakNRetLog (" 排除 {0} 条(来自清单的 :- / @ Exclude):{1}" -f $ListExcludes.Count, ($ListExcludes -join '、'))
}
if ($CatalogExcludes.Count -gt 0) {
Write-Log (" 排除 {0} 条(来自名录 Slot 的 Exclude):{1}" -f $CatalogExcludes.Count, ($CatalogExcludes -join '、'))
Write-BakNRetLog (" 排除 {0} 条(来自名录 Slot 的 Exclude):{1}" -f $CatalogExcludes.Count, ($CatalogExcludes -join '、'))
}
if ($ConfigExcludes.Count -gt 0) {
Write-Log (" 排除 {0} 条(来自 BackupConfig.psd1 的 DefaultExcludes):{1}" -f $ConfigExcludes.Count, ($ConfigExcludes -join '、'))
Write-BakNRetLog (" 排除 {0} 条(来自 BackupConfig.psd1 的 DefaultExcludes):{1}" -f $ConfigExcludes.Count, ($ConfigExcludes -join '、'))
}
if ($ListExcludes.Count -eq 0 -and $CatalogExcludes.Count -eq 0 -and $ConfigExcludes.Count -eq 0) {
Write-Log ' 排除:无(整包收下)'
Write-BakNRetLog ' 排除:无(整包收下)'
}
}
@@ -1,4 +1,4 @@
function Write-Log {
function Write-BakNRetLog {
<#
.SYNOPSIS
写一条日志到控制台,并在启用日志文件时落盘。
+2 -2
View File
@@ -1,4 +1,4 @@
function Write-BaknretAtomicText {
function Write-BakNRetAtomicText {
<#
.SYNOPSIS
原子写一个文本文件(先写 .tmp,再替换)。
@@ -17,7 +17,7 @@
[System.IO.File]::WriteAllText($temp, $Text, $script:LogEncoding)
if (-not (Test-Path -LiteralPath $Path)) {
Move-BaknretArchiveIntoPlace -TempPath $temp -DestinationPath $Path
Move-BakNRetArchiveIntoPlace -TempPath $temp -DestinationPath $Path
return $Path
}
+2 -2
View File
@@ -1,4 +1,4 @@
function Write-BaknretManifest {
function Write-BakNRetManifest {
<#
.SYNOPSIS
原子写入 manifest.json(UTF-8 无 BOM)。
@@ -18,6 +18,6 @@
# 复用原子写,而不是自己"删旧再改名":后者一旦在中途失败,旧 manifest 已经没了 ——
# 而 manifest 是"这块归档是谁的"的唯一账本,丢了它只能靠文件名反推。
Write-BaknretAtomicText -Path $Path -Text $json
Write-BakNRetAtomicText -Path $Path -Text $json
return $Path
}
+2 -2
View File
@@ -1,4 +1,4 @@
# BakNRet
# BakNRet
一个 Windows 备份 / 恢复工具:把机器上指定的软件与目录收进归档,并能把它们放回原位。本文件是本项目的术语表——「要处理什么」「东西放在哪」这些概念在本仓库里只有一个叫法,写作与命名都照这里的词来。
@@ -70,4 +70,4 @@ _Avoid_: 无用归档、残留、垃圾文件
**BakNRet**:
本工具的名称,任何场合(文档、文件名、标识符)都一律写作 `BakNRet`。
_Avoid_: Baknret、baknret、BakRet、BaknRet 备份工具
_Avoid_: BakNRet、baknret、BakRet、BaknRet 备份工具
+1 -1
View File
@@ -14,7 +14,7 @@
逐条决策(都在本次改造里确认过,理由另见 docs/adr/):
* PSAvoidUsingWriteHost 全局排除 —— Write-Log 的彩色控制台输出是这份工具的**刻意设计**,
* PSAvoidUsingWriteHost 全局排除 —— Write-BakNRetLog 的彩色控制台输出是这份工具的**刻意设计**,
不是疏忽。这是架构性选择,所以在配置里排除掉,而不是逐处 Suppress 假装它是例外。
* PSUseSingularNouns 放行 SecurityRecords / MapKeys / ArchiveTopLevelNames ——
单数名 Get-BakNretSecurityRecord 已被"单对象版本"占用,为了规则把两个语义搅在一起
+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
+40 -40
View File
@@ -37,7 +37,7 @@ BeforeAll {
# 见 BakNRet.Tests.ps1 里的说明:本机沙箱禁止 PowerShell 为捕获原生子进程输出建管道,
# 所以走"临时 .cmd + 文件重定向 + Invoke-ExternalCommand(继承 stdio)"这条路。
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
param(
[Parameter(Mandatory = $true)][string]$Script,
[hashtable]$Parameters = @{}
@@ -138,7 +138,7 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It '一个软件多个 Slot:Kind=Multi,Slot 按名排序且说明被保留' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog['pair'].Kind | Should -Be 'Multi'
@($catalog['pair'].Slots).Count | Should -Be 2
(@($catalog['pair'].Slots | ForEach-Object { $_.Name }) -join ',') | Should -Be 'A,B'
@@ -150,7 +150,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '每个 Slot 都是一个独立的归档项来源(Kind=slot / Origin=catalog)' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'A,B'
(@($resolved.Items | ForEach-Object { $_.Kind }) -join ',') | Should -Be 'slot,slot'
@@ -161,22 +161,22 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It 'Slot 级排除写在 Slot 自己身上(相对本 Slot 的归档根)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
(@($catalog['slotex'].Slots[0].Exclude) -join '|') | Should -Be '!*Cache|logs\'
$entry = ConvertFrom-BackupListLine -Line 'slotex'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
(@($resolved.Items[0].Exclude) -join '|') | Should -Be '!*Cache|logs\'
$resolved.HasExcludeOverride | Should -BeFalse
}
It '数组里"当前不存在"的 Slot 仍然产出归档项(恢复要靠它还原回原位)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog['partial'].Kind | Should -Be 'Partial'
@($catalog['partial'].Missing) | Should -Contain $script:MissingDir
$entry = ConvertFrom-BackupListLine -Line 'partial'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
@($resolved.Items | ForEach-Object { $_.RealPath }) | Should -Contain $script:MissingDir
$resolved.Blocking | Should -BeNullOrEmpty # 源不存在不是致命错误
@@ -184,7 +184,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '文件 Slot:归档项是文件项(归档里就是名为 Slot 的文件)' {
$entry = ConvertFrom-BackupListLine -Line 'fileapp'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].IsFile | Should -BeTrue
$resolved.Items[0].ArchivePath | Should -Be 'Cfg'
@@ -193,13 +193,13 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It '旧的裸字符串 / 字符串数组写法被拒绝(ERROR + 跳过)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog.ContainsKey('legacystr') | Should -BeFalse
$catalog.ContainsKey('legacyarr') | Should -BeFalse
}
It '旧的 @{ Dirs = @(...) } 写法不再展开,留下 Invalid 与原因' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog.ContainsKey('legacydirs') | Should -BeTrue
$catalog['legacydirs'].Kind | Should -Be 'Invalid'
$catalog['legacydirs'].Error | Should -Not -BeNullOrEmpty
@@ -208,7 +208,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '多 Slot 的名录条目在清单里仍然按软件名命名归档' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be 'pair'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be 'pair'
}
}
@@ -227,7 +227,7 @@ Describe '清单修饰符:新契约格式' {
It ':: 覆盖 Path:单 Slot 条目直接生效' {
$entry = ConvertFrom-BackupListLine -Line "solo :: $script:DirB"
$entry.Overrides.ContainsKey('Path') | Should -BeTrue
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].ArchivePath | Should -Be 'Only'
$resolved.Items[0].RealPath | Should -Be $script:DirB
@@ -236,7 +236,7 @@ Describe '清单修饰符:新契约格式' {
It ':: 覆盖遇到多 Slot 条目 -> Blocking(不知道给哪一个,绝不猜)' {
$entry = ConvertFrom-BackupListLine -Line "pair :: $script:DirB"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 0
$resolved.Blocking | Should -Match '不能用一个'
}
@@ -246,7 +246,7 @@ Describe '清单修饰符:新契约格式' {
(@($entry.ExcludePatterns) -join '|') | Should -Be 'logs\|!*Cache'
(@($entry.Includes) -join '|') | Should -Be "Mods:$script:DirB"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved.HasExcludeOverride | Should -BeTrue
$resolved.HasIncludeOverride | Should -BeTrue
@($resolved.Items | ForEach-Object { $_.ArchivePath }) | Should -Contain 'Mods'
@@ -261,13 +261,13 @@ Describe '清单修饰符:新契约格式' {
}
It ':encrypt / :!encrypt 覆盖名录里的加密默认值' {
$base = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp') -CatalogPath $script:FormatCatalog -MaxDepth 3
$base = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp') -CatalogPath $script:FormatCatalog -MaxDepth 3
$base.Encrypt | Should -BeTrue # 名录里 Cfg Slot 标了 Encrypt
$off = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp :!encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$off = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp :!encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$off.Encrypt | Should -BeFalse
$on = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'pair :encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$on = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'pair :encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$on.Encrypt | Should -BeTrue
}
@@ -278,8 +278,8 @@ Describe '清单修饰符:新契约格式' {
# @pathname 对软件名条目也会改用真实路径命名
$entry = ConvertFrom-BackupListLine -Line 'pair @pathname'
$expected = Get-BackupBaseName -RawPath $script:DirA
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be $expected
$expected = Get-BakNRetBackupBaseName -RawPath $script:DirA
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be $expected
}
It '同一行里重复写同类记号会累积(不静默丢掉前一条规则)' {
@@ -350,14 +350,14 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
$script:SlotList = Write-ListFile -Path (Join-Path $script:SlotRoot 'list.txt') -Content "appkit`n"
$script:SlotBackupDir = Join-Path $script:SlotRoot 'Backups'
$script:SlotBackupRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:SlotBackupRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:SlotList
BackupDir = $script:SlotBackupDir
ConfigPath = $script:SlotConfig
Force = $true
QuietTool = $true
}
$script:SlotManifest = Read-BaknretManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$script:SlotManifest = Read-BakNRetManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$script:SlotArchive = @(Get-ChildItem -LiteralPath $script:SlotBackupDir -File -Filter *.7z)[0]
}
@@ -367,7 +367,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
}
It '归档顶层就是各个 Slot 名(目录 Slot + 文件 Slot + Include 项)' {
$top = @(Get-ArchiveTopLevelNames -ArchivePath $script:SlotArchive.FullName -SevenZip $script:SevenZip)
$top = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:SlotArchive.FullName -SevenZip $script:SevenZip)
(@($top | Sort-Object) -join ',') | Should -Be 'Cfg,Data,Extra,Modules'
}
@@ -396,7 +396,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
Remove-Item -LiteralPath (Join-Path $script:SlotRoot 'apps') -Recurse -Force
Remove-Item -LiteralPath $script:SlotInclude -Recurse -Force
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:SlotList
BackupDir = $script:SlotBackupDir
ConfigPath = $script:SlotConfig
@@ -422,7 +422,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
}
It '恢复之后 manifest 记下 lastRestoreAt' {
$manifest = Read-BaknretManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$manifest.items['appkit'].lastRestoreAt | Should -Not -BeNullOrEmpty
}
}
@@ -462,7 +462,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
}
It '归档确实是旧布局:顶层是源目录名而不是 Slot 名' {
$top = @(Get-ArchiveTopLevelNames -ArchivePath $script:LegacyArchive -SevenZip $script:SevenZip)
$top = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:LegacyArchive -SevenZip $script:SevenZip)
(@($top | Sort-Object) -join ',') | Should -Be $script:LegacyLeaf
}
@@ -471,7 +471,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
# 不能靠 7z 的退出码猜(7z 对不存在的条目同样返回 0)。
Remove-Item -LiteralPath (Join-Path $script:LegacyDestParent $script:LegacyLeaf) -Recurse -Force -ErrorAction SilentlyContinue
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:LegacyList
BackupDir = $script:LegacyBackupDir
ConfigPath = $script:LegacyConfig
@@ -490,7 +490,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
$missingList = Write-ListFile -Path (Join-Path $script:LegacyRoot 'missing-list.txt') `
-Content "oldapp :: $script:LegacyRoot\dest2\Nothing Here`n"
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $missingList
BackupDir = $script:LegacyBackupDir
ConfigPath = $script:LegacyConfig
@@ -522,7 +522,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script
$script:RejectConfig = Write-ListFile -Path (Join-Path $script:RejectRoot 'config.psd1') -Content "@{ SoftwareCatalog = '$script:RejectCatalog' }`n"
$script:RejectBackupDir = Join-Path $script:RejectRoot 'Backups'
$script:RejectRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:RejectRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:RejectList
BackupDir = $script:RejectBackupDir
ConfigPath = $script:RejectConfig
@@ -538,7 +538,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script
}
It 'manifest 里记下这次是 failed,并带上原因' {
$manifest = Read-BaknretManifest -Path (Join-Path $script:RejectBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:RejectBackupDir 'manifest.json')
$record = $manifest.items['collide']
$record.action | Should -Be 'failed'
$record.reason | Should -Match '归档内路径冲突'
@@ -568,7 +568,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
# 第一次:清单里有 my-app
Write-ListFile -Path $script:OrphanList -Content "my-app`n" | Out-Null
$null = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$null = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
@@ -578,7 +578,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
# 第二次:把条目从清单里拿掉,归档留在磁盘上
Write-ListFile -Path $script:OrphanList -Content "# 清单里没有条目了`n" | Out-Null
$script:OrphanRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:OrphanRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
@@ -588,7 +588,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
It '归档确实还在磁盘上,manifest 里也还留着历史记录' {
Test-Path -LiteralPath (Join-Path $script:OrphanBackupDir 'my-app.7z') | Should -BeTrue
$manifest = Read-BaknretManifest -Path (Join-Path $script:OrphanBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:OrphanBackupDir 'manifest.json')
$manifest.items.Contains('my-app') | Should -BeTrue
}
@@ -610,16 +610,16 @@ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写'
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SyncRoot 'real.7z') 'x'
$script:SyncManifestPath = Join-Path $script:SyncRoot 'manifest.json'
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$manifest.items['real'] = [pscustomobject]@{ archive = 'real.7z'; action = 'backed-up'; source = 'real' }
$manifest.items['gone'] = [pscustomobject]@{ archive = 'gone.7z'; action = 'missing-source'; source = 'gone' }
$manifest.items['none'] = [pscustomobject]@{ archive = $null; action = 'missing-source'; source = 'none' }
Write-BaknretManifest -Path $script:SyncManifestPath -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $script:SyncManifestPath -Manifest $manifest | Out-Null
}
It '存在的归档保留 archive,不存在的被清空' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$cleared = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared.Count | Should -Be 1
$cleared[0] | Should -Be 'gone'
@@ -629,16 +629,16 @@ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写'
}
It '清空 archive 时保留条目本身的历史(source / action 不动)' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$null = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest.items['gone'].source | Should -Be 'gone'
$manifest.items['gone'].action | Should -Be 'missing-source'
}
It '人工删掉归档之后再同步一次,记录会被纠正过来' {
Remove-Item -LiteralPath (Join-Path $script:SyncRoot 'real.7z') -Force
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$cleared = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared | Should -Contain 'real'
$manifest.items['real'].archive | Should -BeNullOrEmpty
+40 -40
View File
@@ -118,7 +118,7 @@ BeforeAll {
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
}
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
<# .SYNOPSIS 用子进程跑 Backup.ps1 / Restore.ps1(两个脚本结尾都 exit,必须独立进程)。 #>
param(
[Parameter(Mandatory = $true)][string]$Script,
@@ -224,31 +224,31 @@ AfterAll {
Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' {
# ============================================================================
It '锚定模式只命中它自己那棵子树' {
Test-BaknretPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'Other\Cache' -Patterns @('Default\Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Other\Cache' -Patterns @('Default\Cache') | Should -BeFalse
}
It '! 通配按任意层级的组件名匹配(* 不是正则)' {
Test-BaknretPathExcluded -RelativePath 'a\Code Cache\f' -Patterns @('!*Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'Code Cache' -Patterns @('!*Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'a\teamcache.bin' -Patterns @('!*Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\Code Cache\f' -Patterns @('!*Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'Code Cache' -Patterns @('!*Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'a\teamcache.bin' -Patterns @('!*Cache') | Should -BeFalse
}
It '!re: 走正则,且组件名与整条相对路径都算命中' {
Test-BaknretPathExcluded -RelativePath 'x\y\a.log' -Patterns @('!re:\.log$') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'x\y\a.txt' -Patterns @('!re:\.log$') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'Cache\data' -Patterns @('!re:^Cache$') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'x\y\a.log' -Patterns @('!re:\.log$') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'x\y\a.txt' -Patterns @('!re:\.log$') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Cache\data' -Patterns @('!re:^Cache$') | Should -BeTrue
}
It '没有模式时一律不排除' {
Test-BaknretPathExcluded -RelativePath 'a\b' -Patterns @() | Should -BeFalse
Test-BaknretPathExcluded -RelativePath '' -Patterns @('!*') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\b' -Patterns @() | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath '' -Patterns @('!*') | Should -BeFalse
}
It '模式里的空格按 7z 的规矩当 ? 处理' {
Test-BaknretPathExcluded -RelativePath 'a\Cache' -Patterns @('!*Cache Extras') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'a\Cache Extras' -Patterns @('!*Cache Extras') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'a\Cache' -Patterns @('!*Cache Extras') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\Cache Extras' -Patterns @('!*Cache Extras') | Should -BeTrue
}
}
@@ -257,19 +257,19 @@ Describe 'SID 映射(跨机恢复)' {
# ============================================================================
It '整 SID 精确替换' {
$sddl = 'O:S-1-5-21-1-2-3-1001G:S-1-5-21-1-2-3-1001D:(A;;FA;;;S-1-5-21-1-2-3-1001)'
$mapped = Convert-BaknretSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped = Convert-BakNRetSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped | Should -Be 'O:S-1-5-21-9-8-7-1001G:S-1-5-21-9-8-7-1001D:(A;;FA;;;S-1-5-21-9-8-7-1001)'
}
It '不会误伤以它为前缀的更长的 SID' {
$sddl = 'D:(A;;FA;;;S-1-5-21-1-2-3-1001)(A;;FA;;;S-1-5-21-1-2-3-10012)'
$mapped = Convert-BaknretSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped = Convert-BakNRetSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped | Should -Be 'D:(A;;FA;;;S-1-5-21-9-8-7-1001)(A;;FA;;;S-1-5-21-1-2-3-10012)'
}
It '空映射表时原样返回' {
$sddl = 'D:(A;;FA;;;SY)'
Convert-BaknretSidMap -Sddl $sddl -SidMap @{} | Should -Be $sddl
Convert-BakNRetSidMap -Sddl $sddl -SidMap @{} | Should -Be $sddl
}
}
@@ -283,7 +283,7 @@ Describe '安全描述符采集' {
}
It 'Full:每个对象一条记录,键是归档内相对路径' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture.Scanned | Should -Be 3
$capture.Kept | Should -Be 3
$capture.Errors | Should -Be 0
@@ -291,7 +291,7 @@ Describe '安全描述符采集' {
}
It '根记录的 SDDL 保留了 CREATOR OWNER、IO 标志、孤儿 SID 和 protected 位' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$root = @($capture.Records | Where-Object { $_.p -eq 'Data' })[0]
$root.s | Should -Match 'D:PAI'
$root.s | Should -Match '\(A;OICIIO;GA;;;CO\)'
@@ -300,23 +300,23 @@ Describe '安全描述符采集' {
}
It 'Smart 比 Full 少,但根永远保留' {
$full = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$smart = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Smart
$full = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$smart = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Smart
$smart.Kept | Should -BeLessOrEqual $full.Kept
@($smart.Records | ForEach-Object { $_.p }) | Should -Contain 'Data'
}
It 'Roots 只存归档项的根,不再往下走' {
$roots = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Roots
$roots = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Roots
$roots.Kept | Should -Be 1
$roots.Records[0].p | Should -Be 'Data'
}
It 'sidecar 往返:条数与 SDDL 原样保留' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$path = Join-Path $script:Sandbox 'roundtrip.acl.json'
Save-BaknretSecuritySidecar -Path $path -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$sidecar = Read-BaknretSecuritySidecar -Path $path
Save-BakNRetSecuritySidecar -Path $path -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$sidecar = Read-BakNRetSecuritySidecar -Path $path
$sidecar.Records.Count | Should -Be 3
$record = @($sidecar.Records | Where-Object { $_.p -eq 'Data\sub\a.txt' })[0]
$record.k | Should -Be 'f'
@@ -324,7 +324,7 @@ Describe '安全描述符采集' {
}
It '旁挂文件不存在时读出 $null(调用方据此打告警,而不是静默当没事)' {
Read-BaknretSecuritySidecar -Path (Join-Path $script:Sandbox 'nope.acl.json') | Should -BeNullOrEmpty
Read-BakNRetSecuritySidecar -Path (Join-Path $script:Sandbox 'nope.acl.json') | Should -BeNullOrEmpty
}
It '排除模式在采集时同样生效(采集树 == 归档树)' {
@@ -336,7 +336,7 @@ Describe '安全描述符采集' {
[System.IO.File]::WriteAllText((Join-Path $walkRoot 'keep.txt'), 'x')
$walkItem = [pscustomobject]@{ ArchivePath = 'Data'; RealPath = $walkRoot }
$capture = Get-BaknretSecurityRecords -Items @($walkItem) -Mode Full -ScopeMap @{ 0 = @('!Cache') }
$capture = Get-BakNRetSecurityRecords -Items @($walkItem) -Mode Full -ScopeMap @{ 0 = @('!Cache') }
@($capture.Records | ForEach-Object { $_.p }) | Should -Not -Contain 'Data\Cache'
@($capture.Records | ForEach-Object { $_.p }) | Should -Contain 'Data\keep.txt'
}
@@ -350,17 +350,17 @@ Describe '安全描述符回放' {
$script:TargetRoot = Join-Path $script:Sandbox 'restore\target'
$script:RestoreFingerprints = New-AclSourceTree -Root $script:RestoreRoot
$capture = Get-BaknretSecurityRecords -Items @([pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:RestoreRoot }) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @([pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:RestoreRoot }) -Mode Full
$script:RestoreSidecarPath = Join-Path $script:Sandbox 'restore.acl.json'
Save-BaknretSecuritySidecar -Path $script:RestoreSidecarPath -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$script:RestoreSidecar = Read-BaknretSecuritySidecar -Path $script:RestoreSidecarPath
Save-BakNRetSecuritySidecar -Path $script:RestoreSidecarPath -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$script:RestoreSidecar = Read-BakNRetSecuritySidecar -Path $script:RestoreSidecarPath
}
It '回放后根对象的安全描述符与源逐字节一致(protected / CO / 孤儿 SID 全在)' {
# 干净目标:只拷内容,不带 ACL(ACL 是新建对象的默认值)
& robocopy.exe $script:RestoreRoot $script:TargetRoot /E /COPY:DAT /NFL /NDL /NJH /NJS /NP | Out-Null
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' -TargetPath $script:TargetRoot
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' -TargetPath $script:TargetRoot
$result.Total | Should -Be 3
$result.Failed | Should -Be 0
$result.Applied | Should -Be 3
@@ -374,7 +374,7 @@ Describe '安全描述符回放' {
$targetPath = if ($relative -eq '.') { $script:TargetRoot } else { Join-Path $script:TargetRoot $relative }
# 唯一允许的差异:陈旧继承 ACE 被"冻结"成显式 + protected 的对象,
# protected 位会从 False 变 True(见 Get-BaknretSecuritySddlWithStale)。
# protected 位会从 False 变 True(见 Get-BakNRetSecuritySddlWithStale)。
$expected = $script:RestoreFingerprints[$relative] -replace ' P=(True|False) ', ' P='
$actual = (Get-AclFingerprint -Path $targetPath) -replace ' P=(True|False) ', ' P='
$actual | Should -Be $expected -Because "$relative 的 ACE 集合应当与源一致"
@@ -382,7 +382,7 @@ Describe '安全描述符回放' {
}
It '目标不存在或不是普通对象时记 Skipped,不记 Failed' {
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' `
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' `
-TargetPath (Join-Path $script:Sandbox 'restore\does-not-exist')
$result.Total | Should -Be 3
$result.Skipped | Should -Be 3
@@ -390,7 +390,7 @@ Describe '安全描述符回放' {
}
It '归档根名对不上时一条都不回放(不会把兄弟项的 ACL 倒过来)' {
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Other' -TargetPath $script:TargetRoot
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Other' -TargetPath $script:TargetRoot
$result.Total | Should -Be 0
$result.Applied | Should -Be 0
}
@@ -406,7 +406,7 @@ Describe '安全描述符回放' {
Records = @([pscustomobject]@{ p = 'Data'; k = 'd'; s = $sddl })
}
$result = Restore-BaknretSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result = Restore-BakNRetSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result.Failed | Should -Be 0
($result.Applied + $result.OwnerFailed) | Should -Be 1
(Get-Acl -LiteralPath $path).Sddl | Should -Match '\(A;;FA;;;SY\)'
@@ -416,7 +416,7 @@ Describe '安全描述符回放' {
$record = [pscustomobject]@{ p = 'Data'; k = 'd'; s = $null; e = '读不到' }
$sidecar = [pscustomobject]@{ Records = @($record) }
$path = Join-Path $script:Sandbox 'restore\bogus-group'
$result = Restore-BaknretSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result = Restore-BakNRetSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result.Skipped | Should -Be 1
$result.Applied | Should -Be 0
$result.Failed | Should -Be 0
@@ -432,7 +432,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
}
It '备份会写出 <归档名>.acl.json,并在 manifest 里记下它' {
$result = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -454,7 +454,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
Reset-AclTree -Path $script:Harness.SourcePath
(Test-Path -LiteralPath $script:Harness.SourcePath) | Should -BeFalse
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -476,7 +476,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
It '-SkipSecurity 时不回放(目标保持新建对象的默认 ACL)' {
Reset-AclTree -Path $script:Harness.SourcePath
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -491,7 +491,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
Reset-AclTree -Path $script:Harness.SourcePath
Get-ChildItem -LiteralPath $script:Harness.BackupDir -Filter '*.acl.json' | Remove-Item -Force
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
+173 -173
View File
@@ -54,7 +54,7 @@ BeforeAll {
# 于是这里改成:把整条命令写进临时 .cmd,由 cmd 自己把输出重定向到文件,
# 再用 Invoke-ExternalCommand 调 `cmd /c <批处理>`。退出码来自真实进程,
# 输出从文件读回,全程不经过 PowerShell 的管道。
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
param(
[Parameter(Mandatory = $true)][string]$Script,
[hashtable]$Parameters = @{}
@@ -337,29 +337,29 @@ Describe 'BackupList.txt 解析' {
}
It '记号切分:引号内的空白不切分,引号本身留在记号里' {
(@(Split-BaknretToken -Text 'A "B C" D') -join '|') | Should -Be 'A|"B C"|D'
(@(Split-BakNRetToken -Text 'A "B C" D') -join '|') | Should -Be 'A|"B C"|D'
# 引号不配对时按"引号延伸到行尾"处理,不抛异常
(@(Split-BaknretToken -Text 'A "B C') -join '|') | Should -Be 'A|"B C'
(@(Split-BaknretToken -Text '') -join '|') | Should -Be ''
(@(Split-BakNRetToken -Text 'A "B C') -join '|') | Should -Be 'A|"B C'
(@(Split-BakNRetToken -Text '') -join '|') | Should -Be ''
}
It '记号识别只认完整记号' {
(Test-BaknretMarker -Token '::') | Should -Be 'path'
(Test-BaknretMarker -Token ':-') | Should -Be 'exclude'
(Test-BaknretMarker -Token ':+') | Should -Be 'include'
(Test-BaknretMarker -Token ':encrypt') | Should -Be 'encrypt'
(Test-BaknretMarker -Token ':!encrypt') | Should -Be 'noencrypt'
(Test-BaknretMarker -Token '@') | Should -Be 'at'
(Test-BaknretMarker -Token 'C:\a:-b') | Should -BeNullOrEmpty
(Test-BaknretMarker -Token ':') | Should -BeNullOrEmpty
(Test-BakNRetMarker -Token '::') | Should -Be 'path'
(Test-BakNRetMarker -Token ':-') | Should -Be 'exclude'
(Test-BakNRetMarker -Token ':+') | Should -Be 'include'
(Test-BakNRetMarker -Token ':encrypt') | Should -Be 'encrypt'
(Test-BakNRetMarker -Token ':!encrypt') | Should -Be 'noencrypt'
(Test-BakNRetMarker -Token '@') | Should -Be 'at'
(Test-BakNRetMarker -Token 'C:\a:-b') | Should -BeNullOrEmpty
(Test-BakNRetMarker -Token ':') | Should -BeNullOrEmpty
}
It '去引号:成对才去,不成对原样返回' {
(Remove-BaknretQuote -Text '"a"') | Should -Be 'a'
(Remove-BaknretQuote -Text "'a'") | Should -Be 'a'
(Remove-BaknretQuote -Text 'a') | Should -Be 'a'
(Remove-BaknretQuote -Text '"a') | Should -Be '"a'
(Remove-BaknretQuote -Text '""') | Should -Be ''
(Remove-BakNRetQuote -Text '"a"') | Should -Be 'a'
(Remove-BakNRetQuote -Text "'a'") | Should -Be 'a'
(Remove-BakNRetQuote -Text 'a') | Should -Be 'a'
(Remove-BakNRetQuote -Text '"a') | Should -Be '"a'
(Remove-BakNRetQuote -Text '""') | Should -Be ''
}
}
@@ -382,49 +382,49 @@ Describe '归档命名' {
# 注意:Pester 的 It 名字里出现 $( ) 会被求值,所以标题里不要写子表达式。
It '基础命名规则:末级名_from_上级路径用加号连接' {
Get-BackupBaseName -RawPath 'C:\Programs\FooClolor' | Should -Be 'FooClolor_from_C_+Programs'
Get-BakNRetBackupBaseName -RawPath 'C:\Programs\FooClolor' | Should -Be 'FooClolor_from_C_+Programs'
}
It '/ 与 \ 以及重复分隔符结果一致' {
$a = Get-BackupBaseName -RawPath 'C:\a/b'
$b = Get-BackupBaseName -RawPath 'C:\\a\\\\b'
$a = Get-BakNRetBackupBaseName -RawPath 'C:\a/b'
$b = Get-BakNRetBackupBaseName -RawPath 'C:\\a\\\\b'
$a | Should -Be $b
}
It '命名与路径往返' {
$base = Get-BackupBaseName -RawPath 'D:\UserData\Documents\Aria'
Convert-BackupFileNameToPath -FileName ($base + '.7z') | Should -Be 'D:\UserData\Documents\Aria'
$base = Get-BakNRetBackupBaseName -RawPath 'D:\UserData\Documents\Aria'
Convert-BakNRetBackupFileNameToPath -FileName ($base + '.7z') | Should -Be 'D:\UserData\Documents\Aria'
}
It '归档名里不含非法文件名字符' {
$base = Get-BackupBaseName -RawPath 'C:\a<b>c|d?e*f'
$base = Get-BakNRetBackupBaseName -RawPath 'C:\a<b>c|d?e*f'
($base.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())) | Should -Be -1
}
It '%变量% 写法里的 % 会保留在归档名里' {
Get-BackupBaseName -RawPath '%UserProfile%\.ssh' | Should -Be '.ssh_from_%UserProfile%'
Get-BakNRetBackupBaseName -RawPath '%UserProfile%\.ssh' | Should -Be '.ssh_from_%UserProfile%'
}
It '软件名条目:默认用软件名做归档名' {
$entry = ConvertFrom-BackupListLine -Line 'my-app'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'my-app'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'my-app'
}
It '字面路径条目:仍用路径命名算法(现有清单无需改写)' {
$entry = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'FooClolor_from_C_+Programs'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'FooClolor_from_C_+Programs'
}
It '@pathname 用名录里的真实路径命名,而不是软件名' {
$entry = ConvertFrom-BackupListLine -Line 'my-app @pathname'
$expected = Get-BackupBaseName -RawPath $script:NamingApp
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be $expected
$expected = Get-BakNRetBackupBaseName -RawPath $script:NamingApp
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be $expected
}
It '名录里没有该名字:归档名退回可读目录名,并给出 Error' {
$entry = ConvertFrom-BackupListLine -Line 'no-such-thing'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'no-such-thing'
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3
$resolved.Error | Should -Not -BeNullOrEmpty
@($resolved.Items).Count | Should -Be 0
}
@@ -443,142 +443,142 @@ Describe '7z 排除参数翻译' {
function New-TestArchiveItem {
param([string]$ArchivePath, [string]$RealPath = 'C:\nonexistent-path')
New-BaknretArchiveItem -ArchivePath $ArchivePath -RealPath $RealPath -Kind 'path' -Origin 'path' -Exists $false -IsFile $false
New-BakNRetArchiveItem -ArchivePath $ArchivePath -RealPath $RealPath -Kind 'path' -Origin 'path' -Exists $false -IsFile $false
}
}
It '相对模式自动补上归档根目录名' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('logs\')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('logs\')
@($result.Arguments) | Should -Contain '-x!Foo\logs'
$result.Error | Should -BeNullOrEmpty
}
It '模式已带根名前缀时,Split-BaknretPatternScope 先摘掉前缀,结果不重复' {
It '模式已带根名前缀时,Split-BakNRetPatternScope 先摘掉前缀,结果不重复' {
$item = New-TestArchiveItem -ArchivePath 'Foo'
$map = Split-BaknretPatternScope -Items @($item) -Patterns @('Foo\logs\')
$map = Split-BakNRetPatternScope -Items @($item) -Patterns @('Foo\logs\')
(@($map[0]) -join '|') | Should -Be 'logs'
$result = Get-BaknretExcludeArgument -Item $item -Patterns @($map[0])
$result = Get-BakNRetExcludeArgument -Item $item -Patterns @($map[0])
@($result.Arguments) | Should -Contain '-x!Foo\logs'
@($result.Arguments) | Should -Not -Contain '-x!Foo\Foo\logs'
}
It '! 前缀翻译成递归组件匹配(-xr!)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache')
@($result.Arguments) | Should -Contain '-xr!*Cache'
}
It '模式里的空格转成 ?(归档项自己的名字按原样保留)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('Default\Code Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('Default\Code Cache')
@($result.Arguments) | Should -Contain '-x!User Data\Default\Code?Cache'
$plain = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('Default\Code Cache')
$plain = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('Default\Code Cache')
@($plain.Arguments) | Should -Contain '-x!Foo\Default\Code?Cache'
}
It '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache', 'Default\Code Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache', 'Default\Code Cache')
(@($result.Arguments) -join ' ') | Should -Not -Match '"'
}
It '空模式被忽略' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('', ' ', '!')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('', ' ', '!')
@($result.Arguments).Count | Should -Be 0
$result.Error | Should -BeNullOrEmpty
}
It '!re: 会把正则展开成精确的 -x! 参数' {
$item = New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree
$result = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^Cache$')
$result = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^Cache$')
@($result.Arguments) | Should -Contain '-x!Top\sub\Cache'
# 命中目录后不再往下走(一个命中只产生一条参数,避免命令行爆炸)
$pruned = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^sub$')
$pruned = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^sub$')
@($pruned.Arguments) | Should -Contain '-x!Top\sub'
@($pruned.Arguments).Count | Should -Be 1
}
It '非法正则给出 Error,而不是抛异常' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) -Patterns @('!re:[')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) -Patterns @('!re:[')
@($result.Arguments).Count | Should -Be 0
$result.Error | Should -Not -BeNullOrEmpty
}
It '正则命中数超过上限时明确报错' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) `
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) `
-Patterns @('!re:.') -MaxRegexMatches 1
$result.Error | Should -Not -BeNullOrEmpty
}
It '参数总长超过安全上限时明确报错' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top') `
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top') `
-Patterns @('!*zzz') -MaxCommandLineChars 5
$result.Error | Should -Not -BeNullOrEmpty
}
It 'Split-BaknretPatternScope:指名 Slot 的模式只分给那个 Slot' {
It 'Split-BakNRetPatternScope:指名 Slot 的模式只分给那个 Slot' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('Alpha\logs\')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('Alpha\logs\')
(@($map[0]) -join '|') | Should -Be 'logs'
@($map[1]).Count | Should -Be 0
# 用 / 分隔也一样
$slash = Split-BaknretPatternScope -Items $items -Patterns @('Beta/sub')
$slash = Split-BakNRetPatternScope -Items $items -Patterns @('Beta/sub')
(@($slash[1]) -join '|') | Should -Be 'sub'
@($slash[0]).Count | Should -Be 0
}
It 'Split-BaknretPatternScope:没点名任何 Slot 的模式广播给每一项' {
It 'Split-BakNRetPatternScope:没点名任何 Slot 的模式广播给每一项' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('plain\path', 'Alpha')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('plain\path', 'Alpha')
(@($map[0]) -join '|') | Should -Be 'plain\path|Alpha'
(@($map[1]) -join '|') | Should -Be 'plain\path|Alpha'
}
It 'Split-BaknretPatternScope:! 与 !re: 模式广播给每一项' {
It 'Split-BakNRetPatternScope:! 与 !re: 模式广播给每一项' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('!*Cache', '!re:^x$')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('!*Cache', '!re:^x$')
(@($map[0]) -join '|') | Should -Be '!*Cache|!re:^x$'
(@($map[1]) -join '|') | Should -Be '!*Cache|!re:^x$'
}
It 'Split-BaknretPatternScope:每个归档项都有键(没有模式时是空数组)' {
It 'Split-BakNRetPatternScope:每个归档项都有键(没有模式时是空数组)' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @()
$map = Split-BakNRetPatternScope -Items $items -Patterns @()
$map.ContainsKey(0) | Should -BeTrue
$map.ContainsKey(1) | Should -BeTrue
@($map[0]).Count | Should -Be 0
}
It 'Merge-BaknretExcludeArgument 去重且保持顺序' {
$merged = Merge-BaknretExcludeArgument -ArgumentLists @(@('-x!a', '-x!b'), @('-x!b', '-x!c'))
It 'Merge-BakNRetExcludeArgument 去重且保持顺序' {
$merged = Merge-BakNRetExcludeArgument -ArgumentLists @(@('-x!a', '-x!b'), @('-x!b', '-x!c'))
(@($merged) -join ' ') | Should -Be '-x!a -x!b -x!c'
}
It '多 Slot 条目:逐项分配 + 翻译 + 去重合成一份参数' {
$items = @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Alpha' -Origin 'catalog' -Exists $true
New-BaknretArchiveItem -ArchivePath 'Beta' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Beta' -Origin 'catalog' -Exists $true
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Alpha' -Origin 'catalog' -Exists $true
New-BakNRetArchiveItem -ArchivePath 'Beta' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Beta' -Origin 'catalog' -Exists $true
)
$patterns = @('Alpha\logs\', '!*Cache', 'Beta\sub', '!*Cache')
$map = Split-BaknretPatternScope -Items $items -Patterns $patterns
$map = Split-BakNRetPatternScope -Items $items -Patterns $patterns
$lists = @()
for ($index = 0; $index -lt $items.Count; $index++) {
$lists += , @((Get-BaknretExcludeArgument -Item $items[$index] -Patterns @($map[$index])).Arguments)
$lists += , @((Get-BakNRetExcludeArgument -Item $items[$index] -Patterns @($map[$index])).Arguments)
}
$effective = @(Merge-BaknretExcludeArgument -ArgumentLists $lists)
$effective = @(Merge-BakNRetExcludeArgument -ArgumentLists $lists)
$effective | Should -Contain '-x!Alpha\logs'
$effective | Should -Contain '-x!Beta\sub'
$effective | Should -Contain '-xr!*Cache'
@@ -600,8 +600,8 @@ Describe '归档项与暂存目录' {
Set-Content -Encoding UTF8 -LiteralPath $script:StagingFile 'file-content'
}
It 'New-BaknretArchiveItem 规范化归档内路径并算出 TopName' {
$item = New-BaknretArchiveItem -ArchivePath '\Alpha\Sub\' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha'
It 'New-BakNRetArchiveItem 规范化归档内路径并算出 TopName' {
$item = New-BakNRetArchiveItem -ArchivePath '\Alpha\Sub\' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha'
$item.ArchivePath | Should -Be 'Alpha\Sub'
$item.TopName | Should -Be 'Alpha'
$item.Kind | Should -Be 'slot'
@@ -610,10 +610,10 @@ Describe '归档项与暂存目录' {
@($item.Exclude).Count | Should -Be 0
}
It 'Get-BaknretArchiveTopName 取归档内相对路径的第一段' {
(Get-BaknretArchiveTopName -ArchivePath 'A\B\c.txt') | Should -Be 'A'
(Get-BaknretArchiveTopName -ArchivePath ' A ') | Should -Be 'A'
(Get-BaknretArchiveTopName -ArchivePath '') | Should -Be ''
It 'Get-BakNRetArchiveTopName 取归档内相对路径的第一段' {
(Get-BakNRetArchiveTopName -ArchivePath 'A\B\c.txt') | Should -Be 'A'
(Get-BakNRetArchiveTopName -ArchivePath ' A ') | Should -Be 'A'
(Get-BakNRetArchiveTopName -ArchivePath '') | Should -Be ''
}
It '暂存半途失败会自己清干净(不留指向真实数据的 junction)' {
@@ -628,12 +628,12 @@ Describe '归档项与暂存目录' {
$items = @(
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true
New-BakNRetArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true
)
{ New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') } | Should -Throw
{ New-BakNRetArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') } | Should -Throw
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
@($left | Where-Object { $_.LinkType -eq 'Junction' }).Count | Should -Be 0
@@ -644,12 +644,12 @@ Describe '归档项与暂存目录' {
It '目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径)' {
$items = @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
New-BaknretArchiveItem -ArchivePath 'Nested\Deep' -RealPath $script:StagingDir -Kind 'include' -Origin 'include' -Exists $true -IsFile $false
New-BaknretArchiveItem -ArchivePath 'Cfg' -RealPath $script:StagingFile -Kind 'slot' -Slot 'Cfg' -Exists $true -IsFile $true
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Nested\Deep' -RealPath $script:StagingDir -Kind 'include' -Origin 'include' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Cfg' -RealPath $script:StagingFile -Kind 'slot' -Slot 'Cfg' -Exists $true -IsFile $true
)
$stage = New-BaknretArchiveStaging -Items $items
$stage = New-BakNRetArchiveStaging -Items $items
try {
$alpha = Get-Item -LiteralPath (Join-Path $stage 'Alpha') -Force
$alpha.LinkType | Should -Be 'Junction'
@@ -663,15 +663,15 @@ Describe '归档项与暂存目录' {
(Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content'
}
finally {
Remove-BaknretArchiveStaging -Root $stage
Remove-BakNRetArchiveStaging -Root $stage
}
}
It 'Remove-BaknretArchiveStaging 只删连接点,不顺着走进真实目录' {
$stage = New-BaknretArchiveStaging -Items @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
It 'Remove-BakNRetArchiveStaging 只删连接点,不顺着走进真实目录' {
$stage = New-BakNRetArchiveStaging -Items @(
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
)
Remove-BaknretArchiveStaging -Root $stage
Remove-BakNRetArchiveStaging -Root $stage
Test-Path -LiteralPath $stage | Should -BeFalse
Test-Path -LiteralPath (Join-Path $script:StagingDir 'inner\a.txt') | Should -BeTrue
@@ -683,23 +683,23 @@ Describe '命令行参数拼接' {
# ============================================================================
It '无空格参数原样输出' {
ConvertTo-NativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
}
It '含空格参数加引号' {
ConvertTo-NativeArgumentString -ArgumentList @('C:\a b\c') | Should -Be '"C:\a b\c"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\a b\c') | Should -Be '"C:\a b\c"'
}
It '引号内的结尾反斜杠翻倍(否则会被当成转义引号)' {
ConvertTo-NativeArgumentString -ArgumentList @('C:\a b\') | Should -Be '"C:\a b\\"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\a b\') | Should -Be '"C:\a b\\"'
}
It '内部引号被转义' {
ConvertTo-NativeArgumentString -ArgumentList @('a"b c') | Should -Be '"a\"b c"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a"b c') | Should -Be '"a\"b c"'
}
It '空参数输出一对空引号' {
ConvertTo-NativeArgumentString -ArgumentList @('') | Should -Be '""'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('') | Should -Be '""'
}
}
@@ -709,11 +709,11 @@ Describe 'manifest 与配置' {
It 'manifest 读写往返' {
$path = Join-Path $script:Sandbox 'roundtrip\manifest.json'
$manifest = Read-BaknretManifest -Path $path
$manifest = Read-BakNRetManifest -Path $path
$manifest.items['demo'] = [pscustomobject]@{ archive = 'demo.7z'; action = 'backed-up'; verified = $true }
Write-BaknretManifest -Path $path -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $path -Manifest $manifest | Out-Null
$again = Read-BaknretManifest -Path $path
$again = Read-BakNRetManifest -Path $path
$again.items.Count | Should -Be 1
$again.items['demo'].archive | Should -Be 'demo.7z'
$again.items['demo'].verified | Should -BeTrue
@@ -721,7 +721,7 @@ Describe 'manifest 与配置' {
It 'manifest 原样保存 layouts(name/kind),全新恢复时靠它判断目录还是文件' {
$path = Join-Path $script:Sandbox 'layouts\manifest.json'
$manifest = Read-BaknretManifest -Path $path
$manifest = Read-BakNRetManifest -Path $path
$manifest.items['app'] = [pscustomobject]@{
archive = 'app.7z'
layouts = @(
@@ -729,28 +729,28 @@ Describe 'manifest 与配置' {
[pscustomobject]@{ name = 'Cfg'; kind = 'file' }
)
}
Write-BaknretManifest -Path $path -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $path -Manifest $manifest | Out-Null
$again = Read-BaknretManifest -Path $path
$again = Read-BakNRetManifest -Path $path
@($again.items['app'].layouts).Count | Should -Be 2
(@($again.items['app'].layouts | ForEach-Object { $_.name + ':' + $_.kind }) -join '|') | Should -Be 'Data:dir|Cfg:file'
}
It 'manifest 损坏时不抛异常,而是重建空清单' {
$path = Write-ListFile -Path (Join-Path $script:Sandbox 'broken.json') -Content '{ this is not json'
{ $script:broken = Read-BaknretManifest -Path $path } | Should -Not -Throw
{ $script:broken = Read-BakNRetManifest -Path $path } | Should -Not -Throw
$script:broken.items.Count | Should -Be 0
}
It '配置缺失时返回默认值' {
$config = Get-BaknretConfig -Path (Join-Path $script:Sandbox 'no-such-config.psd1')
$config = Get-BakNRetConfig -Path (Join-Path $script:Sandbox 'no-such-config.psd1')
$config.BackupDir | Should -Be 'Backups'
$config.VerifyArchive | Should -BeTrue
}
It '配置嵌套段落合并且不丢默认键' {
$path = Write-ListFile -Path (Join-Path $script:Sandbox 'cfg.psd1') -Content "@{ MinFreeSpaceGB = 3; Encryption = @{ Enabled = `$true } }"
$config = Get-BaknretConfig -Path $path
$config = Get-BakNRetConfig -Path $path
$config.MinFreeSpaceGB | Should -Be 3
$config.Encryption.Enabled | Should -BeTrue
# 没写到的子键要保留默认值。Encryption 是哈希表,用 ContainsKey 查。
@@ -761,9 +761,9 @@ Describe 'manifest 与配置' {
$saved = $env:BAKNRET_PASSWORD
try {
$env:BAKNRET_PASSWORD = 'sekrit'
(Get-BaknretPassword) | Should -Be 'sekrit'
(Get-BakNRetPassword) | Should -Be 'sekrit'
$env:BAKNRET_PASSWORD = $null
(Get-BaknretPassword) | Should -BeNullOrEmpty
(Get-BakNRetPassword) | Should -BeNullOrEmpty
}
finally {
$env:BAKNRET_PASSWORD = $saved
@@ -827,7 +827,7 @@ Describe '软件名录' {
}
It '名录解析:条目与 Slot 的字段集合就是新契约' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$entry = $catalog['single']
(@($entry.PSObject.Properties.Name) -join ',') | Should -Be 'Name,Path,Description,Slots,Kind,Missing,Error,Raw'
$entry.Name | Should -Be 'single'
@@ -849,7 +849,7 @@ Describe '软件名录' {
}
It '一个软件多个 Slot:Kind=Multi,Slot 按名字排序' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['multi'].Kind | Should -Be 'Multi'
@($catalog['multi'].Slots).Count | Should -Be 2
(@($catalog['multi'].Slots | ForEach-Object { $_.Name }) -join ',') | Should -Be 'Alpha,Zeta'
@@ -858,7 +858,7 @@ Describe '软件名录' {
}
It 'Kind:Single / Multi / Partial / Unresolved / Invalid' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['single'].Kind | Should -Be 'Single'
$catalog['multi'].Kind | Should -Be 'Multi'
$catalog['partial'].Kind | Should -Be 'Partial'
@@ -873,7 +873,7 @@ Describe '软件名录' {
}
It '前缀补全:<名>_<后缀> 与 <名>-<后缀> 都会被补全' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['legendary'].Slots[0].Resolved | Should -Be (Join-Path $script:CatRoot 'legendary_2.0.4')
$catalog['legendary'].Slots[0].Suffixed | Should -BeTrue
@@ -882,12 +882,12 @@ Describe '软件名录' {
}
It '不会把 Legendary 误配成 LegendarySomething' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['legendary'].Slots[0].Resolved | Should -Not -Be (Join-Path $script:CatRoot 'LegendarySomething')
}
It '一个 Slot 命中多个候选目录:报 Error,绝不悄悄挑一棵树' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$entry = $catalog['dupslot']
$entry.Error | Should -Match '只能对应一个目录'
$entry.Error | Should -Match 'dup_1'
@@ -897,14 +897,14 @@ Describe '软件名录' {
}
It '文件 Slot:Path 指向文件时 IsFile 为真,Encrypt 也带上' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$slot = $catalog['fileslot'].Slots[0]
$slot.IsFile | Should -BeTrue
$slot.Encrypt | Should -BeTrue
}
It '%变量% 会在名录路径里展开' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['envpath'].Slots[0].Resolved | Should -Be ([Environment]::ExpandEnvironmentVariables('%TEMP%'))
}
@@ -916,39 +916,39 @@ Describe '软件名录' {
}
It '读取结果按"路径 + 时间戳 + 长度 + 内容 MD5"缓存' {
$first = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3
$second = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3
$first = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3
$second = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3
[object]::ReferenceEquals($first, $second) | Should -BeTrue
$fresh = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$fresh = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
[object]::ReferenceEquals($first, $fresh) | Should -BeFalse
}
It '名录文件内容变了以后缓存自动失效' {
$path = Write-ListFile -Path (Join-Path $script:CatRoot 'cache.psd1') -Content "@{ 'x' = @{ S = @{ Path = '$script:CatReal'; Description = 'one' } } }"
(Get-SoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'one'
(Get-BakNRetSoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'one'
Write-ListFile -Path $path -Content "@{ 'x' = @{ S = @{ Path = '$script:CatReal'; Description = 'two' } } }" | Out-Null
(Get-SoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'two'
(Get-BakNRetSoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'two'
}
It 'Includes:分文件维护的名录会被合并' {
Write-ListFile -Path (Join-Path $script:CatRoot 'Extra.psd1') -Content "@{ 'extra-app' = @{ S = @{ Path = '$script:CatReal' } } }" | Out-Null
$main = Write-ListFile -Path (Join-Path $script:CatRoot 'Main.psd1') -Content "@{ Includes = @('Extra.psd1'); 'main-app' = @{ S = @{ Path = '$script:CatReal' } } }"
$catalog = Get-SoftwareCatalog -Path $main -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $main -MaxDepth 3 -NoCache
$catalog.ContainsKey('main-app') | Should -BeTrue
$catalog.ContainsKey('extra-app') | Should -BeTrue
}
It '旧的裸字符串 / 字符串数组 / 对象数组写法都会报 ERROR 并被跳过' {
$catalog = Get-SoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog.ContainsKey('bare') | Should -BeFalse
$catalog.ContainsKey('arr') | Should -BeFalse
$catalog.ContainsKey('objarr') | Should -BeFalse
}
It '旧的 @{ Dirs = @(...) } 写法不再展开:留下 Invalid 条目和原因' {
$catalog = Get-SoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog.ContainsKey('dirstyle') | Should -BeTrue
$catalog['dirstyle'].Kind | Should -Be 'Invalid'
$catalog['dirstyle'].Error | Should -Not -BeNullOrEmpty
@@ -956,14 +956,14 @@ Describe '软件名录' {
}
It '软件名会被规范化成合法文件名' {
$clean = Format-CatalogName -Name 'a<b>c:d/e'
$clean = Format-BakNRetCatalogName -Name 'a<b>c:d/e'
($clean.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())) | Should -Be -1
(Format-CatalogName -Name ' My App ') | Should -Be 'My App'
(Format-BakNRetCatalogName -Name ' My App ') | Should -Be 'My App'
}
}
# ============================================================================
Describe 'Resolve-BackupEntry:条目解析' {
Describe 'Resolve-BakNRetBackupEntry:条目解析' {
# ============================================================================
BeforeAll {
@@ -976,7 +976,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It 'resolve 与归档项的字段集合就是新契约(旧字段已删除)' {
$entry = ConvertFrom-BackupListLine -Line 'single'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
(@($resolved.PSObject.Properties.Name) -join ',') | Should -Be 'IsName,CatalogEntry,BaseName,ArchiveFlavor,Direction,Items,Encrypt,ExcludePatterns,HasExcludeOverride,Includes,HasIncludeOverride,Source,Error,Blocking'
@($resolved.Items)[0].PSObject.Properties.Name | Should -Contain 'ArchivePath'
@($resolved.Items)[0].PSObject.Properties.Name | Should -Contain 'Origin'
@@ -985,7 +985,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '软件名条目:isName/CatalogEntry/BaseName/ArchiveFlavor/Source' {
$entry = ConvertFrom-BackupListLine -Line 'single'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.IsName | Should -BeTrue
$resolved.BaseName | Should -Be 'single'
$resolved.ArchiveFlavor | Should -Be 'name'
@@ -997,7 +997,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '软件名条目:Items 来自 Slot(ArchivePath=Slot 名,Kind=slot,Origin=catalog)' {
$entry = ConvertFrom-BackupListLine -Line 'multi'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'Alpha,Zeta'
(@($resolved.Items | ForEach-Object { $_.Kind }) -join ',') | Should -Be 'slot,slot'
@@ -1009,7 +1009,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '字面路径条目:一个 path 项(ArchivePath 是末级名)' {
$entry = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.IsName | Should -BeFalse
$resolved.ArchiveFlavor | Should -Be 'path'
@($resolved.Items).Count | Should -Be 1
@@ -1021,7 +1021,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '字面路径条目上的 @ Path= 覆盖目标路径,但归档名仍按原路径' {
$entry = ConvertFrom-BackupListLine -Line "C:\Programs\Foo @ Path='D:\Elsewhere'"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Items[0].RealPath | Should -Be 'D:\Elsewhere'
$resolved.BaseName | Should -Be 'Foo_from_C_+Programs'
}
@@ -1029,7 +1029,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '名录里的路径不存在时仍给出 Items(恢复要靠它还原回原位)' {
$missingCatalog = Write-ListFile -Path (Join-Path $script:Sandbox 'catalog\Missing.psd1') -Content "@{ 'gone' = @{ S = @{ Path = 'C:\definitely-not-here-98765' } } }"
$entry = ConvertFrom-BackupListLine -Line 'gone'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $missingCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $missingCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].Exists | Should -BeFalse
$resolved.Items[0].RealPath | Should -Be 'C:\definitely-not-here-98765'
@@ -1037,7 +1037,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It ':: 覆盖 Path:单 Slot 条目直接生效' {
$entry = ConvertFrom-BackupListLine -Line "single :: $script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].ArchivePath | Should -Be 'Main'
$resolved.Items[0].RealPath | Should -Be $script:CatOther
@@ -1047,7 +1047,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It ':: / @ Path= 覆盖遇到多 Slot 条目 -> Blocking(不猜是哪一个)' {
foreach ($line in @("multi :: $script:CatOther", "multi @ Path='$script:CatOther'")) {
$entry = ConvertFrom-BackupListLine -Line $line
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 0
$resolved.Blocking | Should -Match '不能用一个'
}
@@ -1055,14 +1055,14 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '条目级 :- 覆盖名录里的排除:HasExcludeOverride 为真' {
$entry = ConvertFrom-BackupListLine -Line 'single :- logs\,!*Cache'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.HasExcludeOverride | Should -BeTrue
(@($resolved.ExcludePatterns) -join '|') | Should -Be 'logs\|!*Cache'
}
It '名录 Slot 的 Include 会追加成 include 项' {
$entry = ConvertFrom-BackupListLine -Line 'incapp'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
$include = @($resolved.Items | Where-Object { $_.Origin -eq 'include' })
$include.Count | Should -Be 1
@@ -1074,7 +1074,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '条目级 :+ / @ Include= 覆盖名录里的 Include' {
foreach ($line in @("incapp :+ Other:$script:CatOther", "incapp @ Include='Other:$script:CatOther'")) {
$entry = ConvertFrom-BackupListLine -Line $line
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.HasIncludeOverride | Should -BeTrue
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'A,Other'
@($resolved.Items)[1].RealPath | Should -Be $script:CatOther
@@ -1082,34 +1082,34 @@ Describe 'Resolve-BackupEntry:条目解析' {
}
It '加密:名录里各 Slot 取或;条目级 :encrypt / :!encrypt 覆盖' {
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp :!encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single :encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line "single @ Encrypt='true'") -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp :!encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single :encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line "single @ Encrypt='true'") -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
}
It '方向标记会传递到 Resolve-BackupEntry.Direction' {
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'backup'
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '- single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'restore'
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'both'
It '方向标记会传递到 Resolve-BakNRetBackupEntry.Direction' {
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'backup'
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '- single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'restore'
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'both'
}
It '归档内路径冲突(include 与 Slot 同名)-> Blocking' {
$entry = ConvertFrom-BackupListLine -Line "conflict :+ Data:$script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Blocking | Should -Match '归档内路径冲突'
}
It '归档内路径冲突(父子关系)-> Blocking' {
$entry = ConvertFrom-BackupListLine -Line "conflict :+ Data\sub:$script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Blocking | Should -Match '父子关系'
}
It '名录里没有该名字:Error 给出,Items 为空' {
$entry = ConvertFrom-BackupListLine -Line 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Error | Should -Match 'no-such-thing'
@($resolved.Items).Count | Should -Be 0
}
@@ -1125,13 +1125,13 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$sentinel = 'SENTINEL-PW-9f3a'
$logPath = $null
try {
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
Set-BaknretDebug
$logPath = Start-BakNRetLog -Directory $sandbox -Prefix 'secret'
Set-BakNRetDebug
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
}
finally {
Set-BaknretDebug -Enabled:$false
Stop-BaknretLog
Set-BakNRetDebug -Enabled:$false
Stop-BakNRetLog
}
$logText = Get-Content -Encoding UTF8 -LiteralPath $logPath -Raw
@@ -1150,21 +1150,21 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$first = $null
$second = $null
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
$first | Should -Not -BeNullOrEmpty
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
$second | Should -BeNullOrEmpty
Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue
Test-Path -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) | Should -BeTrue
}
finally {
Exit-BaknretRunLock -Lock $second
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $second
Exit-BakNRetRunLock -Lock $first
}
# 锁文件是独占打开的(FileShare.None),内容只能在释放之后读
(Get-Content -Encoding UTF8 -LiteralPath (Get-BaknretRunLockPath -Directory $dir) -Raw) |
(Get-Content -Encoding UTF8 -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) -Raw) |
Should -Match "pid=$PID"
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1173,13 +1173,13 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
It '运行锁:释放之后可以重新取得' {
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
$first | Should -Not -BeNullOrEmpty
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $first
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
$second | Should -Not -BeNullOrEmpty
Exit-BaknretRunLock -Lock $second
Exit-BakNRetRunLock -Lock $second
}
finally {
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1192,7 +1192,7 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$empty = Join-Path $env:TEMP ("bnr-empty-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $empty -Force | Out-Null
try {
$summary = Get-FolderSummary -FolderPath $empty
$summary = Get-BakNRetFolderSummary -FolderPath $empty
$summary.FileCount | Should -Be 0
$summary.TotalSize | Should -Not -BeNullOrEmpty
$summary.TotalSize | Should -Be 0
@@ -1207,18 +1207,18 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
New-Item -ItemType Directory -Path $base -Force | Out-Null
$target = Join-Path $base 'text.json'
try {
Write-BaknretAtomicText -Path $target -Text 'FIRST' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'FIRST' | Out-Null
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'FIRST'
# 目标已存在时走 File.Replace
Write-BaknretAtomicText -Path $target -Text 'SECOND' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'SECOND' | Out-Null
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND'
"$target.tmp" | Should -Not -Exist
# 目标只读 -> 替换必然失败 -> 旧内容必须还在。这正是 File.Replace 与"先删后移"
# 的区别:后者失败时旧文件已经没了,而 manifest 丢了只能靠文件名反推。
(Get-Item -LiteralPath $target).IsReadOnly = $true
{ Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' } | Should -Throw
{ Write-BakNRetAtomicText -Path $target -Text 'SHOULD-NOT-LAND' } | Should -Throw
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND'
}
finally {
@@ -1260,9 +1260,9 @@ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSeven
It '排除规则与空格处理在真实归档上生效' {
$patterns = @('!*Cache', 'component_crx_cache', 'Default\Code Cache', 'Default\Extensions', 'Default\IndexedDB')
$item = New-BaknretArchiveItem -ArchivePath $script:IntegrationItem -RealPath $script:IntegrationPath `
$item = New-BakNRetArchiveItem -ArchivePath $script:IntegrationItem -RealPath $script:IntegrationPath `
-Kind 'path' -Origin 'path' -Exists $true -IsFile $false
$excludeArguments = @((Get-BaknretExcludeArgument -Item $item -Patterns $patterns).Arguments)
$excludeArguments = @((Get-BakNRetExcludeArgument -Item $item -Patterns $patterns).Arguments)
$archive = Join-Path $script:IntegrationRoot 'excl.7z'
$arguments = @('a', '-t7z', '-mx=1', '-bso0', '-bsp0') + $excludeArguments + @($archive, $script:IntegrationItem)
@@ -1316,14 +1316,14 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$script:E2EHashes = New-VerifiableSourceTree -Root $script:E2ESource
Write-ListFile -Path $script:E2EList -Content "# e2e`n$script:E2ESource :- logs\,!*Cache`n" | Out-Null
$script:BackupRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:BackupRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Force = $true
QuietTool = $true
}
$script:E2EManifestPath = Join-Path $script:E2EBackupDir 'manifest.json'
$script:E2EManifest = Read-BaknretManifest -Path $script:E2EManifestPath
$script:E2EManifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$script:E2EArchive = @(Get-ChildItem -LiteralPath $script:E2EBackupDir -File -Filter *.7z)[0]
}
@@ -1352,7 +1352,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '字面路径条目沿用 <末级名> 布局;归档里保留应保留内容、不含被排除项' {
(Get-BaknretArchiveTopName -ArchivePath 'My Code Space\keep.txt') | Should -Be 'My Code Space'
(Get-BakNRetArchiveTopName -ArchivePath 'My Code Space\keep.txt') | Should -Be 'My Code Space'
$verify = Join-Path $script:E2ERoot 'verify'
New-Item -ItemType Directory -Path $verify -Force | Out-Null
@@ -1370,7 +1370,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$record.roots | Should -Contain 'My Code Space'
# 和归档里的真实内容对一次账,而不是只信 manifest 自己
$topLevel = @(Get-ArchiveTopLevelNames -ArchivePath $script:E2EArchive.FullName -SevenZip $script:SevenZip)
$topLevel = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:E2EArchive.FullName -SevenZip $script:SevenZip)
(@($topLevel | Sort-Object) -join ',') | Should -Be 'My Code Space'
}
@@ -1378,7 +1378,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$before = (Get-FileHash -LiteralPath $script:E2EManifestPath -Algorithm SHA256).Hash
Remove-Item -LiteralPath $script:E2ESource -Recurse -Force
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
DryRun = $true
@@ -1390,7 +1390,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It 'Restore -WhatIf 同样不写盘' {
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
WhatIf = $true
@@ -1402,7 +1402,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '真实恢复:退出码 0,文件逐字节一致,被排除项没有被恢复出来' {
Test-Path -LiteralPath $script:E2ESource | Should -BeFalse # 保证不是空操作
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Force = $true
@@ -1420,7 +1420,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '真实恢复之后 manifest 才被更新(lastRestoreAt)' {
$manifest = Read-BaknretManifest -Path $script:E2EManifestPath
$manifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$record = $manifest.items[$script:E2EArchive.BaseName]
$record.lastRestoreAt | Should -Not -BeNullOrEmpty
}
@@ -1428,7 +1428,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '孤儿归档会被点名报告,但不影响退出码' {
Copy-Item -LiteralPath $script:E2EArchive.FullName -Destination (Join-Path $script:E2EBackupDir 'Orphaned-Archive.7z') -Force
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
QuietTool = $true
@@ -1440,7 +1440,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '带 -Only 时不做孤儿审计(避免把未选中的归档误报成孤儿)' {
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Only = @('My Code Space')
@@ -1451,20 +1451,20 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '源路径不存在时记为 missing-source,退出码仍为 0(跳过不算失败)' {
$missingList = Write-ListFile -Path (Join-Path $script:E2ERoot 'missing.txt') -Content "Z:\definitely-not-here-12345`n"
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $missingList
BackupDir = $script:E2EBackupDir
QuietTool = $true
}
$run.ExitCode | Should -Be 0
$manifest = Read-BaknretManifest -Path $script:E2EManifestPath
$manifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$manifest.items['definitely-not-here-12345_from_Z_'].action | Should -Be 'missing-source'
}
It '归档名重复时直接报失败(退出码 1),不静默互相覆盖' {
$duplicateList = Write-ListFile -Path (Join-Path $script:E2ERoot 'dup.txt') -Content "$script:E2ESource`n$script:E2ESource`n"
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $duplicateList
BackupDir = $script:E2EBackupDir
Force = $true
@@ -1493,7 +1493,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 正确方向:先正常备份出归档
Write-ListFile -Path $script:DirectionList -Content "$script:DirectionSource`n" | Out-Null
$null = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$null = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
Force = $true
@@ -1507,7 +1507,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 行首 -:仅恢复,备份端应当跳过它,但要把它算作"有主"
Write-ListFile -Path $script:DirectionList -Content "- $script:DirectionSource`n" | Out-Null
$script:BackupDirectionRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:BackupDirectionRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
QuietTool = $true
@@ -1516,7 +1516,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 行首 +:仅备份,恢复端应当跳过它,同样要登记归档名
Remove-Item -LiteralPath $script:DirectionSource -Recurse -Force
Write-ListFile -Path $script:DirectionList -Content "+ $script:DirectionSource`n" | Out-Null
$script:RestoreDirectionRun = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$script:RestoreDirectionRun = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
Force = $true
+4 -4
View File
@@ -82,8 +82,8 @@ if (-not (Test-Path -LiteralPath $restoreScript)) {
exit 1
}
$config = Get-BaknretConfig -Path $ConfigPath
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$config = Get-BakNRetConfig -Path $ConfigPath
$catalogPath = Resolve-BakNRetCatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
if (-not $BackupDir) {
$BackupDir = $config.BackupDir
@@ -100,7 +100,7 @@ if (-not $WorkRoot) {
}
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
$manifest = Read-BaknretManifest -Path (Join-Path $BackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $BackupDir 'manifest.json')
$archiveFiles = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') })
@@ -322,7 +322,7 @@ foreach ($name in $Entries) {
$entry = ConvertFrom-BackupListLine -Line $name
if (-not $entry) { continue }
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
if (-not $resolved.BaseName) {
$failures += "$name :解析不出归档名"
+10 -10
View File
@@ -167,7 +167,7 @@ $manifestPath = Join-Path $backupDir 'manifest.json'
Test-Case 'manifest 记录了条目、动作、校验结果、roots 与 layouts' {
Assert-FileExists $manifestPath
$manifest = Read-BaknretManifest -Path $manifestPath
$manifest = Read-BakNRetManifest -Path $manifestPath
Assert-Equal 1 $manifest.items.Count
$record = $manifest.items[$archives[0].BaseName]
@@ -251,7 +251,7 @@ $cleanExitCode = $LASTEXITCODE
$cleanArchive = Get-ChildItem -LiteralPath $lockBackupDir -File -Filter *.7z | Select-Object -First 1
$cleanSize = $cleanArchive.Length
$lockManifestPath = Join-Path $lockBackupDir 'manifest.json'
$cleanRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$cleanArchive.BaseName]
$cleanRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$cleanArchive.BaseName]
Test-Case '干净归档:退出码 0,manifest 记 warnings=false' {
Assert-Equal 0 $cleanExitCode
@@ -269,7 +269,7 @@ try {
& $backupScript -BackupListPath $lockList -BackupDir $lockBackupDir -ConfigPath $cfg2 -Force -QuietTool
$warnExitCode = $LASTEXITCODE
$afterArchive = Get-ChildItem -LiteralPath $lockBackupDir -File -Filter *.7z | Select-Object -First 1
$afterRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
$afterRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
Test-Case '有警告时拒绝覆盖完整归档:退出码 1,旧归档原样保留' {
Assert-Equal 1 $warnExitCode
@@ -282,7 +282,7 @@ try {
# 明确接受之后才允许覆盖
& $backupScript -BackupListPath $lockList -BackupDir $lockBackupDir -ConfigPath $cfg2 -Force -QuietTool -AcceptWarnings
$acceptExitCode = $LASTEXITCODE
$acceptedRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
$acceptedRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
Test-Case '-AcceptWarnings 允许覆盖,并在 manifest 记 warnings=true' {
Assert-Equal 0 $acceptExitCode
@@ -357,7 +357,7 @@ Test-Case '名录条目:一个软件一个归档,归档名 = 软件名;独
}
Test-Case 'manifest.roots 列出各归档项的顶层名,layouts 标出 dir / file' {
$record = (Read-BaknretManifest -Path (Join-Path $catBackupDir 'manifest.json')).items['my-app']
$record = (Read-BakNRetManifest -Path (Join-Path $catBackupDir 'manifest.json')).items['my-app']
Assert-True ($null -ne $record)
$roots = @($record.roots | Sort-Object)
Assert-Equal 3 $roots.Count
@@ -438,7 +438,7 @@ Test-Case '@pathname 覆盖:用名录里的真实路径跑命名算法(独
[System.IO.File]::WriteAllText($pathList, "my-app @pathname :+ Modules:$settingsFile`n", [System.Text.UTF8Encoding]::new($true))
# settings.json 刚才被删了,重建一份,让 Include 的宿主机路径存在
Set-Content -LiteralPath $settingsFile -Value '{"slot":"file"}' -Encoding UTF8
$expectedBase = Get-BackupBaseName -RawPath $dirA
$expectedBase = Get-BakNRetBackupBaseName -RawPath $dirA
& $backupScript -BackupListPath $pathList -BackupDir $pathNameDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $pathNameDir ($expectedBase + '.7z'))
@@ -578,7 +578,7 @@ if ($sevenZip) {
[System.IO.File]::WriteAllText($legacyList, "legacy-file`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $legacyConfig -LogDir $legacyLogDir -SoftwareCatalog $legacyCatalog
$legacyManifest = Read-BaknretManifest -Path (Join-Path $legacyBackupDir 'manifest.json')
$legacyManifest = Read-BakNRetManifest -Path (Join-Path $legacyBackupDir 'manifest.json')
$legacyManifest.items['legacy-file'] = [ordered]@{
baseName = 'legacy-file'
source = 'legacy-file'
@@ -586,7 +586,7 @@ $legacyManifest.items['legacy-file'] = [ordered]@{
action = 'backed-up'
encrypted = $false
}
Write-BaknretManifest -Path (Join-Path $legacyBackupDir 'manifest.json') -Manifest $legacyManifest | Out-Null
Write-BakNRetManifest -Path (Join-Path $legacyBackupDir 'manifest.json') -Manifest $legacyManifest | Out-Null
# 让"恢复确实做了事"可验证:把活文件改成别的内容,恢复后应回到归档里的内容
Set-Content -LiteralPath $legacyLive -Value '{"version":"changed-after-backup"}' -Encoding UTF8
@@ -646,7 +646,7 @@ Test-Case '字面路径不受名录影响,仍走路径命名' {
[System.IO.File]::WriteAllText($literalList, "$dirA2`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $literalList -BackupDir $literalDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $literalDir ((Get-BackupBaseName -RawPath $dirA2) + '.7z'))
Assert-FileExists (Join-Path $literalDir ((Get-BakNRetBackupBaseName -RawPath $dirA2) + '.7z'))
}
$missingList = Join-Path $WorkRoot 'missing.txt'
@@ -656,7 +656,7 @@ $missingExitCode = $LASTEXITCODE
Test-Case '源路径不存在被记为 missing-source,退出码仍为 0(跳过不算失败)' {
Assert-Equal 0 $missingExitCode
$after = Read-BaknretManifest -Path $manifestPath
$after = Read-BakNRetManifest -Path $manifestPath
$record = $after.items['definitely-not-here-12345_from_Z_']
Assert-True ($null -ne $record) 'manifest 里应有该条目的记录'
Assert-Equal 'missing-source' $record.action
+6 -6
View File
@@ -58,13 +58,13 @@ if (-not (Test-Path -LiteralPath $manifestPath)) {
exit 0
}
$config = Get-BaknretConfig -Path $configPath
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$config = Get-BakNRetConfig -Path $configPath
$catalogPath = Resolve-BakNRetCatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$listLines = @(Get-Content -Encoding UTF8 -LiteralPath $listPath)
# 用 cmd 批次文件把子进程输出落盘再读回。为什么不用 `& pwsh ... 2>&1` 直接捕获:
# 那会给子进程建管道,在受限环境 / 沙箱里会被直接拒绝(见 tests\BakNRet.Tests.ps1 里的同款注释)。
function Invoke-BaknretCaptured {
function Invoke-BakNRetCaptured {
param([string]$Script, [string[]]$Arguments)
$hostExe = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell' }
@@ -130,7 +130,7 @@ Test-Case '真实清单里每个条目都能解析出方向,且软件名都能
if ($item.Direction -eq 'backup') { $backupOnly++ }
if ($item.Direction -eq 'restore') { $restoreOnly++ }
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
if ($resolved.Error -and $resolved.Error -like '*名录里没有*') { $catalogMiss += $resolved.Error }
}
@@ -144,7 +144,7 @@ Write-Host "`n== 只读模式不写一个字节 ==" -ForegroundColor Cyan
Test-Case 'Backup.ps1 -DryRun 退出 0、不再有"名录里没有"告警、manifest 不变' {
$before = Get-ManifestHash
$arguments = @('-BackupListPath', $listPath, '-DryRun', '-QuietTool', '-Only') + $SampleEntries
$run = Invoke-BaknretCaptured -Script $backupScript -Arguments $arguments
$run = Invoke-BakNRetCaptured -Script $backupScript -Arguments $arguments
Assert-Equal 0 $run.ExitCode ("DryRun 退出码不是 0;输出尾部:" + ($run.Output -split "`n" | Select-Object -Last 4 | Out-String))
Assert-False ($run.Output -match "名录里没有 '") "DryRun 仍然把某些条目当成查不到的软件名"
@@ -153,7 +153,7 @@ Test-Case 'Backup.ps1 -DryRun 退出 0、不再有"名录里没有"告警、mani
Test-Case 'Restore.ps1 -VerifyOnly 退出 0、把行首 + 识别成"仅备份"、manifest 不变' {
$before = Get-ManifestHash
$run = Invoke-BaknretCaptured -Script $restoreScript -Arguments @(
$run = Invoke-BakNRetCaptured -Script $restoreScript -Arguments @(
'-BackupListPath', $listPath, '-VerifyOnly', '-Only', $RestoreSample, '-Verbose')
Assert-Equal 0 $run.ExitCode ("VerifyOnly 退出码不是 0;输出尾部:" + ($run.Output -split "`n" | Select-Object -Last 4 | Out-String))
+166 -166
View File
@@ -13,7 +13,7 @@
* 软件名录新结构(`软件名 -> Slot -> @{ Path = ... }`):Slot 排序、前缀补全、
多候选报错、`%VAR%` / `$( ... )` 展开、缓存与 -NoCache
* 归档命名与逆向解析(软件名 / 字面路径 / @pathname)
* 归档项与归档内布局(New-BaknretArchiveItem / 暂存 junction / 硬链接)
* 归档项与归档内布局(New-BakNRetArchiveItem / 暂存 junction / 硬链接)
* 7z 排除参数翻译(!前缀、!re: 正则展开、Slot 前缀分配、去重)
* Windows 命令行拼接
* manifest 读写往返与 layouts
@@ -271,18 +271,18 @@ Test-Case '标记必须是独立记号:C:\a:-b 仍然是一个路径' {
$r = ConvertFrom-BackupListLine -Line 'C:\a:-b'
Assert-Equal 'C:\a:-b' $r.Path
Assert-Equal 0 $r.Overrides.Keys.Count
Assert-Null (Test-BaknretMarker -Token 'C:\a:-b')
Assert-Null (Test-BaknretMarker -Token ':+X')
Assert-Null (Test-BakNRetMarker -Token 'C:\a:-b')
Assert-Null (Test-BakNRetMarker -Token ':+X')
}
Test-Case 'Test-BaknretMarker 识别全部标记(大小写敏感)' {
Assert-Equal 'path' (Test-BaknretMarker -Token '::')
Assert-Equal 'exclude' (Test-BaknretMarker -Token ':-')
Assert-Equal 'include' (Test-BaknretMarker -Token ':+')
Assert-Equal 'encrypt' (Test-BaknretMarker -Token ':encrypt')
Assert-Equal 'noencrypt' (Test-BaknretMarker -Token ':!encrypt')
Assert-Equal 'at' (Test-BaknretMarker -Token '@x')
Assert-Equal 'path' (Test-BaknretMarker -Token ' :: ')
Test-Case 'Test-BakNRetMarker 识别全部标记(大小写敏感)' {
Assert-Equal 'path' (Test-BakNRetMarker -Token '::')
Assert-Equal 'exclude' (Test-BakNRetMarker -Token ':-')
Assert-Equal 'include' (Test-BakNRetMarker -Token ':+')
Assert-Equal 'encrypt' (Test-BakNRetMarker -Token ':encrypt')
Assert-Equal 'noencrypt' (Test-BakNRetMarker -Token ':!encrypt')
Assert-Equal 'at' (Test-BakNRetMarker -Token '@x')
Assert-Equal 'path' (Test-BakNRetMarker -Token ' :: ')
}
Test-Case '记号里有空格时目标可以带空格(第一个修饰符之前全是目标)' {
@@ -308,8 +308,8 @@ Test-Case '没有说明时 Comment 为 $null' {
Assert-Null (ConvertFrom-BackupListLine -Line 'App').Comment
}
Test-Case 'Split-BaknretToken:引号内的空白不切分,引号保留' {
$tokens = @(Split-BaknretToken -Text 'a "b c" d :- ''e,f''')
Test-Case 'Split-BakNRetToken:引号内的空白不切分,引号保留' {
$tokens = @(Split-BakNRetToken -Text 'a "b c" d :- ''e,f''')
Assert-Equal 5 $tokens.Count
Assert-Equal 'a' $tokens[0]
Assert-Equal '"b c"' $tokens[1]
@@ -318,19 +318,19 @@ Test-Case 'Split-BaknretToken:引号内的空白不切分,引号保留' {
Assert-Equal "'e,f'" $tokens[4]
}
Test-Case 'Remove-BaknretQuote:成对引号去掉,不成对原样返回' {
Assert-Equal 'a' (Remove-BaknretQuote -Text "'a'")
Assert-Equal 'a' (Remove-BaknretQuote -Text '"a"')
Assert-Equal '"a' (Remove-BaknretQuote -Text '"a')
Assert-Equal 'plain' (Remove-BaknretQuote -Text 'plain')
Test-Case 'Remove-BakNRetQuote:成对引号去掉,不成对原样返回' {
Assert-Equal 'a' (Remove-BakNRetQuote -Text "'a'")
Assert-Equal 'a' (Remove-BakNRetQuote -Text '"a"')
Assert-Equal '"a' (Remove-BakNRetQuote -Text '"a')
Assert-Equal 'plain' (Remove-BakNRetQuote -Text 'plain')
}
Test-Case 'Test-LiteralPath:带分隔符或 % 的按路径,其余按软件名' {
Assert-True (Test-LiteralPath -Path 'C:\a')
Assert-True (Test-LiteralPath -Path 'C:/a')
Assert-True (Test-LiteralPath -Path '%UserProfile%\.ssh')
Assert-False (Test-LiteralPath -Path 'MyApp')
Assert-True (Test-LiteralPath -Path '')
Test-Case 'Test-BakNRetLiteralPath:带分隔符或 % 的按路径,其余按软件名' {
Assert-True (Test-BakNRetLiteralPath -Path 'C:\a')
Assert-True (Test-BakNRetLiteralPath -Path 'C:/a')
Assert-True (Test-BakNRetLiteralPath -Path '%UserProfile%\.ssh')
Assert-False (Test-BakNRetLiteralPath -Path 'MyApp')
Assert-True (Test-BakNRetLiteralPath -Path '')
}
# ============================================================================
@@ -338,19 +338,19 @@ Write-Host "`n== 归档命名 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '基础命名规则' {
Assert-Equal 'legendary_from_%UserProfile%+.config' (Get-BackupBaseName -RawPath '%UserProfile%/.config/legendary')
Assert-Equal 'FooClolor_from_C_+Programs' (Get-BackupBaseName -RawPath 'C:\Programs\FooClolor')
Assert-Equal 'settings.json_from_C_+Programs' (Get-BackupBaseName -RawPath 'C:\Programs\settings.json')
Assert-Equal 'legendary_from_%UserProfile%+.config' (Get-BakNRetBackupBaseName -RawPath '%UserProfile%/.config/legendary')
Assert-Equal 'FooClolor_from_C_+Programs' (Get-BakNRetBackupBaseName -RawPath 'C:\Programs\FooClolor')
Assert-Equal 'settings.json_from_C_+Programs' (Get-BakNRetBackupBaseName -RawPath 'C:\Programs\settings.json')
}
Test-Case '盘符冒号被归一化成下划线' {
Assert-Equal 'a_from_C_' (Get-BackupBaseName -RawPath 'C:\a')
Assert-Equal 'a_from_C_' (Get-BakNRetBackupBaseName -RawPath 'C:\a')
}
Test-Case '/ 与 \ 以及重复分隔符结果一致' {
$a = Get-BackupBaseName -RawPath '%UserProfile%/.config/scoop'
$b = Get-BackupBaseName -RawPath '%UserProfile%\.config\scoop'
$c = Get-BackupBaseName -RawPath '%UserProfile%\\Documents\\WindowsPowerShell'
$a = Get-BakNRetBackupBaseName -RawPath '%UserProfile%/.config/scoop'
$b = Get-BakNRetBackupBaseName -RawPath '%UserProfile%\.config\scoop'
$c = Get-BakNRetBackupBaseName -RawPath '%UserProfile%\\Documents\\WindowsPowerShell'
Assert-Equal $a $b
Assert-Equal 'WindowsPowerShell_from_%UserProfile%+Documents' $c
}
@@ -362,26 +362,26 @@ Test-Case '命名 <-> 路径往返' {
'%LocalAppData%\Microsoft\Edge\User Data'
)
foreach ($case in $cases) {
$base = Get-BackupBaseName -RawPath $case
$back = Convert-BackupFileNameToPath -FileName "$base.7z"
$base = Get-BakNRetBackupBaseName -RawPath $case
$back = Convert-BakNRetBackupFileNameToPath -FileName "$base.7z"
Assert-Equal ($case -replace '[/\\]+', '\') $back "往返 $case"
}
}
Test-Case '归档名里不含非法文件名字符(保留 & % +)' {
$base = Get-BackupBaseName -RawPath 'C:\a\b?c*d|e'
$base = Get-BakNRetBackupBaseName -RawPath 'C:\a\b?c*d|e'
$invalid = [System.IO.Path]::GetInvalidFileNameChars() | Where-Object { $_ -notin @('&', '%', '+') }
foreach ($ch in $invalid) {
Assert-False ($base.Contains([string]$ch)) "归档名里出现了非法字符 $ch"
}
Assert-Equal 'keep%and&and+plus' (Format-CatalogName -Name 'keep%and&and+plus')
Assert-Equal 'keep%and&and+plus' (Format-BakNRetCatalogName -Name 'keep%and&and+plus')
}
Test-Case 'Get-BaknretArchiveTopName:取归档内相对路径的第一段' {
Assert-Equal 'Slot' (Get-BaknretArchiveTopName -ArchivePath 'Slot\a\b')
Assert-Equal 'Slot' (Get-BaknretArchiveTopName -ArchivePath '\Slot\')
Assert-Equal 'leaf' (Get-BaknretArchiveTopName -ArchivePath 'leaf')
Assert-Equal '' (Get-BaknretArchiveTopName -ArchivePath ' ')
Test-Case 'Get-BakNRetArchiveTopName:取归档内相对路径的第一段' {
Assert-Equal 'Slot' (Get-BakNRetArchiveTopName -ArchivePath 'Slot\a\b')
Assert-Equal 'Slot' (Get-BakNRetArchiveTopName -ArchivePath '\Slot\')
Assert-Equal 'leaf' (Get-BakNRetArchiveTopName -ArchivePath 'leaf')
Assert-Equal '' (Get-BakNRetArchiveTopName -ArchivePath ' ')
}
# ============================================================================
@@ -390,8 +390,8 @@ Write-Host "`n== 归档项构造与暂存 ==" -ForegroundColor Cyan
$itemSandbox = Join-Path $env:TEMP ("baknret-item-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
Test-Case 'New-BaknretArchiveItem:归档内路径被清理,TopName 是第一段' {
$item = New-BaknretArchiveItem -ArchivePath '\AlphaData\' -RealPath 'C:\x'
Test-Case 'New-BakNRetArchiveItem:归档内路径被清理,TopName 是第一段' {
$item = New-BakNRetArchiveItem -ArchivePath '\AlphaData\' -RealPath 'C:\x'
Assert-Equal 'AlphaData' $item.ArchivePath
Assert-Equal 'AlphaData' $item.TopName
Assert-Equal 'slot' $item.Kind
@@ -400,13 +400,13 @@ Test-Case 'New-BaknretArchiveItem:归档内路径被清理,TopName 是第一
Assert-False $item.IsFile
Assert-Equal 0 $item.Exclude.Count
$nested = New-BaknretArchiveItem -ArchivePath 'Alpha\sub' -RealPath 'C:\y' -Kind 'include' -Origin 'include'
$nested = New-BakNRetArchiveItem -ArchivePath 'Alpha\sub' -RealPath 'C:\y' -Kind 'include' -Origin 'include'
Assert-Equal 'Alpha' $nested.TopName
Assert-Equal 'include' $nested.Kind
}
Test-Case '归档项的属性集与契约一致(没有旧的 Sources / Dirs / Variants)' {
$item = New-BaknretArchiveItem -ArchivePath 'A' -RealPath 'C:\a'
$item = New-BakNRetArchiveItem -ArchivePath 'A' -RealPath 'C:\a'
foreach ($expected in 'ArchivePath', 'TopName', 'RealPath', 'Kind', 'Slot', 'Description', 'Origin', 'Exists', 'IsFile', 'Exclude') {
Assert-True ($item.PSObject.Properties.Name -contains $expected) "缺少属性 $expected"
}
@@ -428,13 +428,13 @@ Test-Case '暂存半途失败会自己清干净(不留指向真实数据的 ju
$items = @(
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
(New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false)
(New-BakNRetArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false)
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
(New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true)
(New-BakNRetArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true)
)
$threw = $false
try { New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') | Out-Null } catch { $threw = $true }
try { New-BakNRetArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') | Out-Null } catch { $threw = $true }
Assert-True $threw '第二项应当抛错(源文件不存在,硬链接与复制都会失败)'
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
@@ -445,18 +445,18 @@ Test-Case '暂存半途失败会自己清干净(不留指向真实数据的 ju
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
}
Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
Test-Case 'New-BakNRetArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
$root = Join-Path $itemSandbox 'stage-src'
New-Item -ItemType Directory -Path (Join-Path $root 'App') -Force | Out-Null
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'App\f.txt') -Value 'x'
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $root 'settings.json') -Value '{}'
$items = @(
(New-BaknretArchiveItem -ArchivePath 'AlphaData' -RealPath (Join-Path $root 'App') -Exists $true),
(New-BaknretArchiveItem -ArchivePath 'BetaFile' -RealPath (Join-Path $root 'settings.json') -Exists $true -IsFile $true)
(New-BakNRetArchiveItem -ArchivePath 'AlphaData' -RealPath (Join-Path $root 'App') -Exists $true),
(New-BakNRetArchiveItem -ArchivePath 'BetaFile' -RealPath (Join-Path $root 'settings.json') -Exists $true -IsFile $true)
)
$staging = New-BaknretArchiveStaging -Items $items -Root (Join-Path $itemSandbox 'stage')
$staging = New-BakNRetArchiveStaging -Items $items -Root (Join-Path $itemSandbox 'stage')
try {
$dirLink = Get-Item -LiteralPath (Join-Path $staging 'AlphaData') -Force
$fileLink = Get-Item -LiteralPath (Join-Path $staging 'BetaFile') -Force
@@ -465,44 +465,44 @@ Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接
Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容'
}
finally {
Remove-BaknretArchiveStaging -Root $staging
Remove-BakNRetArchiveStaging -Root $staging
}
Assert-False (Test-Path -LiteralPath $staging) '暂存目录应被清理'
Assert-True (Test-Path -LiteralPath (Join-Path $root 'App\f.txt')) '清理暂存绝不能删到真实数据'
}
Test-Case 'New-BaknretJunction / Remove-BaknretJunction:只删连接点,不动目标' {
Test-Case 'New-BakNRetJunction / Remove-BakNRetJunction:只删连接点,不动目标' {
$target = Join-Path $itemSandbox 'j-target'
$link = Join-Path $itemSandbox 'j-link'
New-Item -ItemType Directory -Path $target -Force | Out-Null
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $target 'keep.txt') -Value 'keep'
New-BaknretJunction -Path $link -Target $target | Out-Null
New-BakNRetJunction -Path $link -Target $target | Out-Null
Assert-Equal 'Junction' (Get-Item -LiteralPath $link -Force).LinkType
Remove-BaknretJunction -Path $link
Remove-BakNRetJunction -Path $link
Assert-False (Test-Path -LiteralPath $link)
Assert-True (Test-Path -LiteralPath (Join-Path $target 'keep.txt')) '目标内容必须留着'
try {
New-BaknretJunction -Path $link -Target $target | Out-Null
Remove-BaknretJunction -Path $link
New-BaknretJunction -Path $link -Target $target | Out-Null
New-BakNRetJunction -Path $link -Target $target | Out-Null
Remove-BakNRetJunction -Path $link
New-BakNRetJunction -Path $link -Target $target | Out-Null
Assert-True $true
}
finally {
Remove-BaknretJunction -Path $link
Remove-BakNRetJunction -Path $link
}
}
Test-Case 'New-BaknretJunction:目标已存在时明确抛错' {
Test-Case 'New-BakNRetJunction:目标已存在时明确抛错' {
$target = Join-Path $itemSandbox 'j2-target'
$link = Join-Path $itemSandbox 'j2-link'
New-Item -ItemType Directory -Path $target -Force | Out-Null
New-Item -ItemType Directory -Path $link -Force | Out-Null
$threw = $false
try { New-BaknretJunction -Path $link -Target $target | Out-Null } catch { $threw = $true }
try { New-BakNRetJunction -Path $link -Target $target | Out-Null } catch { $threw = $true }
Assert-True $threw '同名实体存在时必须抛错,不能静默替换'
}
@@ -515,35 +515,35 @@ Test-Case '旧的 Get-ArchiveExcludeArgument 已经不存在(契约破坏点
}
Test-Case '相对模式自动补上归档项名(锚定在归档根)' {
$item = New-BaknretArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('logs\')
$item = New-BakNRetArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('logs\')
Assert-Equal 1 $exclude.Arguments.Count
Assert-Equal '-x!March7thAssistant\logs' $exclude.Arguments[0]
Assert-Null $exclude.Error
}
Test-Case '模式里再写一遍归档项名不会被当成前缀剥掉(新语义:总是拼在项名之后)' {
$item = New-BaknretArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('March7thAssistant\logs\')
$item = New-BakNRetArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('March7thAssistant\logs\')
Assert-Equal '-x!March7thAssistant\March7thAssistant\logs' $exclude.Arguments[0]
}
Test-Case '! 前缀翻译成任意层级匹配' {
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!*Cache')
$item = New-BakNRetArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('!*Cache')
Assert-Equal '-xr!*Cache' $exclude.Arguments[0]
}
Test-Case '模式里的空格转成 ? (7z 的模式不支持空格)' {
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('Default\Code Cache')
$item = New-BakNRetArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('Default\Code Cache')
Assert-Equal '-x!UserData\Default\Code?Cache' $exclude.Arguments[0]
}
Test-Case '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效)' {
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$item = New-BakNRetArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
$patterns = @('Default\Code Cache', '!*Cache', 'logs\', 'UserData\3rdparty\WebBrowser\UserProfile\Integrated')
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns $patterns
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns $patterns
Assert-Equal 4 $exclude.Arguments.Count
foreach ($argument in $exclude.Arguments) {
Assert-False ($argument.Contains('"')) "参数里出现了引号: $argument"
@@ -551,8 +551,8 @@ Test-Case '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排
}
Test-Case '空模式被忽略' {
$item = New-BaknretArchiveItem -ArchivePath 'x' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('', ' ', 'real\')
$item = New-BakNRetArchiveItem -ArchivePath 'x' -RealPath 'C:\x'
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('', ' ', 'real\')
Assert-Equal 1 $exclude.Arguments.Count
Assert-Equal '-x!x\real' $exclude.Arguments[0]
}
@@ -562,9 +562,9 @@ Test-Case '!re:<正则> 展开成源目录里命中的精确路径' {
foreach ($dir in 'Cache1', 'Cache2', 'Keep', 'sub\Cache3') {
New-Item -ItemType Directory -Path (Join-Path $root $dir) -Force | Out-Null
}
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
$item = New-BakNRetArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^Cache\d+$')
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^Cache\d+$')
Assert-Null $exclude.Error
$sorted = @($exclude.Arguments | Sort-Object)
Assert-Equal 3 $sorted.Count
@@ -575,34 +575,34 @@ Test-Case '!re:<正则> 展开成源目录里命中的精确路径' {
Test-Case '非法的 !re: 正则给出明确的 Error,而不是静默漏排除' {
$root = Join-Path $itemSandbox 'regex-src'
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:*Cache')
$item = New-BakNRetArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:*Cache')
Assert-True ($null -ne $exclude.Error)
Assert-Equal 0 $exclude.Arguments.Count
}
Test-Case '空的 !re: 被忽略' {
$root = Join-Path $itemSandbox 'regex-src'
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:')
$item = New-BakNRetArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:')
Assert-Equal 0 $exclude.Arguments.Count
Assert-Null $exclude.Error
}
Test-Case '排除参数超过命令行安全长度时明确报错' {
$root = Join-Path $itemSandbox 'regex-src'
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('aaaa\bbbb') -MaxCommandLineChars 5
$item = New-BakNRetArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('aaaa\bbbb') -MaxCommandLineChars 5
Assert-True ($null -ne $exclude.Error)
}
Test-Case 'Split-BaknretPatternScope:`<项名>\` 前缀把模式分配给对应归档项' {
Test-Case 'Split-BakNRetPatternScope:`<项名>\` 前缀把模式分配给对应归档项' {
$items = @(
(New-BaknretArchiveItem -ArchivePath 'DefaultConfig' -RealPath 'C:\c'),
(New-BaknretArchiveItem -ArchivePath 'GlobalPersist' -RealPath 'C:\g'),
(New-BaknretArchiveItem -ArchivePath 'UserPersist' -RealPath 'C:\u')
(New-BakNRetArchiveItem -ArchivePath 'DefaultConfig' -RealPath 'C:\c'),
(New-BakNRetArchiveItem -ArchivePath 'GlobalPersist' -RealPath 'C:\g'),
(New-BakNRetArchiveItem -ArchivePath 'UserPersist' -RealPath 'C:\u')
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('GlobalPersist\steam\steamapps', 'plain\one', '!*Cache')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('GlobalPersist\steam\steamapps', 'plain\one', '!*Cache')
Assert-Equal 'steam\steamapps' $map[1][0]
Assert-Equal 'plain\one' $map[0][0] '没点名任何项的模式对每一项各展开一份'
@@ -612,8 +612,8 @@ Test-Case 'Split-BaknretPatternScope:`<项名>\` 前缀把模式分配给对
}
}
Test-Case 'Merge-BaknretExcludeArgument:去重且保序' {
$merged = Merge-BaknretExcludeArgument -ArgumentLists @(@('-x!a', '-xr!c'), @('-x!a', '-x!b'))
Test-Case 'Merge-BakNRetExcludeArgument:去重且保序' {
$merged = Merge-BakNRetExcludeArgument -ArgumentLists @(@('-x!a', '-xr!c'), @('-x!a', '-x!b'))
Assert-Equal 3 $merged.Count
Assert-Equal '-x!a' $merged[0]
Assert-Equal '-xr!c' $merged[1]
@@ -625,23 +625,23 @@ Write-Host "`n== 命令行拼接 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '无空格参数原样输出' {
Assert-Equal 'a -t7z' (ConvertTo-NativeArgumentString -ArgumentList @('a', '-t7z'))
Assert-Equal 'a -t7z' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a', '-t7z'))
}
Test-Case '含空格参数加引号' {
Assert-Equal '"C:\Program Files\x"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\x'))
Assert-Equal '"C:\Program Files\x"' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\Program Files\x'))
}
Test-Case '引号内的结尾反斜杠翻倍(否则会被当成转义引号)' {
Assert-Equal '"C:\Program Files\\"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\'))
Assert-Equal '"C:\Program Files\\"' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\Program Files\'))
}
Test-Case '内部引号被转义' {
Assert-Equal '"say \"hi\""' (ConvertTo-NativeArgumentString -ArgumentList @('say "hi"'))
Assert-Equal '"say \"hi\""' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('say "hi"'))
}
Test-Case '空参数输出一对空引号' {
Assert-Equal '""' (ConvertTo-NativeArgumentString -ArgumentList @(''))
Assert-Equal '""' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @(''))
}
# ============================================================================
@@ -654,7 +654,7 @@ Test-Case 'manifest 读写往返(含 layouts)' {
New-Item -ItemType Directory -Path $sandbox -Force | Out-Null
$path = Join-Path $sandbox 'manifest.json'
$m = Read-BaknretManifest -Path $path
$m = Read-BakNRetManifest -Path $path
Assert-Equal 0 $m.items.Count
$m.compressor = [pscustomobject]@{ name = '7z'; version = '26.03' }
@@ -667,9 +667,9 @@ Test-Case 'manifest 读写往返(含 layouts)' {
[ordered]@{ name = 'BetaFile'; kind = 'file' }
)
}
Write-BaknretManifest -Path $path -Manifest $m | Out-Null
Write-BakNRetManifest -Path $path -Manifest $m | Out-Null
$again = Read-BaknretManifest -Path $path
$again = Read-BakNRetManifest -Path $path
Assert-Equal 1 $again.items.Count
Assert-Equal 'backed-up' $again.items['my-app'].action
Assert-Equal 2 $again.items['my-app'].layouts.Count
@@ -683,12 +683,12 @@ Test-Case 'manifest 读写往返(含 layouts)' {
Test-Case 'manifest 损坏时不抛异常' {
$path = Join-Path $sandbox 'broken.json'
[System.IO.File]::WriteAllText($path, '{ this is not json')
$m = Read-BaknretManifest -Path $path
$m = Read-BakNRetManifest -Path $path
Assert-Equal 0 $m.items.Count
}
Test-Case '配置缺失时返回默认值' {
$config = Get-BaknretConfig -Path (Join-Path $sandbox 'nope.psd1')
$config = Get-BakNRetConfig -Path (Join-Path $sandbox 'nope.psd1')
Assert-Equal 'Backups' $config.BackupDir
Assert-True ($config.MinFreeSpaceGB -gt 0)
}
@@ -696,7 +696,7 @@ Test-Case '配置缺失时返回默认值' {
Test-Case '配置嵌套段落合并且不丢默认键' {
$path = Join-Path $sandbox 'cfg.psd1'
Set-Content -Encoding UTF8 -LiteralPath $path -Value "@{ Encryption = @{ Enabled = `$true }; CompressionLevel = 5 }"
$config = Get-BaknretConfig -Path $path
$config = Get-BakNRetConfig -Path $path
Assert-Equal 5 $config.CompressionLevel
Assert-True $config.Encryption.Enabled
Assert-True $config.Encryption.EncryptHeaders '未覆盖的嵌套键应保留默认值'
@@ -706,9 +706,9 @@ Test-Case '口令:环境变量可读取,取不到返回 $null' {
$old = $env:BAKNRET_PASSWORD
try {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope'))
Assert-Null (Get-BakNRetPassword -PasswordFile (Join-Path $sandbox 'nope'))
$env:BAKNRET_PASSWORD = 'from-env'
Assert-Equal 'from-env' (Get-BaknretPassword)
Assert-Equal 'from-env' (Get-BakNRetPassword)
}
finally {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
@@ -731,7 +731,7 @@ Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' {
$content = "@{`n FooClolor = @{ DefaultData = @{ Path = '$appDir' } }`n 'scoop-config' = @{ DefaultConfig = @{ Path = '$configDir' } }`n '.ssh' = @{ DefaultData = @{ Path = '$appDir' } }`n}`n"
[System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 3 $catalog.Count
Assert-True $catalog.ContainsKey('FooClolor')
Assert-True $catalog.ContainsKey('scoop-config') '带连字符的键必须能解析'
@@ -743,7 +743,7 @@ Test-Case '名录:Slot 按名字排序(哈希表顺序不可依赖)' {
$catPath = Join-Path $catalogSandbox 'sorted.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n FooClolor = @{ Zeta = @{ Path = '$appDir' }; Alpha = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 'Multi' $catalog['FooClolor'].Kind
Assert-Equal 2 $catalog['FooClolor'].Slots.Count
Assert-Equal 'Alpha' $catalog['FooClolor'].Slots[0].Name
@@ -756,7 +756,7 @@ Test-Case '名录:Slot 对象字段与契约一致' {
Set-Content -Encoding UTF8 -LiteralPath $filePath -Value '{}'
[System.IO.File]::WriteAllText($catPath, "@{`n Rich = @{`n Beta = @{ Path = '$filePath'; Encrypt = `$true; Description = 'beta 说明' }`n Alpha = @{ Path = '$configDir'; Exclude = '!*Cache,sub\one'; Include = 'Modules:$appDir' }`n }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
$entry = $catalog['Rich']
foreach ($expected in 'Name', 'Path', 'Description', 'Slots', 'Kind', 'Error', 'Missing', 'Raw') {
Assert-True ($entry.PSObject.Properties.Name -contains $expected) "名录条目缺少 $expected"
@@ -789,7 +789,7 @@ Test-Case '名录解析:目录带版本后缀时按前缀补全(legendary_2.
$catPath = Join-Path $catalogSandbox 'suffix.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n legendary = @{ DefaultConfig = @{ Path = '$root\legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 'Single' $catalog['legendary'].Kind
Assert-True $catalog['legendary'].Slots[0].Resolved -like '*legendary_2.0.4'
Assert-True $catalog['legendary'].Slots[0].Suffixed '补全出来的路径要标记 Suffixed'
@@ -801,20 +801,20 @@ Test-Case '名录解析:不会把 Legendary 误配成 LegendarySomething' {
$catPath = Join-Path $catalogSandbox 'strict.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 'Unresolved' $catalog['Legendary'].Kind '必须以下一个字符是 _ 或 - 为界'
Assert-False $catalog['Legendary'].Slots[0].Exists
Assert-Equal 1 $catalog['Legendary'].Missing.Count
}
Test-Case 'Find-ChildDirectoryByName:_ / - 边界之外不补全' {
Test-Case 'Find-BakNRetChildDirectoryByName:_ / - 边界之外不补全' {
$root = Join-Path $catalogSandbox 'boundary'
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_1.0') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary-2.0') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $root 'LegendarySomething') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $root 'Unrelated') -Force | Out-Null
$found = @(Find-ChildDirectoryByName -Parent $root -Name 'Legendary')
$found = @(Find-BakNRetChildDirectoryByName -Parent $root -Name 'Legendary')
Assert-Equal 2 $found.Count
Assert-True (@($found | Where-Object { $_ -like '*Legendary_1.0' }).Count -eq 1)
Assert-True (@($found | Where-Object { $_ -like '*Legendary-2.0' }).Count -eq 1)
@@ -827,7 +827,7 @@ Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会
$catPath = Join-Path $catalogSandbox 'multicand.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
$entry = $catalog['Legendary']
Assert-True ($null -ne $entry.Error)
Assert-True ($entry.Error -like '*拆成多个 Slot*') '错误里要给出可操作的指引'
@@ -836,7 +836,7 @@ Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会
Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是崩掉' {
$bad = Join-Path $catalogSandbox 'bad.psd1'
[System.IO.File]::WriteAllText($bad, "@{`n A = @{ S = 'C:\x' }`n B = @{ S = @{ Description = 'x' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $bad -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $bad -NoCache
Assert-Equal 'Invalid' $catalog['A'].Kind
Assert-True ($catalog['A'].Error -like '*Slot S*')
Assert-Equal 'Invalid' $catalog['B'].Kind
@@ -846,12 +846,12 @@ Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是
Test-Case '名录:裸字符串 / 数组等旧结构会被 ERROR 跳过(不再当目录用)' {
$legacy = Join-Path $catalogSandbox 'legacy.psd1'
[System.IO.File]::WriteAllText($legacy, "@{`n A = 'C:\Programs\A'`n B = @('C:\1','C:\2')`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $legacy -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $legacy -NoCache
Assert-Equal 0 $catalog.Count '旧结构必须被跳过,不能猜成目录'
$dirs = Join-Path $catalogSandbox 'dirs.psd1'
[System.IO.File]::WriteAllText($dirs, "@{`n C = @{ Dirs = @('C:\1','C:\2') }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $dirs -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $dirs -NoCache
Assert-Equal 1 $catalog.Count
Assert-Equal 'Invalid' $catalog['C'].Kind
Assert-True ($catalog['C'].Error -like '*Dirs*')
@@ -869,7 +869,7 @@ Test-Case '名录:Includes 分文件维护会被合并' {
[System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $basePath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $basePath -NoCache
Assert-True $catalog.ContainsKey('base-app')
Assert-True $catalog.ContainsKey('extra-app') 'Includes 引入的条目也应在'
}
@@ -878,20 +878,20 @@ Test-Case '名录:结果按文件指纹缓存,-NoCache 强制重读' {
$catPath = Join-Path $catalogSandbox 'cache.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$first = Get-SoftwareCatalog -Path $catPath
$second = Get-SoftwareCatalog -Path $catPath
$first = Get-BakNRetSoftwareCatalog -Path $catPath
$second = Get-BakNRetSoftwareCatalog -Path $catPath
Assert-True ([object]::ReferenceEquals($first, $second)) '同一文件同一指纹应命中缓存'
$fresh = Get-SoftwareCatalog -Path $catPath -NoCache
$fresh = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-False ([object]::ReferenceEquals($first, $fresh)) '-NoCache 必须重新读'
[System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$changed = Get-SoftwareCatalog -Path $catPath
$changed = Get-BakNRetSoftwareCatalog -Path $catPath
Assert-Equal $configDir $changed['Cached'].Slots[0].Resolved '文件变了缓存必须失效'
}
# ============================================================================
Write-Host "`n== Resolve-BackupEntry(归档项解析) ==" -ForegroundColor Cyan
Write-Host "`n== Resolve-BakNRetBackupEntry(归档项解析) ==" -ForegroundColor Cyan
# ============================================================================
$resolveSandbox = Join-Path $env:TEMP ("baknret-resolve-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
@@ -909,7 +909,7 @@ $resolveCatalog = Join-Path $resolveSandbox 'catalog.psd1'
Test-Case '软件名条目:一个 Slot -> 一个归档项,路径是 Slot 名,归档名是软件名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-True $resolved.IsName
Assert-Equal 'name' $resolved.ArchiveFlavor
@@ -925,9 +925,9 @@ Test-Case '软件名条目:一个 Slot -> 一个归档项,路径是 Slot 名
Assert-True $resolved.Items[0].Exists
}
Test-Case 'Resolve-BackupEntry 的返回值里没有旧的 Sources / RootName 等属性' {
Test-Case 'Resolve-BakNRetBackupEntry 的返回值里没有旧的 Sources / RootName 等属性' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
foreach ($expected in 'IsName', 'CatalogEntry', 'BaseName', 'ArchiveFlavor', 'Direction', 'Items', 'Encrypt', 'ExcludePatterns', 'HasExcludeOverride', 'Includes', 'HasIncludeOverride', 'Source', 'Error', 'Blocking') {
Assert-True ($resolved.PSObject.Properties.Name -contains $expected) "缺少属性 $expected"
}
@@ -938,7 +938,7 @@ Test-Case 'Resolve-BackupEntry 的返回值里没有旧的 Sources / RootName
Test-Case '字面路径条目:仍用路径命名算法,归档内是历史布局 <末级名>' {
$item = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-False $resolved.IsName
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
@@ -949,34 +949,34 @@ Test-Case '字面路径条目:仍用路径命名算法,归档内是历史布
Test-Case '@pathname 用名录里的真实路径跑命名算法' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor @pathname'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal (Get-BackupBaseName -RawPath $dirA) $resolved.BaseName
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal (Get-BakNRetBackupBaseName -RawPath $dirA) $resolved.BaseName
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath '归档内名字仍由 Slot 决定'
}
Test-Case ':: / @ Path= 覆盖单 Slot 条目的真实路径' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :: $dirB"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal 1 $resolved.Items.Count
Assert-Equal $dirB $resolved.Items[0].RealPath
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
Assert-Null $resolved.Blocking
$atItem = ConvertFrom-BackupListLine -Line "FooClolor @ Path='$dirB'"
$atResolved = Resolve-BackupEntry -Entry $atItem -CatalogPath $resolveCatalog
$atResolved = Resolve-BakNRetBackupEntry -Entry $atItem -CatalogPath $resolveCatalog
Assert-Equal $dirB $atResolved.Items[0].RealPath
}
Test-Case ':: 覆盖路径时,多 Slot 的条目会 Blocking(说不清是哪个 Slot)' {
$item = ConvertFrom-BackupListLine -Line "MultiApp :: $dirB"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-True ($null -ne $resolved.Blocking)
Assert-True ($resolved.Blocking -like '*2 个 Slot*')
}
Test-Case '名录里没有该名字:BaseName 退回可读目录名,并给出 Error' {
$item = ConvertFrom-BackupListLine -Line 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal 'no-such-thing' $resolved.BaseName
Assert-True ($null -ne $resolved.Error) '应给出错误说明'
Assert-Equal 0 $resolved.Items.Count
@@ -986,7 +986,7 @@ Test-Case '名录里的路径不存在时仍给出归档项(恢复要靠它还
$catPath = Join-Path $resolveSandbox 'gone.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = @{ DefaultData = @{ Path = 'C:\definitely\not\here' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$item = ConvertFrom-BackupListLine -Line 'gone-app'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catPath
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catPath
Assert-Equal 1 $resolved.Items.Count '恢复端据 Items 还原'
Assert-Equal 'C:\definitely\not\here' $resolved.Items[0].RealPath
Assert-False $resolved.Items[0].Exists
@@ -995,7 +995,7 @@ Test-Case '名录里的路径不存在时仍给出归档项(恢复要靠它还
Test-Case ':+ 追加项:<归档内相对路径>:<宿主机绝对路径>' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ Modules:$dirExtra"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal 2 $resolved.Items.Count
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
Assert-Equal 'Modules' $resolved.Items[1].ArchivePath
@@ -1011,11 +1011,11 @@ Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' {
$catPath = Join-Path $resolveSandbox 'withinc.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n IncApp = @{ DefaultData = @{ Path = '$dirA'; Include = 'CatalogInc:$incDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'IncApp') -CatalogPath $catPath
$plain = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'IncApp') -CatalogPath $catPath
Assert-Equal 2 $plain.Items.Count
Assert-Equal 'CatalogInc' $plain.Items[1].ArchivePath
$overridden = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line "IncApp :+ EntryInc:$dirExtra") -CatalogPath $catPath
$overridden = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line "IncApp :+ EntryInc:$dirExtra") -CatalogPath $catPath
Assert-Equal 2 $overridden.Items.Count
Assert-Equal 'EntryInc' $overridden.Items[1].ArchivePath
Assert-Equal $dirExtra $overridden.Items[1].RealPath
@@ -1023,45 +1023,45 @@ Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' {
Test-Case '归档内路径冲突:同名追加项 -> Blocking' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ DefaultData:$dirExtra"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-True ($null -ne $resolved.Blocking)
Assert-True ($resolved.Blocking -like '*冲突*')
}
Test-Case '归档内路径冲突:父子关系 -> Blocking' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ DefaultData\sub:$dirExtra"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-True ($null -ne $resolved.Blocking)
Assert-True ($resolved.Blocking -like '*父子*')
}
Test-Case '加密:名录 Slot 的 Encrypt 之和,条目级覆盖优先' {
$catalogEncrypt = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor') -CatalogPath $resolveCatalog
$catalogEncrypt = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor') -CatalogPath $resolveCatalog
Assert-True $catalogEncrypt.Encrypt
$off = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor :!encrypt') -CatalogPath $resolveCatalog
$off = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor :!encrypt') -CatalogPath $resolveCatalog
Assert-False $off.Encrypt
$on = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'MultiApp :encrypt') -CatalogPath $resolveCatalog
$on = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'MultiApp :encrypt') -CatalogPath $resolveCatalog
Assert-True $on.Encrypt '名录没开、条目开了 -> 开'
Assert-False $on.HasExcludeOverride
}
Test-Case '方向标记随条目一路传到 Resolve-BackupEntry' {
Assert-Equal 'backup' (Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ FooClolor') -CatalogPath $resolveCatalog).Direction
Assert-Equal 'restore' (Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '- FooClolor') -CatalogPath $resolveCatalog).Direction
Test-Case '方向标记随条目一路传到 Resolve-BakNRetBackupEntry' {
Assert-Equal 'backup' (Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ FooClolor') -CatalogPath $resolveCatalog).Direction
Assert-Equal 'restore' (Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '- FooClolor') -CatalogPath $resolveCatalog).Direction
}
Test-Case '条目级 :- 覆盖名录里的 Exclude(不是叠加)' {
$catPath = Join-Path $resolveSandbox 'withexc.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n ExcApp = @{ DefaultData = @{ Path = '$dirA'; Exclude = '!*CatalogCache' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp') -CatalogPath $catPath
$plain = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp') -CatalogPath $catPath
Assert-False $plain.HasExcludeOverride
Assert-Equal 0 $plain.ExcludePatterns.Count
Assert-Equal '!*CatalogCache' $plain.Items[0].Exclude[0]
$overridden = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp :- !*Listed') -CatalogPath $catPath
$overridden = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp :- !*Listed') -CatalogPath $catPath
Assert-True $overridden.HasExcludeOverride
Assert-Equal '!*Listed' $overridden.ExcludePatterns[0]
}
@@ -1093,8 +1093,8 @@ else {
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source 'Default\IndexedDB\i.bin') 'i'
$patterns = @('!*Cache', 'component_crx_cache', 'Default\Code Cache', 'Default\Extensions', 'Default\IndexedDB')
$archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'path' -Exists $true
$excludeArgs = @((Get-BaknretExcludeArgument -Item $archiveItem -Patterns $patterns).Arguments)
$archiveItem = New-BakNRetArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'path' -Exists $true
$excludeArgs = @((Get-BakNRetExcludeArgument -Item $archiveItem -Patterns $patterns).Arguments)
Assert-True ($excludeArgs.Count -ge 5) '排除参数应被翻译出来'
$archive = Join-Path $sandbox 'excl.7z'
@@ -1124,8 +1124,8 @@ else {
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $source "$d\x.bin") 'x'
}
$archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'slot' -Exists $true
$excludeArgs = @((Get-BaknretExcludeArgument -Item $archiveItem -Patterns @('!re:^Cache\d+$')).Arguments)
$archiveItem = New-BakNRetArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'slot' -Exists $true
$excludeArgs = @((Get-BakNRetExcludeArgument -Item $archiveItem -Patterns @('!re:^Cache\d+$')).Arguments)
Assert-Equal 3 $excludeArgs.Count '两个顶层 Cache 目录 + 嵌套的 sub\Cache3'
$archive = Join-Path $sandbox 're.7z'
@@ -1159,7 +1159,7 @@ else {
Assert-True (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Cache\c.bin")) '对照组应当包含被排除的那个文件'
}
Test-Case 'Get-ArchiveTopLevelNames 能列出归档内的顶层条目名' {
Test-Case 'Get-BakNRetArchiveTopLevelNames 能列出归档内的顶层条目名' {
$root = Join-Path $sandbox 'topnames'
New-Item -ItemType Directory -Path (Join-Path $root 'Alpha') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $root 'Beta') -Force | Out-Null
@@ -1169,7 +1169,7 @@ else {
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $archive, 'Alpha', 'Beta') -WorkingDirectory $root
Assert-Equal 0 $code
$names = @(Get-ArchiveTopLevelNames -ArchivePath $archive -SevenZip $sevenZip)
$names = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $archive -SevenZip $sevenZip)
$sorted = @($names | Sort-Object)
Assert-Equal 2 $sorted.Count
Assert-Equal 'Alpha' $sorted[0]
@@ -1203,13 +1203,13 @@ Test-Case '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参
$sentinel = 'SENTINEL-PW-9f3a'
$logPath = $null
try {
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
Set-BaknretDebug
$logPath = Start-BakNRetLog -Directory $sandbox -Prefix 'secret'
Set-BakNRetDebug
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
}
finally {
Set-BaknretDebug -Enabled:$false
Stop-BaknretLog
Set-BakNRetDebug -Enabled:$false
Stop-BakNRetLog
}
$logText = Get-Content -Encoding UTF8 -LiteralPath $logPath -Raw
@@ -1236,7 +1236,7 @@ Test-Case '空目录的摘要给 0 而不是 $null(否则空间守卫会静默
$empty = Join-Path $env:TEMP ("bnr-empty-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $empty -Force | Out-Null
try {
$summary = Get-FolderSummary -FolderPath $empty
$summary = Get-BakNRetFolderSummary -FolderPath $empty
Assert-Equal 0 $summary.FileCount
Assert-True ($null -ne $summary.TotalSize) 'TotalSize 不能是 $null'
Assert-True ($summary.TotalSize -is [long]) 'TotalSize 应当是整数'
@@ -1252,11 +1252,11 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧
New-Item -ItemType Directory -Path $base -Force | Out-Null
$target = Join-Path $base 'text.json'
try {
Write-BaknretAtomicText -Path $target -Text 'FIRST' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'FIRST' | Out-Null
Assert-Equal 'FIRST' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
# 目标已存在时走 File.Replace
Write-BaknretAtomicText -Path $target -Text 'SECOND' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'SECOND' | Out-Null
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
Assert-FileMissing "$target.tmp"
@@ -1264,7 +1264,7 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧
# 后者失败时旧文件已经没了,而 manifest 丢了就只能靠文件名反推。
(Get-Item -LiteralPath $target).IsReadOnly = $true
$threw = $false
try { Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' | Out-Null } catch { $threw = $true }
try { Write-BakNRetAtomicText -Path $target -Text 'SHOULD-NOT-LAND' | Out-Null } catch { $threw = $true }
Assert-True $threw '目标只读时替换应当抛错'
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
}
@@ -1286,22 +1286,22 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' {
$first = $null
$second = $null
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $first) '第一次应当拿到锁'
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -eq $second) '同一目录的第二次不应当拿到锁'
Assert-FileExists (Get-BaknretRunLockPath -Directory $dir)
Assert-FileExists (Get-BakNRetRunLockPath -Directory $dir)
}
finally {
Exit-BaknretRunLock -Lock $second
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $second
Exit-BakNRetRunLock -Lock $first
}
# 锁文件是**独占**打开的(FileShare.None),内容只能在释放之后读 —— 而它本来就该
# 留在那里给排查的人看:"到底是谁占着"这个问题不该靠猜。
$info = Get-Content -Encoding UTF8 -LiteralPath (Get-BaknretRunLockPath -Directory $dir) -Raw
$info = Get-Content -Encoding UTF8 -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) -Raw
Assert-True ($info -match "pid=$PID") "锁文件里应当写明持有进程,实际内容是:$info"
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1310,13 +1310,13 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' {
Test-Case '运行锁:释放之后可以重新取得' {
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $first) '第一次应当拿到锁'
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $first
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $second) '释放之后应当能重新拿到锁'
Exit-BaknretRunLock -Lock $second
Exit-BakNRetRunLock -Lock $second
}
finally {
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
+7 -7
View File
@@ -51,14 +51,14 @@ if (-not $BackupListPath) { $BackupListPath = Join-Path $projectRoot 'BackupList
Import-Module (Join-Path $projectRoot 'BakNRet\BakNRet.psd1') -Force
if (-not $ConfigPath) { $ConfigPath = Join-Path $projectRoot 'BackupConfig.psd1' }
$config = Get-BaknretConfig -Path $ConfigPath
$config = Get-BakNRetConfig -Path $ConfigPath
if (-not $BackupDir) {
$BackupDir = $config.BackupDir
if (-not [System.IO.Path]::IsPathRooted($BackupDir)) { $BackupDir = Join-Path $projectRoot $BackupDir }
}
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$catalogPath = Resolve-BakNRetCatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$manifestPath = Join-Path $BackupDir 'manifest.json'
$supportedFormats = @('.7z', '.rar', '.zip', '.tar')
@@ -85,7 +85,7 @@ function Find-ArchiveByBaseName {
# 阶段一:规划
# ---------------------------------------------------------------------------
$manifestOld = Read-BaknretManifest -Path $manifestPath
$manifestOld = Read-BakNRetManifest -Path $manifestPath
# manifest 里的历史归档名按 source / resolvedSource 建索引:
# 条目写法改过(软件名 -> 手写路径、改名、合并)之后,键对不上了,
@@ -121,7 +121,7 @@ foreach ($line in (Get-Content -Encoding UTF8 -LiteralPath $BackupListPath)) {
$item = ConvertFrom-BackupListLine -Line $line
if (-not $item) { continue }
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$newName = $resolved.BaseName
if (-not $newName) { continue }
@@ -143,11 +143,11 @@ foreach ($line in (Get-Content -Encoding UTF8 -LiteralPath $BackupListPath)) {
$pathSource = if ($slots.Count -eq 1) { $slots[0].Declared } else { $null }
}
if ($pathSource) {
$derived = Get-BackupBaseName -RawPath $pathSource
$derived = Get-BakNRetBackupBaseName -RawPath $pathSource
if ($derived) { $candidates += $derived }
}
if ($resolved.IsName) {
$candidates += (Format-CatalogName -Name $item.Path)
$candidates += (Format-BakNRetCatalogName -Name $item.Path)
}
if ($manifestOld.items.Contains($newName)) {
$recorded = $manifestOld.items[$newName]
@@ -316,7 +316,7 @@ $manifestNew = [pscustomobject]@{
compressor = $manifestOld.compressor
items = $rebuilt
}
Write-BaknretManifest -Path $manifestPath -Manifest $manifestNew | Out-Null
Write-BakNRetManifest -Path $manifestPath -Manifest $manifestNew | Out-Null
# ---------------------------------------------------------------------------
# 阶段四:报告
+3 -3
View File
@@ -1,4 +1,4 @@
# tools\lab —— BakNRet 的隔离测试环境(Hyper-V 真机级 VM)
# tools\lab —— BakNRet 的隔离测试环境(Hyper-V 真机级 VM)
在**宿主机之外的 Windows 虚拟机**里跑 BakNRet 的备份 / 恢复 / 测试。宿主机仓库、`Backups\`、
`logs\` 在本环境里只被读取,从不写入;VM 内也没有挂载宿主机的任何目录(一切交互走
@@ -138,14 +138,14 @@ gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 reset # 秒回 clean-baseli
### 在 VM 里直接跑 `tests\Run-Pester.ps1` 会红 8 项(都是中文断言)
`tests\BakNRet*.Tests.ps1` 里的 `Invoke-BaknretScript` 这样抓子进程输出:
`tests\BakNRet*.Tests.ps1` 里的 `Invoke-BakNRetScript` 这样抓子进程输出:
```
cmd /c pwsh -File Backup.ps1 ... > out.txt 2>&1
Get-Content -LiteralPath out.txt -Encoding UTF8
```
而 `Backup.ps1` / `Restore.ps1` 的 `Write-Log` 走 `Write-Host`,写进 `out.txt` 的**字节编码取自
而 `Backup.ps1` / `Restore.ps1` 的 `Write-BakNRetLog` 走 `Write-Host`,写进 `out.txt` 的**字节编码取自
`[Console]::OutputEncoding`**:
| 环境 | `[Console]::OutputEncoding` | 结果 |
+5 -5
View File
@@ -101,7 +101,7 @@ function Get-SecurityFingerprint {
return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' '))
}
function Invoke-BaknretChild {
function Invoke-BakNRetChild {
<#
.SYNOPSIS
用独立进程跑 Backup.ps1 / Restore.ps1(两个脚本结尾都会 exit)。
@@ -190,7 +190,7 @@ function Remove-TreeHard {
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
foreach ($link in $links) {
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir "{0}"' -f $link.FullName))
Remove-BaknretJunction -Path $link.FullName
Remove-BakNRetJunction -Path $link.FullName
}
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir /s /q "{0}"' -f $Path))
@@ -217,7 +217,7 @@ else {
$BackupDir = Join-Path $WorkRoot 'backups'
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
$privileges = Enable-BaknretPrivilege -Name @('SeRestorePrivilege', 'SeBackupPrivilege')
$privileges = Enable-BakNRetPrivilege -Name @('SeRestorePrivilege', 'SeBackupPrivilege')
if ($privileges.Missing.Count -gt 0) {
Write-Host ('[acl] 警告:{0} 不在令牌里 —— 属主无法恢复,B 段会失败(应以管理员/SYSTEM 运行)' -f ($privileges.Missing -join '、')) -ForegroundColor Yellow
}
@@ -398,7 +398,7 @@ $configText = @"
Write-Host ''
Write-Host '[备份] Backup.ps1' -ForegroundColor Yellow
$backup = Invoke-BaknretChild -Script (Join-Path $RepoPath 'Backup.ps1') -Parameters @{
$backup = Invoke-BakNRetChild -Script (Join-Path $RepoPath 'Backup.ps1') -Parameters @{
BackupListPath = $listPath
ConfigPath = $configPath
BackupDir = $BackupDir
@@ -420,7 +420,7 @@ Test-Scenario '源已删除(模拟真的丢了)' ($leftovers.Count -eq 0) ($
Write-Host ''
Write-Host '[恢复] Restore.ps1' -ForegroundColor Yellow
$restore = Invoke-BaknretChild -Script (Join-Path $RepoPath 'Restore.ps1') -Parameters @{
$restore = Invoke-BakNRetChild -Script (Join-Path $RepoPath 'Restore.ps1') -Parameters @{
BackupListPath = $listPath
ConfigPath = $configPath
BackupDir = $BackupDir
+2 -2
View File
@@ -3,12 +3,12 @@
在 VM 内以 UTF-8 控制台编码运行一个测试套件(不改仓库里的任何测试代码)。
.DESCRIPTION
为什么需要它 —— tests\BakNRet*.Tests.ps1 的 Invoke-BaknretScript 是这么抓子进程输出的:
为什么需要它 —— tests\BakNRet*.Tests.ps1 的 Invoke-BakNRetScript 是这么抓子进程输出的:
cmd /c pwsh -File Backup.ps1 ... > out.txt 2>&1
Get-Content -LiteralPath out.txt -Encoding UTF8
而 Backup.ps1 / Restore.ps1 的 Write-Log 走 Write-Host,写进 out.txt 的字节用的是
而 Backup.ps1 / Restore.ps1 的 Write-BakNRetLog 走 Write-Host,写进 out.txt 的字节用的是
`[Console]::OutputEncoding`:
* 宿主机上它是 utf-8 -> 文件是 UTF-8 -> 按 UTF-8 读回,中文正确,套件全绿;