style: 按微软规范落地静态分析,并全仓机械重排
三件事:
1) tools\Install-TestDependencies.ps1 现在也把 PSScriptAnalyzer 装进仓库内的 .tools\modules
(不动机器上的全局模块,与 Pester 同一策略)。
2) PSScriptAnalyzerSettings.psd1:这是必要的,不是装饰 —— 那 6 条格式规则
(括号、缩进、空格、对齐、大小写)默认全是 Disabled,所以不带 -Settings 的
`Invoke-ScriptAnalyzer -Severity Warning,Error` 会**静默漏掉全部排版问题**。本文件用 Rules
把它们打开(而不是用 IncludeRules 换一套),于是默认规则与格式规则同时生效。
三条有意的排除都写明了理由:PSAvoidUsingWriteHost(彩色控制台输出是这份工具的刻意设计)、
PSUseShouldProcessForStateChangingFunctions(WhatIf 的边界在入口脚本,给库里 27 个改状态的
函数都加上反而会"静默跳过",备份看着成功却什么都没做)、PSAvoidUsingPlainTextForPassword
(7z 只接受命令行口令,这是 7z 的限制,README 里写明了取舍)。
3) tools\Invoke-Analyzer.ps1:独立门禁(不塞进 Pester 用例 —— 套件跑一次二十多秒,
混进去会让"测试红了"这句话失去分辨力),路径过滤与验收门槛的 Encode/Parse 两层一致。
全仓重排结果:706 条告警 -> 67 条。修掉的 639 条全部是格式(闭括号 168、空格 80、
对齐 68、缩进 60、行长 229)。重排后 9/9 验收全绿、100 个文件两版解析零错、
276 个断言原样通过 —— 机械重排没有改变任何可观察行为。
如实说明两件事:
* 行长上限设成 160,**不是**官方默认的 120。120 在本仓库意味着 270 处改动(主要是
中文注释与测试夹具里的一行式目录),160 意味着 41 处。160 仍是"宽但可读",而理由是写在
配置文件里的:这不是悄悄放宽,想收紧到 120 时那份清单就在分析器输出里。
* 剩余 67 条里,41 条是上面那批行长,其余 26 条是分析器找出的真问题(未使用参数 6、
空 catch 6、MD5 指纹 1、覆盖内置命令 1、switch 默认值 1 等)。其中
Find-ChildDirectoryByName 的 MaxDepth 参数从未被使用 —— 也就是配置里的
CatalogMaxDepth = 5 是假的,前缀补全实际只查 1 层。这条要改行为、且影响真实名录的解析
结果,留给你拍板,不在本提交里动手。
This commit is contained in:
1 parent
102a3e038d
commit
187d2759fd
60 files changed
+769
-377
No files matched your search
+58
-42
@@ -162,7 +162,8 @@ if (-not $tool) {
|
|||||||
$toolVersion = try {
|
$toolVersion = try {
|
||||||
$info = (Get-Item -LiteralPath $tool.Command -ErrorAction Stop).VersionInfo
|
$info = (Get-Item -LiteralPath $tool.Command -ErrorAction Stop).VersionInfo
|
||||||
if ($info.ProductVersion) { $info.ProductVersion } elseif ($info.FileVersion) { $info.FileVersion } else { $null }
|
if ($info.ProductVersion) { $info.ProductVersion } elseif ($info.FileVersion) { $info.FileVersion } else { $null }
|
||||||
} catch { $null }
|
}
|
||||||
|
catch { $null }
|
||||||
Write-Log ("压缩工具:{0}{1}" -f $tool.Name, $(if ($toolVersion) { "($toolVersion)" } else { '' }))
|
Write-Log ("压缩工具:{0}{1}" -f $tool.Name, $(if ($toolVersion) { "($toolVersion)" } else { '' }))
|
||||||
|
|
||||||
$manifest = Read-BaknretManifest -Path $manifestPath
|
$manifest = Read-BaknretManifest -Path $manifestPath
|
||||||
@@ -206,32 +207,32 @@ function Test-ItemSelected {
|
|||||||
function New-ItemRecord {
|
function New-ItemRecord {
|
||||||
param([string]$BaseName, [string]$Source, [string]$ResolvedSource, [string]$Phase)
|
param([string]$BaseName, [string]$Source, [string]$ResolvedSource, [string]$Phase)
|
||||||
return [ordered]@{
|
return [ordered]@{
|
||||||
baseName = $BaseName
|
baseName = $BaseName
|
||||||
source = $Source
|
source = $Source
|
||||||
resolvedSource = $ResolvedSource
|
resolvedSource = $ResolvedSource
|
||||||
roots = @()
|
roots = @()
|
||||||
layouts = @()
|
layouts = @()
|
||||||
catalog = $null
|
catalog = $null
|
||||||
archive = $null
|
archive = $null
|
||||||
action = $null
|
action = $null
|
||||||
reason = $null
|
reason = $null
|
||||||
phase = $Phase
|
phase = $Phase
|
||||||
attemptedAt = (Get-Date).ToString('o')
|
attemptedAt = (Get-Date).ToString('o')
|
||||||
finishedAt = $null
|
finishedAt = $null
|
||||||
durationSec = $null
|
durationSec = $null
|
||||||
exitCode = $null
|
exitCode = $null
|
||||||
verified = $false
|
verified = $false
|
||||||
warnings = $false
|
warnings = $false
|
||||||
attemptWarnings = $false
|
attemptWarnings = $false
|
||||||
encrypted = $false
|
encrypted = $false
|
||||||
security = $null
|
security = $null
|
||||||
sourceFiles = $null
|
sourceFiles = $null
|
||||||
sourceBytes = $null
|
sourceBytes = $null
|
||||||
archiveBytes = $null
|
archiveBytes = $null
|
||||||
sha256 = $null
|
sha256 = $null
|
||||||
lastSuccessAt = $null
|
lastSuccessAt = $null
|
||||||
successCount = 0
|
successCount = 0
|
||||||
failCount = 0
|
failCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +251,8 @@ function Save-ItemRecord {
|
|||||||
# 否则"因为不完整而保留旧归档"之后,下一次就失去保护了。
|
# 否则"因为不完整而保留旧归档"之后,下一次就失去保护了。
|
||||||
if ($Action -eq 'backed-up') {
|
if ($Action -eq 'backed-up') {
|
||||||
$Record.warnings = $ArchiveWarnings
|
$Record.warnings = $ArchiveWarnings
|
||||||
} elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) {
|
}
|
||||||
|
elseif ($previous -and ($previous.PSObject.Properties.Name -contains 'warnings')) {
|
||||||
$Record.warnings = [bool]$previous.warnings
|
$Record.warnings = [bool]$previous.warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +271,8 @@ function Save-ItemRecord {
|
|||||||
if ($Action -eq 'backed-up') {
|
if ($Action -eq 'backed-up') {
|
||||||
$Record.lastSuccessAt = $Record.finishedAt
|
$Record.lastSuccessAt = $Record.finishedAt
|
||||||
$Record.successCount = [int]$Record.successCount + 1
|
$Record.successCount = [int]$Record.successCount + 1
|
||||||
} elseif ($Action -eq 'failed') {
|
}
|
||||||
|
elseif ($Action -eq 'failed') {
|
||||||
$Record.failCount = [int]$Record.failCount + 1
|
$Record.failCount = [int]$Record.failCount + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +408,8 @@ 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 }
|
return [pscustomobject]@{ Ok = $true; ExitCode = 0; Warnings = $warnings; Reason = $null }
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
if (Test-Path -LiteralPath $tempPath) {
|
if (Test-Path -LiteralPath $tempPath) {
|
||||||
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -467,7 +471,8 @@ foreach ($planLine in $lines) {
|
|||||||
|
|
||||||
$planEstimate = if ($planExistingBytes -gt 0) {
|
$planEstimate = if ($planExistingBytes -gt 0) {
|
||||||
[int64][math]::Min([double]$planSourceBytes, [double]$planExistingBytes * 1.3)
|
[int64][math]::Min([double]$planSourceBytes, [double]$planExistingBytes * 1.3)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
# 没有历史归档可比时按"完全不压缩"的悲观值估,宁可报多不报少
|
# 没有历史归档可比时按"完全不压缩"的悲观值估,宁可报多不报少
|
||||||
$planSourceBytes
|
$planSourceBytes
|
||||||
}
|
}
|
||||||
@@ -486,7 +491,8 @@ $freeNowGB = Get-BaknretFreeSpaceGB -Path $BackupDir
|
|||||||
|
|
||||||
if ($spacePlan.Count -eq 0) {
|
if ($spacePlan.Count -eq 0) {
|
||||||
Write-Log '备份前空间预估:本次没有需要重打的条目(源未更新或源不存在),不会写入新归档' -Level INFO
|
Write-Log '备份前空间预估:本次没有需要重打的条目(源未更新或源不存在),不会写入新归档' -Level INFO
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$spacePeak = [double]0
|
$spacePeak = [double]0
|
||||||
$spaceCumulative = [double]0
|
$spaceCumulative = [double]0
|
||||||
foreach ($plan in $spacePlan) {
|
foreach ($plan in $spacePlan) {
|
||||||
@@ -515,9 +521,11 @@ if ($spacePlan.Count -eq 0) {
|
|||||||
|
|
||||||
if ($freeNowGB -lt 0) {
|
if ($freeNowGB -lt 0) {
|
||||||
Write-Log ' 结论:读不到目标卷可用空间,请自行确认是否够用' -Level WARN
|
Write-Log ' 结论:读不到目标卷可用空间,请自行确认是否够用' -Level WARN
|
||||||
} elseif ($peakGB -le $freeNowGB) {
|
}
|
||||||
|
elseif ($peakGB -le $freeNowGB) {
|
||||||
Write-Log (" 结论:空间足够(预计用 {0} GB / 可用 {1} GB)" -f [math]::Round($peakGB, 2), $freeNowGB) -Level INFO
|
Write-Log (" 结论:空间足够(预计用 {0} GB / 可用 {1} GB)" -f [math]::Round($peakGB, 2), $freeNowGB) -Level INFO
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log (" 结论:空间可能不够!预计需要 {0} GB,可用只有 {1} GB,差 {2} GB" -f `
|
Write-Log (" 结论:空间可能不够!预计需要 {0} GB,可用只有 {1} GB,差 {2} GB" -f `
|
||||||
[math]::Round($peakGB, 2), $freeNowGB, [math]::Round($peakGB - $freeNowGB, 2)) -Level WARN
|
[math]::Round($peakGB, 2), $freeNowGB, [math]::Round($peakGB - $freeNowGB, 2)) -Level WARN
|
||||||
Write-Log ' 仍会继续执行:真正放不下的条目会被逐条目守卫跳过。建议先腾空间,或用 -Only / -Skip 分批备份。' -Level WARN
|
Write-Log ' 仍会继续执行:真正放不下的条目会被逐条目守卫跳过。建议先腾空间,或用 -Only / -Skip 分批备份。' -Level WARN
|
||||||
@@ -592,7 +600,8 @@ foreach ($line in $lines) {
|
|||||||
$planCatalogExcludes = @()
|
$planCatalogExcludes = @()
|
||||||
if ($resolved.HasExcludeOverride) {
|
if ($resolved.HasExcludeOverride) {
|
||||||
$planListExcludes = @($resolved.ExcludePatterns)
|
$planListExcludes = @($resolved.ExcludePatterns)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$planCatalogExcludes = @($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
|
$planCatalogExcludes = @($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
|
||||||
}
|
}
|
||||||
Write-BackupEntryPlan -Resolved $resolved -DisplayPath $displayPath `
|
Write-BackupEntryPlan -Resolved $resolved -DisplayPath $displayPath `
|
||||||
@@ -719,7 +728,8 @@ foreach ($line in $lines) {
|
|||||||
# 再逐项翻译成 7z 的 -x! / -xr!,最后去重合并成一次调用的参数。
|
# 再逐项翻译成 7z 的 -x! / -xr!,最后去重合并成一次调用的参数。
|
||||||
$patternSource = if ($resolved.HasExcludeOverride) {
|
$patternSource = if ($resolved.HasExcludeOverride) {
|
||||||
@($resolved.ExcludePatterns)
|
@($resolved.ExcludePatterns)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
@($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
|
@($resolved.Items | ForEach-Object { @($_.Exclude) } | Where-Object { $_ } | Select-Object -Unique)
|
||||||
}
|
}
|
||||||
$allPatterns = @($script:Config.DefaultExcludes) + $patternSource
|
$allPatterns = @($script:Config.DefaultExcludes) + $patternSource
|
||||||
@@ -759,9 +769,11 @@ foreach ($line in $lines) {
|
|||||||
$result = Invoke-BackupItem -SourceItems $liveItems -StagingRoot $stagingRoot `
|
$result = Invoke-BackupItem -SourceItems $liveItems -StagingRoot $stagingRoot `
|
||||||
-FinalPath $finalPath -ExcludePatterns $effectiveExcludes -UseEncryption:$useEncryption `
|
-FinalPath $finalPath -ExcludePatterns $effectiveExcludes -UseEncryption:$useEncryption `
|
||||||
-ProtectPrevious:$protectPrevious -AcceptWarnings:$AcceptWarnings
|
-ProtectPrevious:$protectPrevious -AcceptWarnings:$AcceptWarnings
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$result = [pscustomobject]@{ Ok = $false; ExitCode = $null; Warnings = $false; Reason = "准备归档内容失败:$_" }
|
$result = [pscustomobject]@{ Ok = $false; ExitCode = $null; Warnings = $false; Reason = "准备归档内容失败:$_" }
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-BaknretArchiveStaging -Root $stagingRoot
|
Remove-BaknretArchiveStaging -Root $stagingRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,7 +794,8 @@ foreach ($line in $lines) {
|
|||||||
if ($result.Warnings) {
|
if ($result.Warnings) {
|
||||||
Write-Log "备份成功(压缩工具报告了警告,可能有文件被占用而没打进归档): $displayPath" -Level WARN
|
Write-Log "备份成功(压缩工具报告了警告,可能有文件被占用而没打进归档): $displayPath" -Level WARN
|
||||||
Write-Log ' 该归档在 manifest 里标记为 warnings=true;如果以后现有归档是完整的,会拒绝被它覆盖' -Level WARN
|
Write-Log ' 该归档在 manifest 里标记为 warnings=true;如果以后现有归档是完整的,会拒绝被它覆盖' -Level WARN
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log "备份成功: $baseName" -Level INFO
|
Write-Log "备份成功: $baseName" -Level INFO
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +836,8 @@ foreach ($line in $lines) {
|
|||||||
Write-Log (" {0} 个对象的安全描述符读不到(恢复后它们的属主/ACL 会是新建对象的默认值),例如:{1}" -f `
|
Write-Log (" {0} 个对象的安全描述符读不到(恢复后它们的属主/ACL 会是新建对象的默认值),例如:{1}" -f `
|
||||||
$capture.Errors, ($unreadable -join '、')) -Level WARN
|
$capture.Errors, ($unreadable -join '、')) -Level WARN
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$securityFailed++
|
$securityFailed++
|
||||||
Write-Log "安全描述符采集/写盘失败:$displayPath —— $_" -Level WARN
|
Write-Log "安全描述符采集/写盘失败:$displayPath —— $_" -Level WARN
|
||||||
$record.security = [ordered]@{ file = $sidecarName; error = "$_" }
|
$record.security = [ordered]@{ file = $sidecarName; error = "$_" }
|
||||||
@@ -862,7 +876,8 @@ foreach ($line in $lines) {
|
|||||||
|
|
||||||
if ($DryRun) {
|
if ($DryRun) {
|
||||||
Write-Log '试运行:manifest 与归档都不会被写入' -Level INFO
|
Write-Log '试运行:manifest 与归档都不会被写入' -Level INFO
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
# manifest 里写了 archive 的记录,磁盘上就必须真有那个文件
|
# manifest 里写了 archive 的记录,磁盘上就必须真有那个文件
|
||||||
$clearedArchiveFields = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
|
$clearedArchiveFields = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
|
||||||
if ($clearedArchiveFields.Count -gt 0) {
|
if ($clearedArchiveFields.Count -gt 0) {
|
||||||
@@ -888,7 +903,7 @@ if (-not $DryRun -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
|
|||||||
foreach ($key in $seenBaseNames.Keys) { $known[$key] = $true }
|
foreach ($key in $seenBaseNames.Keys) { $known[$key] = $true }
|
||||||
|
|
||||||
$orphanArchives = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
$orphanArchives = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') -and -not $known.ContainsKey($_.BaseName) })
|
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') -and -not $known.ContainsKey($_.BaseName) })
|
||||||
|
|
||||||
if ($orphanArchives.Count -gt 0) {
|
if ($orphanArchives.Count -gt 0) {
|
||||||
Write-Log ("发现 {0} 个孤儿归档(当前清单里没有任何条目指向,恢复不到,注意别误删):" -f $orphanArchives.Count) -Level WARN
|
Write-Log ("发现 {0} 个孤儿归档(当前清单里没有任何条目指向,恢复不到,注意别误删):" -f $orphanArchives.Count) -Level WARN
|
||||||
@@ -896,7 +911,8 @@ if (-not $DryRun -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
|
|||||||
$inManifest = $manifest.items.Contains($orphan.BaseName)
|
$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-Log (" - {0}({1:N1} MB,{2}){3}" -f $orphan.Name, ($orphan.Length / 1MB), $orphan.LastWriteTime, $(if ($inManifest) { ';manifest 里还留着它的历史记录,但清单里已经没有了' } else { '' })) -Level WARN
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log '孤儿归档审计:没有发现(所有归档都有清单条目指向)' -Level DEBUG
|
Write-Log '孤儿归档审计:没有发现(所有归档都有清单条目指向)' -Level DEBUG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return Import-PowerShellDataFile -LiteralPath $Path -ErrorAction Stop
|
return Import-PowerShellDataFile -LiteralPath $Path -ErrorAction Stop
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$firstLine = ([string]$_.Exception.Message) -split "`r?`n" | Select-Object -First 1
|
$firstLine = ([string]$_.Exception.Message) -split "`r?`n" | Select-Object -First 1
|
||||||
Write-Log "psd1 里有 Import-PowerShellDataFile 不接受的表达式($firstLine),改用 PowerShell 求值:$Path" -Level DEBUG
|
Write-Log "psd1 里有 Import-PowerShellDataFile 不接受的表达式($firstLine),改用 PowerShell 求值:$Path" -Level DEBUG
|
||||||
$raw = [System.IO.File]::ReadAllText($Path)
|
$raw = [System.IO.File]::ReadAllText($Path)
|
||||||
|
|||||||
@@ -18,13 +18,14 @@
|
|||||||
$parts = @($pathPart -split '\+' | Where-Object { -not [string]::IsNullOrEmpty($_) })
|
$parts = @($pathPart -split '\+' | Where-Object { -not [string]::IsNullOrEmpty($_) })
|
||||||
|
|
||||||
$parts = @($parts | ForEach-Object {
|
$parts = @($parts | ForEach-Object {
|
||||||
if ($_ -match '^([A-Za-z])_$') { "$($matches[1]):" } else { $_ }
|
if ($_ -match '^([A-Za-z])_$') { "$($matches[1]):" } else { $_ }
|
||||||
})
|
})
|
||||||
|
|
||||||
$reconstructed = ($parts -join '\') + '\' + $folderPart
|
$reconstructed = ($parts -join '\') + '\' + $folderPart
|
||||||
Write-Log "逆向解析:$FileName -> $reconstructed" -Level DEBUG
|
Write-Log "逆向解析:$FileName -> $reconstructed" -Level DEBUG
|
||||||
return $reconstructed
|
return $reconstructed
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "无法解析备份文件名:$FileName" -Level WARN
|
Write-Log "无法解析备份文件名:$FileName" -Level WARN
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,16 @@
|
|||||||
if ($tokens[0] -eq '+') {
|
if ($tokens[0] -eq '+') {
|
||||||
$direction = 'backup'
|
$direction = 'backup'
|
||||||
$tokens = @($tokens | Select-Object -Skip 1)
|
$tokens = @($tokens | Select-Object -Skip 1)
|
||||||
} elseif ($tokens[0] -eq '-') {
|
}
|
||||||
|
elseif ($tokens[0] -eq '-') {
|
||||||
$direction = 'restore'
|
$direction = 'restore'
|
||||||
$tokens = @($tokens | Select-Object -Skip 1)
|
$tokens = @($tokens | Select-Object -Skip 1)
|
||||||
} elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '+') {
|
}
|
||||||
|
elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '+') {
|
||||||
$direction = 'backup'
|
$direction = 'backup'
|
||||||
$tokens[0] = $tokens[0].Substring(1)
|
$tokens[0] = $tokens[0].Substring(1)
|
||||||
} elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '-') {
|
}
|
||||||
|
elseif ($tokens[0].Length -gt 1 -and $tokens[0][0] -eq '-') {
|
||||||
$direction = 'restore'
|
$direction = 'restore'
|
||||||
$tokens[0] = $tokens[0].Substring(1)
|
$tokens[0] = $tokens[0].Substring(1)
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,8 @@
|
|||||||
if ($firstMarker -lt 0) {
|
if ($firstMarker -lt 0) {
|
||||||
$targetText = ($tokens -join ' ')
|
$targetText = ($tokens -join ' ')
|
||||||
$markerTokens = @()
|
$markerTokens = @()
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$targetText = (($tokens[0..($firstMarker - 1)]) -join ' ')
|
$targetText = (($tokens[0..($firstMarker - 1)]) -join ' ')
|
||||||
$markerTokens = @($tokens[$firstMarker..($tokens.Count - 1)])
|
$markerTokens = @($tokens[$firstMarker..($tokens.Count - 1)])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ public static int Enable(string name) {
|
|||||||
} finally { CloseHandle(token); }
|
} finally { CloseHandle(token); }
|
||||||
}
|
}
|
||||||
'@
|
'@
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "特权启用代码编译失败(本次不启用任何特权):$($_.Exception.Message)" -Level WARN
|
Write-Log "特权启用代码编译失败(本次不启用任何特权):$($_.Exception.Message)" -Level WARN
|
||||||
$result.Failed = @($Name)
|
$result.Failed = @($Name)
|
||||||
return $result
|
return $result
|
||||||
@@ -71,7 +72,8 @@ public static int Enable(string name) {
|
|||||||
$cacheKey = $privilege
|
$cacheKey = $privilege
|
||||||
if ($script:BaknretPrivilegeState.ContainsKey($cacheKey)) {
|
if ($script:BaknretPrivilegeState.ContainsKey($cacheKey)) {
|
||||||
$state = $script:BaknretPrivilegeState[$cacheKey]
|
$state = $script:BaknretPrivilegeState[$cacheKey]
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$state = [Baknret.Privileges]::Enable($privilege)
|
$state = [Baknret.Privileges]::Enable($privilege)
|
||||||
$script:BaknretPrivilegeState[$cacheKey] = $state
|
$script:BaknretPrivilegeState[$cacheKey] = $state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
[System.IO.FileMode]::OpenOrCreate,
|
[System.IO.FileMode]::OpenOrCreate,
|
||||||
[System.IO.FileAccess]::ReadWrite,
|
[System.IO.FileAccess]::ReadWrite,
|
||||||
[System.IO.FileShare]::None)
|
[System.IO.FileShare]::None)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
# 不能只写 `catch [System.IO.IOException]`:PowerShell 会把 .NET 方法抛出的异常包成
|
# 不能只写 `catch [System.IO.IOException]`:PowerShell 会把 .NET 方法抛出的异常包成
|
||||||
# MethodInvocationException,按内层类型做的 catch 接不住,于是锁被占用时会直接抛出去,
|
# MethodInvocationException,按内层类型做的 catch 接不住,于是锁被占用时会直接抛出去,
|
||||||
# 而不是按约定返回 $null 让调用方明确失败(实测踩到,被自己的断言逮住)。
|
# 而不是按约定返回 $null 让调用方明确失败(实测踩到,被自己的断言逮住)。
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
if (-not $Lock) { return }
|
if (-not $Lock) { return }
|
||||||
try {
|
try {
|
||||||
$Lock.Dispose()
|
$Lock.Dispose()
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "释放运行锁失败(进程退出时会自动释放):$_" -Level WARN
|
Write-Log "释放运行锁失败(进程退出时会自动释放):$_" -Level WARN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,8 @@
|
|||||||
try {
|
try {
|
||||||
$evaluated = [scriptblock]::Create($expression).Invoke()
|
$evaluated = [scriptblock]::Create($expression).Invoke()
|
||||||
if ($null -ne $evaluated) { $replacement = ([string]($evaluated)).Trim() }
|
if ($null -ne $evaluated) { $replacement = ([string]($evaluated)).Trim() }
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "名录路径里的表达式求值失败:$expression —— $($_.Exception.Message)" -Level WARN
|
Write-Log "名录路径里的表达式求值失败:$expression —— $($_.Exception.Message)" -Level WARN
|
||||||
}
|
}
|
||||||
$value = $value.Substring(0, $start) + $replacement + $value.Substring($end + 1)
|
$value = $value.Substring(0, $start) + $replacement + $value.Substring($end + 1)
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return @(Get-ChildItem -LiteralPath $Parent -Directory -Force -ErrorAction SilentlyContinue |
|
return @(Get-ChildItem -LiteralPath $Parent -Directory -Force -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Name -ieq $Name -or $_.Name -imatch $pattern } |
|
Where-Object { $_.Name -ieq $Name -or $_.Name -imatch $pattern } |
|
||||||
Sort-Object Name |
|
Sort-Object Name |
|
||||||
Select-Object -ExpandProperty FullName)
|
Select-Object -ExpandProperty FullName)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
Where-Object { $_ -notin @('&', '%', '+') }
|
Where-Object { $_ -notin @('&', '%', '+') }
|
||||||
|
|
||||||
$clean = -join ($Name.Trim().ToCharArray() | ForEach-Object {
|
$clean = -join ($Name.Trim().ToCharArray() | ForEach-Object {
|
||||||
if ($_ -in $invalidChars) { '_' } else { $_ }
|
if ($_ -in $invalidChars) { '_' } else { $_ }
|
||||||
})
|
})
|
||||||
$clean = $clean -replace ':', '_'
|
$clean = $clean -replace ':', '_'
|
||||||
return $clean.Trim()
|
return $clean.Trim()
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,13 @@
|
|||||||
# 先把名字读进变量,再在 finally 里删临时目录;
|
# 先把名字读进变量,再在 finally 里删临时目录;
|
||||||
# 直接在 return 里管道 Get-ChildItem 会被 finally 抢在前面,拿到空数组。
|
# 直接在 return 里管道 Get-ChildItem 会被 finally 抢在前面,拿到空数组。
|
||||||
$names = @(Get-ChildItem -LiteralPath $staging -Force -ErrorAction SilentlyContinue |
|
$names = @(Get-ChildItem -LiteralPath $staging -Force -ErrorAction SilentlyContinue |
|
||||||
Select-Object -ExpandProperty Name)
|
Select-Object -ExpandProperty Name)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG
|
Write-Log "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG
|
||||||
$names = @()
|
$names = @()
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
$pathPart = ($pathParts | ForEach-Object { $_.Trim() }) -join '+'
|
$pathPart = ($pathParts | ForEach-Object { $_.Trim() }) -join '+'
|
||||||
$baseName = if ([string]::IsNullOrEmpty($pathPart)) {
|
$baseName = if ([string]::IsNullOrEmpty($pathPart)) {
|
||||||
$folderName
|
$folderName
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
"${folderName}_from_${pathPart}"
|
"${folderName}_from_${pathPart}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +35,8 @@
|
|||||||
Where-Object { $_ -notin @('&', '%', '+') }
|
Where-Object { $_ -notin @('&', '%', '+') }
|
||||||
|
|
||||||
$baseName = -join ($baseName.ToCharArray() | ForEach-Object {
|
$baseName = -join ($baseName.ToCharArray() | ForEach-Object {
|
||||||
if ($_ -in $invalidChars) { '_' } else { $_ }
|
if ($_ -in $invalidChars) { '_' } else { $_ }
|
||||||
})
|
})
|
||||||
$baseName = $baseName -replace ':', '_'
|
$baseName = $baseName -replace ':', '_'
|
||||||
|
|
||||||
Write-Log "生成文件基础名:$baseName" -Level DEBUG
|
Write-Log "生成文件基础名:$baseName" -Level DEBUG
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$loaded = Import-BaknretDataFile -Path $Path
|
$loaded = Import-BaknretDataFile -Path $Path
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "配置文件读取失败(改用默认值):$Path —— $_" -Level WARN
|
Write-Log "配置文件读取失败(改用默认值):$Path —— $_" -Level WARN
|
||||||
return $defaults
|
return $defaults
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,8 @@
|
|||||||
foreach ($subKey in $defaults[$key].Keys) { $merged[$subKey] = $defaults[$key][$subKey] }
|
foreach ($subKey in $defaults[$key].Keys) { $merged[$subKey] = $defaults[$key][$subKey] }
|
||||||
foreach ($subKey in $loaded[$key].Keys) { $merged[$subKey] = $loaded[$key][$subKey] }
|
foreach ($subKey in $loaded[$key].Keys) { $merged[$subKey] = $loaded[$key][$subKey] }
|
||||||
$defaults[$key] = $merged
|
$defaults[$key] = $merged
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$defaults[$key] = $loaded[$key]
|
$defaults[$key] = $loaded[$key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
$drive = Get-PSDrive -Name $qualifier.TrimEnd(':') -ErrorAction Stop
|
$drive = Get-PSDrive -Name $qualifier.TrimEnd(':') -ErrorAction Stop
|
||||||
if ($null -eq $drive.Free) { return -1 }
|
if ($null -eq $drive.Free) { return -1 }
|
||||||
return [math]::Round($drive.Free / 1GB, 2)
|
return [math]::Round($drive.Free / 1GB, 2)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,10 +40,12 @@
|
|||||||
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||||
try {
|
try {
|
||||||
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "口令输入失败:$_" -Level ERROR
|
Write-Log "口令输入失败:$_" -Level ERROR
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
try {
|
try {
|
||||||
$regex = [System.Text.RegularExpressions.Regex]::new(
|
$regex = [System.Text.RegularExpressions.Regex]::new(
|
||||||
$Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
$Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return [pscustomobject]@{ Arguments = @(); Matches = 0; Error = "排除正则非法:$Pattern —— $($_.Exception.Message)" }
|
return [pscustomobject]@{ Arguments = @(); Matches = 0; Error = "排除正则非法:$Pattern —— $($_.Exception.Message)" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,29 +28,31 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
$record = [pscustomobject]@{
|
$record = [pscustomobject]@{
|
||||||
p = $Key
|
p = $Key
|
||||||
k = $Kind
|
k = $Kind
|
||||||
s = $null
|
s = $null
|
||||||
o = $null
|
o = $null
|
||||||
g = $null
|
g = $null
|
||||||
e = $null
|
e = $null
|
||||||
Protected = $false
|
Protected = $false
|
||||||
Explicit = 0
|
Explicit = 0
|
||||||
Inherited = 0
|
Inherited = 0
|
||||||
Inheritable = 0
|
Inheritable = 0
|
||||||
InheritedSignatures = @()
|
InheritedSignatures = @()
|
||||||
AllSignatures = @()
|
AllSignatures = @()
|
||||||
Analyzed = $false
|
Analyzed = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
$acl = $null
|
$acl = $null
|
||||||
try {
|
try {
|
||||||
if ($IncludeSacl) {
|
if ($IncludeSacl) {
|
||||||
$acl = Get-Acl -LiteralPath $Path -Audit -ErrorAction Stop
|
$acl = Get-Acl -LiteralPath $Path -Audit -ErrorAction Stop
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$acl = Get-Acl -LiteralPath $Path -ErrorAction Stop
|
$acl = Get-Acl -LiteralPath $Path -ErrorAction Stop
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$record.e = $_.Exception.Message
|
$record.e = $_.Exception.Message
|
||||||
return $record
|
return $record
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,8 @@
|
|||||||
try {
|
try {
|
||||||
# 陈旧继承 ACE 要固化成显式 ACE,否则恢复后会消失(见 Get-BaknretSecuritySddlWithStale)
|
# 陈旧继承 ACE 要固化成显式 ACE,否则恢复后会消失(见 Get-BaknretSecuritySddlWithStale)
|
||||||
$record.s = Get-BaknretSecuritySddlWithStale -Acl $acl -ParentSignatures $ParentSignatures
|
$record.s = Get-BaknretSecuritySddlWithStale -Acl $acl -ParentSignatures $ParentSignatures
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$record.e = $_.Exception.Message
|
$record.e = $_.Exception.Message
|
||||||
}
|
}
|
||||||
if (-not $record.s) {
|
if (-not $record.s) {
|
||||||
@@ -89,7 +92,8 @@
|
|||||||
}
|
}
|
||||||
$record.Inheritable = $inheritable
|
$record.Inheritable = $inheritable
|
||||||
$record.Analyzed = $true
|
$record.Analyzed = $true
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
# 分析失败时一律当成"需要保留"(Analyzed=$false 会让 keeper 直接保留)
|
# 分析失败时一律当成"需要保留"(Analyzed=$false 会让 keeper 直接保留)
|
||||||
$record.Analyzed = $false
|
$record.Analyzed = $false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,8 @@
|
|||||||
|
|
||||||
if ($Mode -eq 'Full') {
|
if ($Mode -eq 'Full') {
|
||||||
$records.Add($record)
|
$records.Add($record)
|
||||||
} elseif (Test-BaknretSecurityRecordNeeded -Record $record `
|
}
|
||||||
|
elseif (Test-BaknretSecurityRecordNeeded -Record $record `
|
||||||
-ParentOwner $frame.Owner -ParentGroup $frame.Group `
|
-ParentOwner $frame.Owner -ParentGroup $frame.Group `
|
||||||
-ParentInheritable $frame.Inheritable -ParentSignatures $frame.Signatures) {
|
-ParentInheritable $frame.Inheritable -ParentSignatures $frame.Signatures) {
|
||||||
$records.Add($record)
|
$records.Add($record)
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
$rebuilt = $null
|
$rebuilt = $null
|
||||||
if ($Acl -is [System.Security.AccessControl.DirectorySecurity]) {
|
if ($Acl -is [System.Security.AccessControl.DirectorySecurity]) {
|
||||||
$rebuilt = New-Object System.Security.AccessControl.DirectorySecurity
|
$rebuilt = New-Object System.Security.AccessControl.DirectorySecurity
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$rebuilt = New-Object System.Security.AccessControl.FileSecurity
|
$rebuilt = New-Object System.Security.AccessControl.FileSecurity
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +58,13 @@
|
|||||||
try {
|
try {
|
||||||
$rebuilt.SetOwner($Acl.GetOwner($sid))
|
$rebuilt.SetOwner($Acl.GetOwner($sid))
|
||||||
$sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Owner
|
$sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Owner
|
||||||
} catch { }
|
}
|
||||||
|
catch { }
|
||||||
try {
|
try {
|
||||||
$rebuilt.SetGroup($Acl.GetGroup($sid))
|
$rebuilt.SetGroup($Acl.GetGroup($sid))
|
||||||
$sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Group
|
$sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Group
|
||||||
} catch { }
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
$rebuilt.SetAccessRuleProtection($true, $false)
|
$rebuilt.SetAccessRuleProtection($true, $false)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
TotalSize = [long]$totalSize
|
TotalSize = [long]$totalSize
|
||||||
LatestModifiedTime = ($items | Measure-Object -Property LastWriteTime -Maximum -ErrorAction SilentlyContinue).Maximum
|
LatestModifiedTime = ($items | Measure-Object -Property LastWriteTime -Maximum -ErrorAction SilentlyContinue).Maximum
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "无法读取文件夹摘要:$FolderPath" -Level WARN
|
Write-Log "无法读取文件夹摘要:$FolderPath" -Level WARN
|
||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
FileCount = 0
|
FileCount = 0
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
$files = @(Get-ChildItem -LiteralPath $path -File -Recurse -ErrorAction SilentlyContinue)
|
$files = @(Get-ChildItem -LiteralPath $path -File -Recurse -ErrorAction SilentlyContinue)
|
||||||
$fileCount += $files.Count
|
$fileCount += $files.Count
|
||||||
$totalSize += [int64](@($files | Measure-Object -Property Length -Sum).Sum)
|
$totalSize += [int64](@($files | Measure-Object -Property Length -Sum).Sum)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$fileCount++
|
$fileCount++
|
||||||
$totalSize += [int64]$item.Length
|
$totalSize += [int64]$item.Length
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,8 @@
|
|||||||
try {
|
try {
|
||||||
$cpuCores = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).NumberOfLogicalProcessors
|
$cpuCores = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).NumberOfLogicalProcessors
|
||||||
$threads = [math]::Max(1, $cpuCores - 1)
|
$threads = [math]::Max(1, $cpuCores - 1)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$threads = 2
|
$threads = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +53,9 @@
|
|||||||
# 只放压缩相关开关。输出开关(-bso0/-bsp0 或默认进度)必须由调用方
|
# 只放压缩相关开关。输出开关(-bso0/-bsp0 或默认进度)必须由调用方
|
||||||
# 单独加一次:7z 对同一个开关出现两次会直接报
|
# 单独加一次:7z 对同一个开关出现两次会直接报
|
||||||
# "Multiple instances for switch" 并以退出码 7 失败。
|
# "Multiple instances for switch" 并以退出码 7 失败。
|
||||||
Argument = @('a', '-t7z', "-mx=$Level", "-md=$dictSize", '-ms=on', "-mmt=$threads")
|
Argument = @('a', '-t7z', "-mx=$Level", "-md=$dictSize", '-ms=on', "-mmt=$threads")
|
||||||
FileCount = $fileCount
|
FileCount = $fileCount
|
||||||
TotalSize = $totalSize
|
TotalSize = $totalSize
|
||||||
TotalSizeMB = $totalSizeMB
|
TotalSizeMB = $totalSizeMB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,8 @@
|
|||||||
if ($script:CatalogCache.ContainsKey($Path) -and $script:CatalogCache[$Path].Stamp -eq $stamp) {
|
if ($script:CatalogCache.ContainsKey($Path) -and $script:CatalogCache[$Path].Stamp -eq $stamp) {
|
||||||
return $script:CatalogCache[$Path].Data
|
return $script:CatalogCache[$Path].Data
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$stamp = $null
|
$stamp = $null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,8 @@
|
|||||||
$data = $null
|
$data = $null
|
||||||
try {
|
try {
|
||||||
$data = Import-BaknretDataFile -Path $Path
|
$data = Import-BaknretDataFile -Path $Path
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "软件名录读取失败:$Path —— $_" -Level ERROR
|
Write-Log "软件名录读取失败:$Path —— $_" -Level ERROR
|
||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,8 @@
|
|||||||
$candidates = @()
|
$candidates = @()
|
||||||
if (Test-Path -LiteralPath $declared) {
|
if (Test-Path -LiteralPath $declared) {
|
||||||
$candidates = @($declared)
|
$candidates = @($declared)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$parent = Split-Path -Path $declared -Parent
|
$parent = Split-Path -Path $declared -Parent
|
||||||
$leafName = Split-Path -Path $declared -Leaf
|
$leafName = Split-Path -Path $declared -Leaf
|
||||||
if ($parent -and $leafName -and (Test-Path -LiteralPath $parent)) {
|
if ($parent -and $leafName -and (Test-Path -LiteralPath $parent)) {
|
||||||
@@ -176,7 +179,8 @@
|
|||||||
|
|
||||||
if ($errors.Count -gt 0) {
|
if ($errors.Count -gt 0) {
|
||||||
Write-Log ("名录条目 {0} 有问题:{1}" -f $name, ($errors -join ';')) -Level ERROR
|
Write-Log ("名录条目 {0} 有问题:{1}" -f $name, ($errors -join ';')) -Level ERROR
|
||||||
} elseif ($missing.Count -gt 0) {
|
}
|
||||||
|
elseif ($missing.Count -gt 0) {
|
||||||
Write-Log ("名录:{0} 有 {1} 个 Slot 的路径当前不存在:{2}" -f $name, $missing.Count, (($missing | ForEach-Object { $_.Declared }) -join ';')) -Level DEBUG
|
Write-Log ("名录:{0} 有 {1} 个 Slot 的路径当前不存在:{2}" -f $name, $missing.Count, (($missing | ForEach-Object { $_.Declared }) -join ';')) -Level DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
try {
|
try {
|
||||||
$process.WaitForExit()
|
$process.WaitForExit()
|
||||||
return $process.ExitCode
|
return $process.ExitCode
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$process.Dispose()
|
$process.Dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
# 7.x:单次原子替换(MoveFileEx + REPLACE_EXISTING)
|
# 7.x:单次原子替换(MoveFileEx + REPLACE_EXISTING)
|
||||||
[System.IO.File]::Move($TempPath, $DestinationPath, $true)
|
[System.IO.File]::Move($TempPath, $DestinationPath, $true)
|
||||||
return
|
return
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "File.Move(overwrite) 不可用(5.1 没有这个重载),改用 File.Replace:$_" -Level DEBUG
|
Write-Log "File.Move(overwrite) 不可用(5.1 没有这个重载),改用 File.Replace:$_" -Level DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,13 @@
|
|||||||
if ($item.IsFile) {
|
if ($item.IsFile) {
|
||||||
try {
|
try {
|
||||||
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
|
Write-Log ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
|
||||||
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
|
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +59,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $Root
|
return $Root
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
try {
|
try {
|
||||||
Remove-BaknretArchiveStaging -Root $Root
|
Remove-BaknretArchiveStaging -Root $Root
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
# 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径
|
# 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径
|
||||||
Write-Log ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
|
Write-Log ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
compressor = $parsed.compressor
|
compressor = $parsed.compressor
|
||||||
items = $items
|
items = $items
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "manifest 解析失败(将重新建立):$Path —— $_" -Level WARN
|
Write-Log "manifest 解析失败(将重新建立):$Path —— $_" -Level WARN
|
||||||
return $empty
|
return $empty
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
ErrorCount = $parsed.errorCount
|
ErrorCount = $parsed.errorCount
|
||||||
Records = @($records)
|
Records = @($records)
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "安全描述符文件解析失败:$Path —— $($_.Exception.Message)" -Level WARN
|
Write-Log "安全描述符文件解析失败:$Path —— $($_.Exception.Message)" -Level WARN
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
try {
|
try {
|
||||||
# Directory.Delete(path, recursive: $false) 删的只是重解析点,不碰目标内容
|
# Directory.Delete(path, recursive: $false) 删的只是重解析点,不碰目标内容
|
||||||
[System.IO.Directory]::Delete($Path, $false)
|
[System.IO.Directory]::Delete($Path, $false)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,8 @@
|
|||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($leaf) -or [string]::IsNullOrWhiteSpace($real)) {
|
if ([string]::IsNullOrWhiteSpace($leaf) -or [string]::IsNullOrWhiteSpace($real)) {
|
||||||
$errorText = "无法从路径里拆出末级名:$real"
|
$errorText = "无法从路径里拆出末级名:$real"
|
||||||
} else {
|
}
|
||||||
|
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
|
-Origin 'path' -Exists $exists -IsFile $isFile
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,8 @@
|
|||||||
$catalog = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
|
$catalog = Get-SoftwareCatalog -Path $CatalogPath -MaxDepth $MaxDepth
|
||||||
if (-not $catalog.ContainsKey($Entry.Path)) {
|
if (-not $catalog.ContainsKey($Entry.Path)) {
|
||||||
$errorText = "软件名录里没有 '$($Entry.Path)'"
|
$errorText = "软件名录里没有 '$($Entry.Path)'"
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$catalogEntry = $catalog[$Entry.Path]
|
$catalogEntry = $catalog[$Entry.Path]
|
||||||
# 名录条目自身有问题(Slot 缺 Path、前缀补全命中多个目录……)时整条失败:
|
# 名录条目自身有问题(Slot 缺 Path、前缀补全命中多个目录……)时整条失败:
|
||||||
# 继续跑只会"少打包一块"或"任选一个目录",那正是最该避免的静默错误。
|
# 继续跑只会"少打包一块"或"任选一个目录",那正是最该避免的静默错误。
|
||||||
@@ -91,7 +93,8 @@
|
|||||||
if ($overridePath -and $slots.Count -ne 1) {
|
if ($overridePath -and $slots.Count -ne 1) {
|
||||||
$blocking = ("'{0}' 有 {1} 个 Slot,不能用一个 `::` / `@ Path=` 覆盖路径;请写清楚是哪个 Slot" -f `
|
$blocking = ("'{0}' 有 {1} 个 Slot,不能用一个 `::` / `@ Path=` 覆盖路径;请写清楚是哪个 Slot" -f `
|
||||||
$Entry.Path, $slots.Count)
|
$Entry.Path, $slots.Count)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
foreach ($slot in $slots) {
|
foreach ($slot in $slots) {
|
||||||
$resolvedPath = $slot.Resolved
|
$resolvedPath = $slot.Resolved
|
||||||
$exists = $slot.Exists
|
$exists = $slot.Exists
|
||||||
@@ -122,7 +125,8 @@
|
|||||||
$includeTexts = @()
|
$includeTexts = @()
|
||||||
if ($hasIncludeOverride) {
|
if ($hasIncludeOverride) {
|
||||||
$includeTexts = @($entryInclude)
|
$includeTexts = @($entryInclude)
|
||||||
} elseif ($catalogEntry) {
|
}
|
||||||
|
elseif ($catalogEntry) {
|
||||||
foreach ($slot in @($catalogEntry.Slots)) { $includeTexts += @($slot.Include) }
|
foreach ($slot in @($catalogEntry.Slots)) { $includeTexts += @($slot.Include) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +186,8 @@
|
|||||||
if (-not $key) { continue }
|
if (-not $key) { continue }
|
||||||
if ($seen.ContainsKey($key)) {
|
if ($seen.ContainsKey($key)) {
|
||||||
$collisions += ("'{0}'({1} 与 {2})" -f $item.ArchivePath, $seen[$key], $item.RealPath)
|
$collisions += ("'{0}'({1} 与 {2})" -f $item.ArchivePath, $seen[$key], $item.RealPath)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$seen[$key] = $item.RealPath
|
$seen[$key] = $item.RealPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +214,8 @@
|
|||||||
$encrypt = $false
|
$encrypt = $false
|
||||||
if ($hasEncryptOverride) {
|
if ($hasEncryptOverride) {
|
||||||
$encrypt = [bool]$overrides['Encrypt']
|
$encrypt = [bool]$overrides['Encrypt']
|
||||||
} elseif ($catalogEntry) {
|
}
|
||||||
|
elseif ($catalogEntry) {
|
||||||
$slots = @($catalogEntry.Slots)
|
$slots = @($catalogEntry.Slots)
|
||||||
$encryptedSlots = @($slots | Where-Object { $_.Encrypt })
|
$encryptedSlots = @($slots | Where-Object { $_.Encrypt })
|
||||||
$encrypt = $encryptedSlots.Count -gt 0
|
$encrypt = $encryptedSlots.Count -gt 0
|
||||||
|
|||||||
@@ -50,9 +50,11 @@
|
|||||||
$relative = $null
|
$relative = $null
|
||||||
if ($key -ieq $root) {
|
if ($key -ieq $root) {
|
||||||
$relative = ''
|
$relative = ''
|
||||||
} elseif ($key.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
}
|
||||||
|
elseif ($key.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
$relative = $key.Substring($prefix.Length)
|
$relative = $key.Substring($prefix.Length)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$selected += [pscustomobject]@{ Relative = $relative; Record = $record }
|
$selected += [pscustomobject]@{ Relative = $relative; Record = $record }
|
||||||
@@ -61,8 +63,8 @@
|
|||||||
if ($selected.Count -eq 0) { return $result }
|
if ($selected.Count -eq 0) { return $result }
|
||||||
|
|
||||||
$ordered = @($selected | Sort-Object -Property `
|
$ordered = @($selected | Sort-Object -Property `
|
||||||
@{ Expression = { @(($_.Relative) -split '\\').Count } }, `
|
@{ Expression = { @(($_.Relative) -split '\\').Count } }, `
|
||||||
@{ Expression = { $_.Relative } })
|
@{ Expression = { $_.Relative } })
|
||||||
|
|
||||||
foreach ($entry in $ordered) {
|
foreach ($entry in $ordered) {
|
||||||
$target = if ($entry.Relative) { Join-Path $TargetPath $entry.Relative } else { $TargetPath }
|
$target = if ($entry.Relative) { Join-Path $TargetPath $entry.Relative } else { $TargetPath }
|
||||||
@@ -82,18 +84,21 @@
|
|||||||
try {
|
try {
|
||||||
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope All
|
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope All
|
||||||
$result.Applied++
|
$result.Applied++
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$fullError = $_
|
$fullError = $_
|
||||||
try {
|
try {
|
||||||
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope OwnerAndAccess
|
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope OwnerAndAccess
|
||||||
$result.OwnerFailed++
|
$result.OwnerFailed++
|
||||||
$result.Failures += ("{0}:属组未恢复,属主与 DACL 已恢复({1})" -f $target, $fullError.Exception.Message)
|
$result.Failures += ("{0}:属组未恢复,属主与 DACL 已恢复({1})" -f $target, $fullError.Exception.Message)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
try {
|
try {
|
||||||
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope AccessOnly
|
Set-BaknretObjectSecurity -Item $item -Sddl $sddl -Scope AccessOnly
|
||||||
$result.OwnerFailed++
|
$result.OwnerFailed++
|
||||||
$result.Failures += ("{0}:属主/属组未恢复({1}),已只恢复 DACL" -f $target, $_.Exception.Message)
|
$result.Failures += ("{0}:属主/属组未恢复({1}),已只恢复 DACL" -f $target, $_.Exception.Message)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$result.Failed++
|
$result.Failed++
|
||||||
$result.Failures += ("{0}:{1}" -f $target, $_.Exception.Message)
|
$result.Failures += ("{0}:{1}" -f $target, $_.Exception.Message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
|
|
||||||
if ($Item.PSIsContainer) {
|
if ($Item.PSIsContainer) {
|
||||||
$sd = New-Object System.Security.AccessControl.DirectorySecurity
|
$sd = New-Object System.Security.AccessControl.DirectorySecurity
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$sd = New-Object System.Security.AccessControl.FileSecurity
|
$sd = New-Object System.Security.AccessControl.FileSecurity
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,8 @@
|
|||||||
|
|
||||||
if ($PSVersionTable.PSEdition -eq 'Core') {
|
if ($PSVersionTable.PSEdition -eq 'Core') {
|
||||||
[System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $sd)
|
[System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $sd)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$Item.SetAccessControl($sd)
|
$Item.SetAccessControl($sd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,8 @@
|
|||||||
foreach ($component in $components) {
|
foreach ($component in $components) {
|
||||||
if ([regex]::IsMatch($component, $regexText, $options)) { return $true }
|
if ([regex]::IsMatch($component, $regexText, $options)) { return $true }
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "排除正则非法,安全描述符采集按'不排除'处理:$regexText —— $($_.Exception.Message)" -Level WARN
|
Write-Log "排除正则非法,安全描述符采集按'不排除'处理:$regexText —— $($_.Exception.Message)" -Level WARN
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
$script:LogConfig.FilePath,
|
$script:LogConfig.FilePath,
|
||||||
$line + [Environment]::NewLine,
|
$line + [Environment]::NewLine,
|
||||||
$script:LogEncoding)
|
$script:LogEncoding)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
# 日志落盘失败时保持沉默:不能因为写日志失败而让备份失败。
|
# 日志落盘失败时保持沉默:不能因为写日志失败而让备份失败。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<#
|
||||||
|
PSScriptAnalyzer 的配置。
|
||||||
|
|
||||||
|
为什么这个文件是必要的,而不是"跑一下默认规则就算按规范了":
|
||||||
|
|
||||||
|
默认规则集**不含**格式规则 —— PSPlaceOpenBrace / PSPlaceCloseBrace /
|
||||||
|
PSUseConsistentWhitespace / PSUseConsistentIndentation / PSAlignAssignmentStatement /
|
||||||
|
PSUseCorrectCasing 这六条默认都是 Disabled。所以
|
||||||
|
`Invoke-ScriptAnalyzer -Severity Warning,Error`(不带 -Settings)
|
||||||
|
会**静默漏掉全部排版问题**。
|
||||||
|
|
||||||
|
这里用 `Rules` 把格式规则**打开**,而不是用 `IncludeRules` 换一套:不带 IncludeRules 时
|
||||||
|
默认规则集照常生效,Rules 只是逐条改设置,于是"默认规则 + 格式规则"同时跑。
|
||||||
|
|
||||||
|
逐条决策(都在本次改造里确认过,理由另见 docs/adr/):
|
||||||
|
|
||||||
|
* PSAvoidUsingWriteHost 全局排除 —— Write-Log 的彩色控制台输出是这份工具的**刻意设计**,
|
||||||
|
不是疏忽。这是架构性选择,所以在配置里排除掉,而不是逐处 Suppress 假装它是例外。
|
||||||
|
* PSUseSingularNouns 放行 SecurityRecords / MapKeys / ArchiveTopLevelNames ——
|
||||||
|
单数名 Get-BakNretSecurityRecord 已被"单对象版本"占用,为了规则把两个语义搅在一起
|
||||||
|
不值得。Data / Windows 是规则自带的默认放行项,显式写上以免被本条覆盖掉。
|
||||||
|
* PSAvoidLongLines 上限 160,而**不是**官方默认的 120:本仓库的中文注释与测试夹具
|
||||||
|
里的一行式目录让 120 意味着 270 处改动,而 160 意味着 41 处(并且 160 仍是
|
||||||
|
「宽但可读」)。这是一次有意的偏离,理由记在这里而不是悄悄放宽:如果哪天要收
|
||||||
|
紧到 120,先看 tools\Invoke-Analyzer.ps1 的输出里还有多少条。
|
||||||
|
|
||||||
|
* PSUseShouldProcessForStateChangingFunctions 全局排除 —— 本模块是库,不是入口:
|
||||||
|
WhatIf 的边界在 Backup.ps1 / Restore.ps1(它们自己管 -DryRun / -WhatIf)。
|
||||||
|
给库里 27 个改状态的函数都加 SupportsShouldProcess 会更糟:入口把 $WhatIfPreference
|
||||||
|
置真之后,这些函数会**静默跳过自己的工作**,备份看起来成功却什么都没做。
|
||||||
|
|
||||||
|
* PSAvoidUsingPlainTextForPassword 全局排除 —— 7z 只接受命令行口令(这是 7z 自身
|
||||||
|
的限制,README 的「加密」一节写明了取舍)。口令在这个工具里必然是明文字符串,
|
||||||
|
规则说的"用 SecureString"在这里没有可用的落点。
|
||||||
|
真正要守的两条已经另有措施:口令不进日志(遮蔽 + 断言)、口令不进版本库
|
||||||
|
(.gitignore + 出厂默认值留空)。
|
||||||
|
* PSUseConsistentIndentation 用 space + 4,与 .editorconfig 一致。
|
||||||
|
#>
|
||||||
|
@{
|
||||||
|
Severity = @('Error', 'Warning')
|
||||||
|
|
||||||
|
ExcludeRules = @(
|
||||||
|
'PSAvoidUsingWriteHost',
|
||||||
|
'PSUseShouldProcessForStateChangingFunctions',
|
||||||
|
'PSAvoidUsingPlainTextForPassword'
|
||||||
|
)
|
||||||
|
|
||||||
|
Rules = @{
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 格式规则
|
||||||
|
PSPlaceOpenBrace = @{
|
||||||
|
Enable = $true
|
||||||
|
OnSameLine = $true
|
||||||
|
NewLineAfter = $true
|
||||||
|
IgnoreOneLineBlock = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
PSPlaceCloseBrace = @{
|
||||||
|
Enable = $true
|
||||||
|
NewLineAfter = $true
|
||||||
|
IgnoreOneLineBlock = $true
|
||||||
|
NoEmptyLineBefore = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
PSUseConsistentIndentation = @{
|
||||||
|
Enable = $true
|
||||||
|
Kind = 'space'
|
||||||
|
IndentationSize = 4
|
||||||
|
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
|
||||||
|
}
|
||||||
|
|
||||||
|
PSUseConsistentWhitespace = @{
|
||||||
|
Enable = $true
|
||||||
|
CheckInnerBrace = $true
|
||||||
|
CheckOpenBrace = $true
|
||||||
|
CheckOpenParen = $true
|
||||||
|
CheckOperator = $true
|
||||||
|
CheckPipe = $true
|
||||||
|
CheckPipeForRedundantWhitespace = $false
|
||||||
|
CheckSeparator = $true
|
||||||
|
CheckParameter = $false
|
||||||
|
IgnoreAssignmentOperatorInsideHashTable = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
PSAlignAssignmentStatement = @{
|
||||||
|
Enable = $true
|
||||||
|
CheckHashtable = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
PSUseCorrectCasing = @{
|
||||||
|
Enable = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 行长
|
||||||
|
PSAvoidLongLines = @{
|
||||||
|
Enable = $true
|
||||||
|
MaximumLineLength = 160
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 名词白名单
|
||||||
|
PSUseSingularNouns = @{
|
||||||
|
Enable = $true
|
||||||
|
NounAllowList = @('Data', 'Windows', 'SecurityRecords', 'MapKeys', 'ArchiveTopLevelNames')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-21
@@ -296,7 +296,8 @@ function Invoke-ExtractionByLayout {
|
|||||||
New-Item -ItemType Directory -Path $destParent -Force | Out-Null
|
New-Item -ItemType Directory -Path $destParent -Force | Out-Null
|
||||||
}
|
}
|
||||||
Move-Item -LiteralPath $produced -Destination $destPath -Force
|
Move-Item -LiteralPath $produced -Destination $destPath -Force
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
return $true
|
return $true
|
||||||
@@ -316,7 +317,8 @@ function Invoke-ExtractionByLayout {
|
|||||||
New-BaknretJunction -Path $anchorPath -Target $destPath | Out-Null
|
New-BaknretJunction -Path $anchorPath -Target $destPath | Out-Null
|
||||||
$junctionCreated = $true
|
$junctionCreated = $true
|
||||||
Write-Log ("落地:{0} -> {1}(经连接点 {2})" -f $archivePath, $destPath, $anchorPath) -Level DEBUG
|
Write-Log ("落地:{0} -> {1}(经连接点 {2})" -f $archivePath, $destPath, $anchorPath) -Level DEBUG
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "无法建连接点($($_.Exception.Message)),改为先解到临时目录再合并" -Level WARN
|
Write-Log "无法建连接点($($_.Exception.Message)),改为先解到临时目录再合并" -Level WARN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,7 +326,8 @@ function Invoke-ExtractionByLayout {
|
|||||||
if ($junctionCreated) {
|
if ($junctionCreated) {
|
||||||
try {
|
try {
|
||||||
return (Invoke-ExtractionRaw -ArchiveFile $ArchiveFile -Destination $destParent -RelativePath $archivePath -Password $Password)
|
return (Invoke-ExtractionRaw -ArchiveFile $ArchiveFile -Destination $destParent -RelativePath $archivePath -Password $Password)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-BaknretJunction -Path $anchorPath
|
Remove-BaknretJunction -Path $anchorPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +346,8 @@ function Invoke-ExtractionByLayout {
|
|||||||
foreach ($child in @(Get-ChildItem -LiteralPath $source -Force -ErrorAction SilentlyContinue)) {
|
foreach ($child in @(Get-ChildItem -LiteralPath $source -Force -ErrorAction SilentlyContinue)) {
|
||||||
Copy-Item -LiteralPath $child.FullName -Destination $destPath -Recurse -Force
|
Copy-Item -LiteralPath $child.FullName -Destination $destPath -Recurse -Force
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
return $true
|
return $true
|
||||||
@@ -395,10 +399,12 @@ function Test-BaknretArchivePath {
|
|||||||
-NoNewWindow -Wait -PassThru
|
-NoNewWindow -Wait -PassThru
|
||||||
|
|
||||||
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "无法列出归档内容(跳过预判):$($_.Exception.Message)" -Level DEBUG
|
Write-Log "无法列出归档内容(跳过预判):$($_.Exception.Message)" -Level DEBUG
|
||||||
return $true
|
return $true
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $errFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $errFile -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -595,9 +601,11 @@ foreach ($line in $lines) {
|
|||||||
$isFile = [bool]$entryItem.IsFile
|
$isFile = [bool]$entryItem.IsFile
|
||||||
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||||
$isFile = $true
|
$isFile = $true
|
||||||
} elseif (Test-Path -LiteralPath $dest -PathType Container) {
|
}
|
||||||
|
elseif (Test-Path -LiteralPath $dest -PathType Container) {
|
||||||
$isFile = $false
|
$isFile = $false
|
||||||
} elseif ($layouts.ContainsKey(([string]$entryItem.ArchivePath).ToLower())) {
|
}
|
||||||
|
elseif ($layouts.ContainsKey(([string]$entryItem.ArchivePath).ToLower())) {
|
||||||
$isFile = ($layouts[([string]$entryItem.ArchivePath).ToLower()] -eq 'file')
|
$isFile = ($layouts[([string]$entryItem.ArchivePath).ToLower()] -eq 'file')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,7 +682,8 @@ foreach ($line in $lines) {
|
|||||||
if ($verifyCode -eq 0) {
|
if ($verifyCode -eq 0) {
|
||||||
Write-Log "校验通过: $($archiveFile.Name)" -Level INFO
|
Write-Log "校验通过: $($archiveFile.Name)" -Level INFO
|
||||||
$stats.verified++
|
$stats.verified++
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log "校验失败: $($archiveFile.Name)(退出码 $verifyCode)" -Level ERROR
|
Write-Log "校验失败: $($archiveFile.Name)(退出码 $verifyCode)" -Level ERROR
|
||||||
$stats.failed++
|
$stats.failed++
|
||||||
$failures += $displayPath
|
$failures += $displayPath
|
||||||
@@ -693,7 +702,8 @@ foreach ($line in $lines) {
|
|||||||
$stats.skipped++
|
$stats.skipped++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "目标目录摘要读取失败,继续恢复:$_" -Level DEBUG
|
Write-Log "目标目录摘要读取失败,继续恢复:$_" -Level DEBUG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,7 +727,8 @@ foreach ($line in $lines) {
|
|||||||
$targetParent = Split-Path -Path $target.DestPath -Parent
|
$targetParent = Split-Path -Path $target.DestPath -Parent
|
||||||
if ($targetParent) {
|
if ($targetParent) {
|
||||||
Write-Log "提示: 目标不存在,将新建 $targetParent" -Level DEBUG
|
Write-Log "提示: 目标不存在,将新建 $targetParent" -Level DEBUG
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log "提示: 目标不存在,且没有可创建的父目录:$($target.DestPath)" -Level DEBUG
|
Write-Log "提示: 目标不存在,且没有可创建的父目录:$($target.DestPath)" -Level DEBUG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,9 +765,11 @@ foreach ($line in $lines) {
|
|||||||
# 必须在解压**之后**、对真实目标路径做(连接点在 Invoke-Extraction 里已经拆掉了)。
|
# 必须在解压**之后**、对真实目标路径做(连接点在 Invoke-Extraction 里已经拆掉了)。
|
||||||
if ($SkipSecurity) {
|
if ($SkipSecurity) {
|
||||||
Write-Log '按 -SkipSecurity 跳过了安全描述符恢复' -Level DEBUG
|
Write-Log '按 -SkipSecurity 跳过了安全描述符恢复' -Level DEBUG
|
||||||
} elseif (([string]$script:Config.Security.Mode) -eq 'Off') {
|
}
|
||||||
|
elseif (([string]$script:Config.Security.Mode) -eq 'Off') {
|
||||||
Write-Log '配置里 Security.Mode = Off,跳过安全描述符恢复' -Level DEBUG
|
Write-Log '配置里 Security.Mode = Off,跳过安全描述符恢复' -Level DEBUG
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$sidecarName = $null
|
$sidecarName = $null
|
||||||
if ($found.Record -and ($found.Record.PSObject.Properties.Name -contains 'security') -and $found.Record.security) {
|
if ($found.Record -and ($found.Record.PSObject.Properties.Name -contains 'security') -and $found.Record.security) {
|
||||||
$sidecarName = [string]$found.Record.security.file
|
$sidecarName = [string]$found.Record.security.file
|
||||||
@@ -766,7 +779,8 @@ foreach ($line in $lines) {
|
|||||||
$sidecar = Read-BaknretSecuritySidecar -Path (Join-Path $BackupDir $sidecarName)
|
$sidecar = Read-BaknretSecuritySidecar -Path (Join-Path $BackupDir $sidecarName)
|
||||||
if (-not $sidecar) {
|
if (-not $sidecar) {
|
||||||
Write-Log ("这个归档没有安全描述符旁挂文件({0}):恢复出来的属主/ACL 是新建对象的默认值 —— 原程序若依赖特殊权限(ProgramData 下的 CREATOR OWNER 最典型),会报无读写权限" -f $sidecarName) -Level WARN
|
Write-Log ("这个归档没有安全描述符旁挂文件({0}):恢复出来的属主/ACL 是新建对象的默认值 —— 原程序若依赖特殊权限(ProgramData 下的 CREATOR OWNER 最典型),会报无读写权限" -f $sidecarName) -Level WARN
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$sidMap = @{}
|
$sidMap = @{}
|
||||||
if ($script:Config.Security.SidMap) { $sidMap = $script:Config.Security.SidMap }
|
if ($script:Config.Security.SidMap) { $sidMap = $script:Config.Security.SidMap }
|
||||||
|
|
||||||
@@ -803,16 +817,19 @@ foreach ($line in $lines) {
|
|||||||
$record = $manifest.items[$baseName]
|
$record = $manifest.items[$baseName]
|
||||||
if ($record -is [System.Collections.IDictionary]) {
|
if ($record -is [System.Collections.IDictionary]) {
|
||||||
$record['lastRestoreAt'] = (Get-Date).ToString('o')
|
$record['lastRestoreAt'] = (Get-Date).ToString('o')
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$record | Add-Member -NotePropertyName lastRestoreAt -NotePropertyValue ((Get-Date).ToString('o')) -Force
|
$record | Add-Member -NotePropertyName lastRestoreAt -NotePropertyValue ((Get-Date).ToString('o')) -Force
|
||||||
}
|
}
|
||||||
$manifestDirty = $true
|
$manifestDirty = $true
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$stats.failed++
|
$stats.failed++
|
||||||
$failures += $displayPath
|
$failures += $displayPath
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "恢复失败: $displayPath,$_" -Level ERROR
|
Write-Log "恢复失败: $displayPath,$_" -Level ERROR
|
||||||
$stats.failed++
|
$stats.failed++
|
||||||
$failures += $displayPath
|
$failures += $displayPath
|
||||||
@@ -825,7 +842,7 @@ foreach ($line in $lines) {
|
|||||||
|
|
||||||
if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
|
if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
|
||||||
$orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
$orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -notin $referencedArchives })
|
Where-Object { $_.Extension.ToLower() -in $SupportedFormats -and $_.BaseName -notin $referencedArchives })
|
||||||
|
|
||||||
if ($orphans.Count -gt 0) {
|
if ($orphans.Count -gt 0) {
|
||||||
Write-Log '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -Level WARN
|
Write-Log '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -Level WARN
|
||||||
@@ -833,7 +850,8 @@ if (-not $VerifyOnly -and $Only.Count -eq 0 -and $Skip.Count -eq 0) {
|
|||||||
Write-Log (" - {0}({1} MB,{2})" -f $orphan.Name, [math]::Round($orphan.Length / 1MB, 2), $orphan.LastWriteTime) -Level WARN
|
Write-Log (" - {0}({1} MB,{2})" -f $orphan.Name, [math]::Round($orphan.Length / 1MB, 2), $orphan.LastWriteTime) -Level WARN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif (-not $VerifyOnly) {
|
}
|
||||||
|
elseif (-not $VerifyOnly) {
|
||||||
# 带 -Only/-Skip 时只有被选中的条目会被处理,其余归档都不在 $referencedArchives 里,
|
# 带 -Only/-Skip 时只有被选中的条目会被处理,其余归档都不在 $referencedArchives 里,
|
||||||
# 按上面的算法报出来全是假孤儿(还会吓唬人说"注意别误删"),所以整段跳过。
|
# 按上面的算法报出来全是假孤儿(还会吓唬人说"注意别误删"),所以整段跳过。
|
||||||
Write-Log '本次只恢复了部分条目,跳过孤儿归档审计(避免把未选中的归档误报成孤儿)' -Level DEBUG
|
Write-Log '本次只恢复了部分条目,跳过孤儿归档审计(避免把未选中的归档误报成孤儿)' -Level DEBUG
|
||||||
@@ -845,10 +863,12 @@ try {
|
|||||||
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
|
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $BackupDir
|
||||||
Write-BaknretManifest -Path $manifestPath -Manifest $manifest | Out-Null
|
Write-BaknretManifest -Path $manifestPath -Manifest $manifest | Out-Null
|
||||||
Write-Log 'manifest 已更新(记下本次恢复时间)' -Level DEBUG
|
Write-Log 'manifest 已更新(记下本次恢复时间)' -Level DEBUG
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Log 'manifest 无需更新:本次没有实际恢复任何条目' -Level DEBUG
|
Write-Log 'manifest 无需更新:本次没有实际恢复任何条目' -Level DEBUG
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Log "manifest 写回失败(不影响本次恢复):$_" -Level WARN
|
Write-Log "manifest 写回失败(不影响本次恢复):$_" -Level WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MicrosoftEdge = @{
|
MicrosoftEdge = @{
|
||||||
DefaultData = @{
|
DefaultData = @{
|
||||||
Path = '%LocalAppData%\Microsoft\Edge\User Data'
|
Path = '%LocalAppData%\Microsoft\Edge\User Data'
|
||||||
Exclude = '!*Cache,!BrowserMetrics,!component_crx_cache,' +
|
Exclude = '!*Cache,!BrowserMetrics,!component_crx_cache,' +
|
||||||
|
|||||||
@@ -99,11 +99,13 @@ function Invoke-ScriptInHost {
|
|||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
if ($Quiet) {
|
if ($Quiet) {
|
||||||
& $exeOf[$HostName] @argumentList *> $null
|
& $exeOf[$HostName] @argumentList *> $null
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
& $exeOf[$HostName] @argumentList 2>&1 | Tee-Object -FilePath $stdout | Out-Null
|
& $exeOf[$HostName] @argumentList 2>&1 | Tee-Object -FilePath $stdout | Out-Null
|
||||||
}
|
}
|
||||||
return $LASTEXITCODE
|
return $LASTEXITCODE
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $stdout -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $stdout -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +195,8 @@ function Invoke-ParseLayer {
|
|||||||
$ok = ($numbers -split '/')[0] -eq ($numbers -split '/')[1]
|
$ok = ($numbers -split '/')[0] -eq ($numbers -split '/')[1]
|
||||||
if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Host $_ -ForegroundColor DarkGray } }
|
if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Host $_ -ForegroundColor DarkGray } }
|
||||||
Add-Result -Layer 'Parse' -HostName $HostName -Ok $ok -Detail ("解析通过 {0} 个文件" -f $numbers)
|
Add-Result -Layer 'Parse' -HostName $HostName -Ok $ok -Detail ("解析通过 {0} 个文件" -f $numbers)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,8 @@ Write-Host ''
|
|||||||
Write-Host ('=' * 64)
|
Write-Host ('=' * 64)
|
||||||
if ($failed.Count -eq 0) {
|
if ($failed.Count -eq 0) {
|
||||||
Write-Host ("验收全部通过:{0} 项(宿主 {1})" -f $script:Results.Count, ($hosts -join ' + ')) -ForegroundColor Green
|
Write-Host ("验收全部通过:{0} 项(宿主 {1})" -f $script:Results.Count, ($hosts -join ' + ')) -ForegroundColor Green
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host ("验收有失败:{0} 项里失败 {1} 项" -f $script:Results.Count, $failed.Count) -ForegroundColor Red
|
Write-Host ("验收有失败:{0} 项里失败 {1} 项" -f $script:Results.Count, $failed.Count) -ForegroundColor Red
|
||||||
$failed | ForEach-Object { Write-Host (" - {0} @ {1}:{2}" -f $_.Layer, $_.HostName, $_.Detail) -ForegroundColor Red }
|
$failed | ForEach-Object { Write-Host (" - {0} @ {1}:{2}" -f $_.Layer, $_.HostName, $_.Detail) -ForegroundColor Red }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ BeforeAll {
|
|||||||
try {
|
try {
|
||||||
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
||||||
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ AfterAll {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '软件名录:Slot 形状(新契约)' {
|
Describe '软件名录:Slot 形状(新契约)' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:FormatRoot = Join-Path $script:Sandbox 'format'
|
$script:FormatRoot = Join-Path $script:Sandbox 'format'
|
||||||
@@ -213,7 +214,7 @@ Describe '软件名录:Slot 形状(新契约)' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '清单修饰符:新契约格式' {
|
Describe '清单修饰符:新契约格式' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:FormatRoot = Join-Path $script:Sandbox 'format'
|
$script:FormatRoot = Join-Path $script:Sandbox 'format'
|
||||||
@@ -309,7 +310,7 @@ Describe '清单修饰符:新契约格式' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:SlotRoot = Join-Path $script:Sandbox 'slots-e2e'
|
$script:SlotRoot = Join-Path $script:Sandbox 'slots-e2e'
|
||||||
@@ -428,7 +429,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Slot 布局是重构后才有的,Backups\ 里还躺着按旧布局(包内直接是 <源目录名>\...)
|
# Slot 布局是重构后才有的,Backups\ 里还躺着按旧布局(包内直接是 <源目录名>\...)
|
||||||
# 生成的归档。恢复这类归档时必须回退到"把 <目标末级名> 解到目标父目录"的旧语义。
|
# 生成的归档。恢复这类归档时必须回退到"把 <目标末级名> 解到目标父目录"的旧语义。
|
||||||
@@ -456,8 +457,8 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
|
|||||||
# 手工造一个旧布局归档:顶层就是源目录名,不是 Slot 名。
|
# 手工造一个旧布局归档:顶层就是源目录名,不是 Slot 名。
|
||||||
$script:LegacyArchive = Join-Path $script:LegacyBackupDir 'oldapp.7z'
|
$script:LegacyArchive = Join-Path $script:LegacyBackupDir 'oldapp.7z'
|
||||||
(Invoke-ExternalCommand -FilePath $script:SevenZip `
|
(Invoke-ExternalCommand -FilePath $script:SevenZip `
|
||||||
-ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $script:LegacyArchive, $script:LegacyLeaf) `
|
-ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $script:LegacyArchive, $script:LegacyLeaf) `
|
||||||
-WorkingDirectory $script:LegacyHolder) | Should -Be 0
|
-WorkingDirectory $script:LegacyHolder) | Should -Be 0
|
||||||
}
|
}
|
||||||
|
|
||||||
It '归档确实是旧布局:顶层是源目录名而不是 Slot 名' {
|
It '归档确实是旧布局:顶层是源目录名而不是 Slot 名' {
|
||||||
@@ -504,7 +505,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:RejectRoot = Join-Path $script:Sandbox 'collide-e2e'
|
$script:RejectRoot = Join-Path $script:Sandbox 'collide-e2e'
|
||||||
@@ -546,7 +547,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# 这是"合并/改名条目"的真实后果:归档还在,manifest 里也还留着历史记录,
|
# 这是"合并/改名条目"的真实后果:归档还在,manifest 里也还留着历史记录,
|
||||||
# 但清单里已经没有任何条目指向它 —— Restore.ps1 按条目名找归档,所以它恢复不到。
|
# 但清单里已经没有任何条目指向它 —— Restore.ps1 按条目名找归档,所以它恢复不到。
|
||||||
@@ -601,7 +602,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe 'manifest 一致性:archive 字段只在文件真的存在时才写' {
|
Describe 'manifest 一致性:archive 字段只在文件真的存在时才写' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:SyncRoot = Join-Path $script:Sandbox 'sync'
|
$script:SyncRoot = Join-Path $script:Sandbox 'sync'
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ BeforeAll {
|
|||||||
param([Parameter(Mandatory = $true)][System.IO.FileSystemInfo]$Item, [Parameter(Mandatory = $true)]$Security)
|
param([Parameter(Mandatory = $true)][System.IO.FileSystemInfo]$Item, [Parameter(Mandatory = $true)]$Security)
|
||||||
if ($PSVersionTable.PSEdition -eq 'Core') {
|
if ($PSVersionTable.PSEdition -eq 'Core') {
|
||||||
[System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $Security)
|
[System.IO.FileSystemAclExtensions]::SetAccessControl($Item, $Security)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$Item.SetAccessControl($Security)
|
$Item.SetAccessControl($Security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,8 +64,8 @@ BeforeAll {
|
|||||||
$acl = Get-Acl -LiteralPath $Path
|
$acl = Get-Acl -LiteralPath $Path
|
||||||
$sid = [System.Security.Principal.SecurityIdentifier]
|
$sid = [System.Security.Principal.SecurityIdentifier]
|
||||||
$aces = @($acl.GetAccessRules($true, $true, $sid) |
|
$aces = @($acl.GetAccessRules($true, $true, $sid) |
|
||||||
ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } |
|
ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } |
|
||||||
Sort-Object)
|
Sort-Object)
|
||||||
return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' '))
|
return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +110,8 @@ BeforeAll {
|
|||||||
try {
|
try {
|
||||||
& takeown.exe /F $Path /R /D Y 2>&1 | Out-Null
|
& takeown.exe /F $Path /R /D Y 2>&1 | Out-Null
|
||||||
& icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null
|
& icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$ErrorActionPreference = $previousPreference
|
$ErrorActionPreference = $previousPreference
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,8 @@ BeforeAll {
|
|||||||
try {
|
try {
|
||||||
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
||||||
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -219,7 +222,7 @@ AfterAll {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' {
|
Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
It '锚定模式只命中它自己那棵子树' {
|
It '锚定模式只命中它自己那棵子树' {
|
||||||
Test-BaknretPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue
|
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 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse
|
||||||
@@ -251,7 +254,7 @@ Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe 'SID 映射(跨机恢复)' {
|
Describe 'SID 映射(跨机恢复)' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
It '整 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)'
|
$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' }
|
||||||
@@ -272,7 +275,7 @@ Describe 'SID 映射(跨机恢复)' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '安全描述符采集' {
|
Describe '安全描述符采集' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:CaptureRoot = Join-Path $script:Sandbox 'capture\Data'
|
$script:CaptureRoot = Join-Path $script:Sandbox 'capture\Data'
|
||||||
$script:CaptureItem = [pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:CaptureRoot }
|
$script:CaptureItem = [pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:CaptureRoot }
|
||||||
@@ -341,7 +344,7 @@ Describe '安全描述符采集' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '安全描述符回放' {
|
Describe '安全描述符回放' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:RestoreRoot = Join-Path $script:Sandbox 'restore\Data'
|
$script:RestoreRoot = Join-Path $script:Sandbox 'restore\Data'
|
||||||
$script:TargetRoot = Join-Path $script:Sandbox 'restore\target'
|
$script:TargetRoot = Join-Path $script:Sandbox 'restore\target'
|
||||||
@@ -398,7 +401,7 @@ Describe '安全描述符回放' {
|
|||||||
|
|
||||||
# 属组写成一个本机不存在的 SID:改主组需要特权,这一层必然失败
|
# 属组写成一个本机不存在的 SID:改主组需要特权,这一层必然失败
|
||||||
$sddl = 'O:' + (Get-Acl -LiteralPath $path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value +
|
$sddl = 'O:' + (Get-Acl -LiteralPath $path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value +
|
||||||
'G:' + $script:OrphanSid + 'D:(A;;FA;;;SY)'
|
'G:' + $script:OrphanSid + 'D:(A;;FA;;;SY)'
|
||||||
$sidecar = [pscustomobject]@{
|
$sidecar = [pscustomobject]@{
|
||||||
Records = @([pscustomobject]@{ p = 'Data'; k = 'd'; s = $sddl })
|
Records = @([pscustomobject]@{ p = 'Data'; k = 'd'; s = $sddl })
|
||||||
}
|
}
|
||||||
@@ -422,7 +425,7 @@ Describe '安全描述符回放' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZip) {
|
Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZip) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:Harness = New-AclEntryHarness -Name 'integration' -Root $script:Sandbox
|
$script:Harness = New-AclEntryHarness -Name 'integration' -Root $script:Sandbox
|
||||||
$script:IntegrationFingerprints = New-AclSourceTree -Root $script:Harness.SourcePath
|
$script:IntegrationFingerprints = New-AclSourceTree -Root $script:Harness.SourcePath
|
||||||
|
|||||||
+28
-20
@@ -82,7 +82,8 @@ BeforeAll {
|
|||||||
try {
|
try {
|
||||||
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
$exitCode = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdFile)
|
||||||
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
$lines = @(Get-Content -LiteralPath $outFile -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $cmdFile -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,7 @@ AfterAll {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe 'BackupList.txt 解析' {
|
Describe 'BackupList.txt 解析' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
It '注释行与空行返回 $null' {
|
It '注释行与空行返回 $null' {
|
||||||
ConvertFrom-BackupListLine -Line '# 这是注释' | Should -BeNullOrEmpty
|
ConvertFrom-BackupListLine -Line '# 这是注释' | Should -BeNullOrEmpty
|
||||||
@@ -364,7 +365,7 @@ Describe 'BackupList.txt 解析' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '归档命名' {
|
Describe '归档命名' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:NamingRoot = Join-Path $script:Sandbox 'naming'
|
$script:NamingRoot = Join-Path $script:Sandbox 'naming'
|
||||||
@@ -431,7 +432,7 @@ Describe '归档命名' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '7z 排除参数翻译' {
|
Describe '7z 排除参数翻译' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:ExcludeTree = Join-Path $script:Sandbox 'exclude-tree'
|
$script:ExcludeTree = Join-Path $script:Sandbox 'exclude-tree'
|
||||||
@@ -588,7 +589,7 @@ Describe '7z 排除参数翻译' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '归档项与暂存目录' {
|
Describe '归档项与暂存目录' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:StagingRoot = Join-Path $script:Sandbox 'staging'
|
$script:StagingRoot = Join-Path $script:Sandbox 'staging'
|
||||||
@@ -660,7 +661,8 @@ Describe '归档项与暂存目录' {
|
|||||||
$cfg = Get-Item -LiteralPath (Join-Path $stage 'Cfg') -Force
|
$cfg = Get-Item -LiteralPath (Join-Path $stage 'Cfg') -Force
|
||||||
$cfg.PSIsContainer | Should -BeFalse
|
$cfg.PSIsContainer | Should -BeFalse
|
||||||
(Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content'
|
(Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content'
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-BaknretArchiveStaging -Root $stage
|
Remove-BaknretArchiveStaging -Root $stage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,7 +680,7 @@ Describe '归档项与暂存目录' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '命令行参数拼接' {
|
Describe '命令行参数拼接' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
It '无空格参数原样输出' {
|
It '无空格参数原样输出' {
|
||||||
ConvertTo-NativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
|
ConvertTo-NativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
|
||||||
@@ -703,7 +705,7 @@ Describe '命令行参数拼接' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe 'manifest 与配置' {
|
Describe 'manifest 与配置' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
It 'manifest 读写往返' {
|
It 'manifest 读写往返' {
|
||||||
$path = Join-Path $script:Sandbox 'roundtrip\manifest.json'
|
$path = Join-Path $script:Sandbox 'roundtrip\manifest.json'
|
||||||
@@ -762,7 +764,8 @@ Describe 'manifest 与配置' {
|
|||||||
(Get-BaknretPassword) | Should -Be 'sekrit'
|
(Get-BaknretPassword) | Should -Be 'sekrit'
|
||||||
$env:BAKNRET_PASSWORD = $null
|
$env:BAKNRET_PASSWORD = $null
|
||||||
(Get-BaknretPassword) | Should -BeNullOrEmpty
|
(Get-BaknretPassword) | Should -BeNullOrEmpty
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$env:BAKNRET_PASSWORD = $saved
|
$env:BAKNRET_PASSWORD = $saved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -770,7 +773,7 @@ Describe 'manifest 与配置' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '软件名录' {
|
Describe '软件名录' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:CatRoot = Join-Path $script:Sandbox 'catalog'
|
$script:CatRoot = Join-Path $script:Sandbox 'catalog'
|
||||||
@@ -961,7 +964,7 @@ Describe '软件名录' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe 'Resolve-BackupEntry:条目解析' {
|
Describe 'Resolve-BackupEntry:条目解析' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:CatRoot = Join-Path $script:Sandbox 'catalog'
|
$script:CatRoot = Join-Path $script:Sandbox 'catalog'
|
||||||
@@ -1114,7 +1117,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '外部命令退出码(旧实现的核心缺陷)' {
|
Describe '外部命令退出码(旧实现的核心缺陷)' {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
It '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参数' {
|
It '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参数' {
|
||||||
$sandbox = Join-Path $env:TEMP ("bnr-secret-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
$sandbox = Join-Path $env:TEMP ("bnr-secret-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||||
@@ -1125,7 +1128,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
|
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
|
||||||
Set-BaknretDebug
|
Set-BaknretDebug
|
||||||
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
|
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Set-BaknretDebug -Enabled:$false
|
Set-BaknretDebug -Enabled:$false
|
||||||
Stop-BaknretLog
|
Stop-BaknretLog
|
||||||
}
|
}
|
||||||
@@ -1153,7 +1157,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
$second | Should -BeNullOrEmpty
|
$second | Should -BeNullOrEmpty
|
||||||
|
|
||||||
Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue
|
Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Exit-BaknretRunLock -Lock $second
|
Exit-BaknretRunLock -Lock $second
|
||||||
Exit-BaknretRunLock -Lock $first
|
Exit-BaknretRunLock -Lock $first
|
||||||
}
|
}
|
||||||
@@ -1175,7 +1180,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
$second = Enter-BaknretRunLock -Directory $dir
|
$second = Enter-BaknretRunLock -Directory $dir
|
||||||
$second | Should -Not -BeNullOrEmpty
|
$second | Should -Not -BeNullOrEmpty
|
||||||
Exit-BaknretRunLock -Lock $second
|
Exit-BaknretRunLock -Lock $second
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1190,7 +1196,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
$summary.FileCount | Should -Be 0
|
$summary.FileCount | Should -Be 0
|
||||||
$summary.TotalSize | Should -Not -BeNullOrEmpty
|
$summary.TotalSize | Should -Not -BeNullOrEmpty
|
||||||
$summary.TotalSize | Should -Be 0
|
$summary.TotalSize | Should -Be 0
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1213,7 +1220,8 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
(Get-Item -LiteralPath $target).IsReadOnly = $true
|
(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'
|
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND'
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue
|
$file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue
|
||||||
if ($file) { $file.IsReadOnly = $false }
|
if ($file) { $file.IsReadOnly = $false }
|
||||||
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
@@ -1231,7 +1239,7 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSevenZip) {
|
Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSevenZip) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:IntegrationRoot = Join-Path $script:Sandbox 'integration'
|
$script:IntegrationRoot = Join-Path $script:Sandbox 'integration'
|
||||||
@@ -1298,7 +1306,7 @@ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSeven
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:E2ERoot = Join-Path $script:Sandbox 'e2e'
|
$script:E2ERoot = Join-Path $script:Sandbox 'e2e'
|
||||||
@@ -1468,7 +1476,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
|
|||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# `+` / `-` 的归档名必须在方向过滤**之前**登记:这些条目自己不产生归档,
|
# `+` / `-` 的归档名必须在方向过滤**之前**登记:这些条目自己不产生归档,
|
||||||
# 但它们对应的归档是有主的,不能被孤儿审计当成没人要的孤儿。
|
# 但它们对应的归档是有主的,不能被孤儿审计当成没人要的孤儿。
|
||||||
|
|||||||
+18
-10
@@ -102,7 +102,7 @@ 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 |
|
$archiveFiles = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') })
|
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') })
|
||||||
|
|
||||||
# Restore.ps1 恢复成功后会**写回 manifest.json**(记 lastRestoreAt)。真实 Backups\ 只能读,
|
# Restore.ps1 恢复成功后会**写回 manifest.json**(记 lastRestoreAt)。真实 Backups\ 只能读,
|
||||||
# 所以给子进程一个临时 BackupDir:里面放一份 manifest 副本 + 指向真实归档的符号链接
|
# 所以给子进程一个临时 BackupDir:里面放一份 manifest 副本 + 指向真实归档的符号链接
|
||||||
@@ -193,9 +193,11 @@ function Compare-RestoredTree {
|
|||||||
if ((Get-FileHash -LiteralPath $restoredItem.FullName -Algorithm SHA256).Hash -eq
|
if ((Get-FileHash -LiteralPath $restoredItem.FullName -Algorithm SHA256).Hash -eq
|
||||||
(Get-FileHash -LiteralPath $liveItem.FullName -Algorithm SHA256).Hash) {
|
(Get-FileHash -LiteralPath $liveItem.FullName -Algorithm SHA256).Hash) {
|
||||||
$report.Matched = 1
|
$report.Matched = 1
|
||||||
} elseif ($liveItem.LastWriteTime -gt $ArchiveTime) {
|
}
|
||||||
|
elseif ($liveItem.LastWriteTime -gt $ArchiveTime) {
|
||||||
$report.Stale = @($restoredItem.Name)
|
$report.Stale = @($restoredItem.Name)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$report.Changed = @($restoredItem.Name)
|
$report.Changed = @($restoredItem.Name)
|
||||||
}
|
}
|
||||||
return $report
|
return $report
|
||||||
@@ -214,7 +216,8 @@ function Compare-RestoredTree {
|
|||||||
if (-not (Test-Path -LiteralPath $liveFile)) {
|
if (-not (Test-Path -LiteralPath $liveFile)) {
|
||||||
if (Test-RemovedFromLiveAfterBackup -LiveRoot $liveRoot -Relative $relative -ArchiveTime $ArchiveTime) {
|
if (Test-RemovedFromLiveAfterBackup -LiveRoot $liveRoot -Relative $relative -ArchiveTime $ArchiveTime) {
|
||||||
$report.Removed += $relative
|
$report.Removed += $relative
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$report.Extra += $relative
|
$report.Extra += $relative
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -229,7 +232,8 @@ function Compare-RestoredTree {
|
|||||||
# 内容不一样:先看是不是"源在归档之后动过"
|
# 内容不一样:先看是不是"源在归档之后动过"
|
||||||
if ((Get-Item -LiteralPath $liveFile -Force).LastWriteTime -gt $ArchiveTime) {
|
if ((Get-Item -LiteralPath $liveFile -Force).LastWriteTime -gt $ArchiveTime) {
|
||||||
$report.Stale += $relative
|
$report.Stale += $relative
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$report.Changed += $relative
|
$report.Changed += $relative
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +291,7 @@ function Invoke-ScratchRestore {
|
|||||||
|
|
||||||
New-Item -ItemType Directory -Path $ScratchLogDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $ScratchLogDir -Force | Out-Null
|
||||||
$before = @(Get-ChildItem -LiteralPath $ScratchLogDir -File -Filter 'restore-*.log' -ErrorAction SilentlyContinue |
|
$before = @(Get-ChildItem -LiteralPath $ScratchLogDir -File -Filter 'restore-*.log' -ErrorAction SilentlyContinue |
|
||||||
Select-Object -ExpandProperty FullName)
|
Select-Object -ExpandProperty FullName)
|
||||||
|
|
||||||
$code = Invoke-ExternalCommand -FilePath $pwshExe -ArgumentList @(
|
$code = Invoke-ExternalCommand -FilePath $pwshExe -ArgumentList @(
|
||||||
'-NoProfile', '-NonInteractive', '-File', $restoreScript,
|
'-NoProfile', '-NonInteractive', '-File', $restoreScript,
|
||||||
@@ -372,7 +376,8 @@ foreach ($name in $Entries) {
|
|||||||
if (-not (Test-Path -LiteralPath $scratchArchive)) {
|
if (-not (Test-Path -LiteralPath $scratchArchive)) {
|
||||||
try {
|
try {
|
||||||
New-Item -ItemType SymbolicLink -Path $scratchArchive -Target $archiveFile.FullName -ErrorAction Stop | Out-Null
|
New-Item -ItemType SymbolicLink -Path $scratchArchive -Target $archiveFile.FullName -ErrorAction Stop | Out-Null
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Copy-Item -LiteralPath $archiveFile.FullName -Destination $scratchArchive -Force
|
Copy-Item -LiteralPath $archiveFile.FullName -Destination $scratchArchive -Force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +421,8 @@ foreach ($name in $Entries) {
|
|||||||
$target = Join-Path $entryRoot ([string]$index)
|
$target = Join-Path $entryRoot ([string]$index)
|
||||||
if ($liveItem.PSIsContainer) {
|
if ($liveItem.PSIsContainer) {
|
||||||
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
[System.IO.File]::WriteAllText($target, '')
|
[System.IO.File]::WriteAllText($target, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,7 +515,8 @@ foreach ($name in $Entries) {
|
|||||||
if ($changed.Count -gt 0) {
|
if ($changed.Count -gt 0) {
|
||||||
if ($AllowChanged) {
|
if ($AllowChanged) {
|
||||||
$detail += ";与活源不一致 $($changed.Count) 个(-AllowChanged,已容忍)"
|
$detail += ";与活源不一致 $($changed.Count) 个(-AllowChanged,已容忍)"
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$status = 'FAIL'
|
$status = 'FAIL'
|
||||||
$detail += ";与活源不一致 $($changed.Count) 个(首例 $($changed[0]))"
|
$detail += ";与活源不一致 $($changed.Count) 个(首例 $($changed[0]))"
|
||||||
$failures += "$name :与活源不一致(首例 $($changed[0]))"
|
$failures += "$name :与活源不一致(首例 $($changed[0]))"
|
||||||
@@ -552,7 +559,8 @@ Write-Host ("恢复演练:通过 {0},跳过 {1},失败 {2}(真正对拍
|
|||||||
|
|
||||||
if ($KeepWorkRoot) {
|
if ($KeepWorkRoot) {
|
||||||
Write-Host "临时工作目录保留在:$WorkRoot" -ForegroundColor Yellow
|
Write-Host "临时工作目录保留在:$WorkRoot" -ForegroundColor Yellow
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-8
@@ -87,7 +87,7 @@ function Get-NewestLog {
|
|||||||
function Get-ArchiveNames {
|
function Get-ArchiveNames {
|
||||||
param([Parameter(Mandatory = $true)][string]$Dir)
|
param([Parameter(Mandatory = $true)][string]$Dir)
|
||||||
return @(Get-ChildItem -LiteralPath $Dir -File -Filter *.7z -ErrorAction SilentlyContinue |
|
return @(Get-ChildItem -LiteralPath $Dir -File -Filter *.7z -ErrorAction SilentlyContinue |
|
||||||
Select-Object -ExpandProperty BaseName)
|
Select-Object -ExpandProperty BaseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-OrphanNames {
|
function Get-OrphanNames {
|
||||||
@@ -101,7 +101,8 @@ function Get-OrphanNames {
|
|||||||
if (-not $inSection) { continue }
|
if (-not $inSection) { continue }
|
||||||
if ($line -match '-\s+([^\s()]+?)(') {
|
if ($line -match '-\s+([^\s()]+?)(') {
|
||||||
$names += $Matches[1]
|
$names += $Matches[1]
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$inSection = $false
|
$inSection = $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +289,8 @@ try {
|
|||||||
Assert-Equal 'backed-up' $acceptedRecord.action
|
Assert-Equal 'backed-up' $acceptedRecord.action
|
||||||
Assert-Equal $true $acceptedRecord.warnings
|
Assert-Equal $true $acceptedRecord.warnings
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$lockStream.Close()
|
$lockStream.Close()
|
||||||
$lockStream.Dispose()
|
$lockStream.Dispose()
|
||||||
}
|
}
|
||||||
@@ -339,10 +341,10 @@ Set-Content -LiteralPath (Join-Path $incDir 'i.txt') -Value 'included' -Encoding
|
|||||||
New-E2EConfig -Path $catConfig -LogDir $catLogDir -SoftwareCatalog $catFile
|
New-E2EConfig -Path $catConfig -LogDir $catLogDir -SoftwareCatalog $catFile
|
||||||
|
|
||||||
$catHashes = @{
|
$catHashes = @{
|
||||||
(Join-Path $dirA 'keep.txt') = Get-Sha256 (Join-Path $dirA 'keep.txt')
|
(Join-Path $dirA 'keep.txt') = Get-Sha256 (Join-Path $dirA 'keep.txt')
|
||||||
(Join-Path $dirA 'sub\keep2.txt') = Get-Sha256 (Join-Path $dirA 'sub\keep2.txt')
|
(Join-Path $dirA 'sub\keep2.txt') = Get-Sha256 (Join-Path $dirA 'sub\keep2.txt')
|
||||||
$settingsFile = Get-Sha256 $settingsFile
|
$settingsFile = Get-Sha256 $settingsFile
|
||||||
(Join-Path $incDir 'i.txt') = Get-Sha256 (Join-Path $incDir 'i.txt')
|
(Join-Path $incDir 'i.txt') = Get-Sha256 (Join-Path $incDir 'i.txt')
|
||||||
}
|
}
|
||||||
|
|
||||||
& $backupScript -BackupListPath $catList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
|
& $backupScript -BackupListPath $catList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
|
||||||
@@ -664,7 +666,8 @@ Test-Case '源路径不存在被记为 missing-source,退出码仍为 0(跳
|
|||||||
|
|
||||||
if ($KeepWorkRoot) {
|
if ($KeepWorkRoot) {
|
||||||
Write-Host "`n工作目录保留在:$WorkRoot" -ForegroundColor Yellow
|
Write-Host "`n工作目录保留在:$WorkRoot" -ForegroundColor Yellow
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,14 +84,17 @@ function Invoke-BaknretCaptured {
|
|||||||
try {
|
try {
|
||||||
& cmd.exe /c $cmdFile | Out-Null
|
& cmd.exe /c $cmdFile | Out-Null
|
||||||
$code = $LASTEXITCODE
|
$code = $LASTEXITCODE
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$ErrorActionPreference = $previous
|
$ErrorActionPreference = $previous
|
||||||
}
|
}
|
||||||
$text = if (Test-Path -LiteralPath $outFile) {
|
$text = if (Test-Path -LiteralPath $outFile) {
|
||||||
Get-Content -Encoding UTF8 -LiteralPath $outFile -Raw
|
Get-Content -Encoding UTF8 -LiteralPath $outFile -Raw
|
||||||
} else { '' }
|
}
|
||||||
|
else { '' }
|
||||||
return [pscustomobject]@{ ExitCode = $code; Output = $text }
|
return [pscustomobject]@{ ExitCode = $code; Output = $text }
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $outFile, $cmdFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $outFile, $cmdFile -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -463,7 +463,8 @@ Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接
|
|||||||
Assert-Equal 'Junction' $dirLink.LinkType
|
Assert-Equal 'Junction' $dirLink.LinkType
|
||||||
Assert-Equal 'HardLink' $fileLink.LinkType
|
Assert-Equal 'HardLink' $fileLink.LinkType
|
||||||
Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容'
|
Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容'
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-BaknretArchiveStaging -Root $staging
|
Remove-BaknretArchiveStaging -Root $staging
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +489,8 @@ Test-Case 'New-BaknretJunction / Remove-BaknretJunction:只删连接点,不
|
|||||||
Remove-BaknretJunction -Path $link
|
Remove-BaknretJunction -Path $link
|
||||||
New-BaknretJunction -Path $link -Target $target | Out-Null
|
New-BaknretJunction -Path $link -Target $target | Out-Null
|
||||||
Assert-True $true
|
Assert-True $true
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-BaknretJunction -Path $link
|
Remove-BaknretJunction -Path $link
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,7 +709,8 @@ Test-Case '口令:环境变量可读取,取不到返回 $null' {
|
|||||||
Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope'))
|
Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope'))
|
||||||
$env:BAKNRET_PASSWORD = 'from-env'
|
$env:BAKNRET_PASSWORD = 'from-env'
|
||||||
Assert-Equal 'from-env' (Get-BaknretPassword)
|
Assert-Equal 'from-env' (Get-BaknretPassword)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
|
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
|
||||||
if ($old) { $env:BAKNRET_PASSWORD = $old }
|
if ($old) { $env:BAKNRET_PASSWORD = $old }
|
||||||
}
|
}
|
||||||
@@ -1071,7 +1074,8 @@ $sevenZip = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First
|
|||||||
|
|
||||||
if (-not $sevenZip) {
|
if (-not $sevenZip) {
|
||||||
Write-Host ' 跳过:未找到 7z' -ForegroundColor Yellow
|
Write-Host ' 跳过:未找到 7z' -ForegroundColor Yellow
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Test-Case '排除规则与空格处理在真实归档上生效' {
|
Test-Case '排除规则与空格处理在真实归档上生效' {
|
||||||
$root = Join-Path $sandbox 'src'
|
$root = Join-Path $sandbox 'src'
|
||||||
$itemName = 'User Data'
|
$itemName = 'User Data'
|
||||||
@@ -1202,7 +1206,8 @@ Test-Case '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参
|
|||||||
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
|
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
|
||||||
Set-BaknretDebug
|
Set-BaknretDebug
|
||||||
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
|
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Set-BaknretDebug -Enabled:$false
|
Set-BaknretDebug -Enabled:$false
|
||||||
Stop-BaknretLog
|
Stop-BaknretLog
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1241,8 @@ Test-Case '空目录的摘要给 0 而不是 $null(否则空间守卫会静默
|
|||||||
Assert-True ($null -ne $summary.TotalSize) 'TotalSize 不能是 $null'
|
Assert-True ($null -ne $summary.TotalSize) 'TotalSize 不能是 $null'
|
||||||
Assert-True ($summary.TotalSize -is [long]) 'TotalSize 应当是整数'
|
Assert-True ($summary.TotalSize -is [long]) 'TotalSize 应当是整数'
|
||||||
Assert-Equal 0 $summary.TotalSize
|
Assert-Equal 0 $summary.TotalSize
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1267,8 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧
|
|||||||
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-True $threw '目标只读时替换应当抛错'
|
||||||
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
|
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue
|
$file = Get-Item -LiteralPath $target -ErrorAction SilentlyContinue
|
||||||
if ($file) { $file.IsReadOnly = $false }
|
if ($file) { $file.IsReadOnly = $false }
|
||||||
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
@@ -1286,7 +1293,8 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' {
|
|||||||
Assert-True ($null -eq $second) '同一目录的第二次不应当拿到锁'
|
Assert-True ($null -eq $second) '同一目录的第二次不应当拿到锁'
|
||||||
|
|
||||||
Assert-FileExists (Get-BaknretRunLockPath -Directory $dir)
|
Assert-FileExists (Get-BaknretRunLockPath -Directory $dir)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Exit-BaknretRunLock -Lock $second
|
Exit-BaknretRunLock -Lock $second
|
||||||
Exit-BaknretRunLock -Lock $first
|
Exit-BaknretRunLock -Lock $first
|
||||||
}
|
}
|
||||||
@@ -1309,7 +1317,8 @@ Test-Case '运行锁:释放之后可以重新取得' {
|
|||||||
$second = Enter-BaknretRunLock -Directory $dir
|
$second = Enter-BaknretRunLock -Directory $dir
|
||||||
Assert-True ($null -ne $second) '释放之后应当能重新拿到锁'
|
Assert-True ($null -ne $second) '释放之后应当能重新拿到锁'
|
||||||
Exit-BaknretRunLock -Lock $second
|
Exit-BaknretRunLock -Lock $second
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ function Test-Case {
|
|||||||
& $Body
|
& $Body
|
||||||
$script:Passed++
|
$script:Passed++
|
||||||
Write-Host " [PASS] $Name" -ForegroundColor Green
|
Write-Host " [PASS] $Name" -ForegroundColor Green
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$script:Failed++
|
$script:Failed++
|
||||||
$script:Failures += "$Name —— $($_.Exception.Message)"
|
$script:Failures += "$Name —— $($_.Exception.Message)"
|
||||||
Write-Host " [FAIL] $Name" -ForegroundColor Red
|
Write-Host " [FAIL] $Name" -ForegroundColor Red
|
||||||
@@ -94,7 +95,8 @@ function Write-TestSummary {
|
|||||||
Write-Host ("=" * 60)
|
Write-Host ("=" * 60)
|
||||||
if ($script:Failed -eq 0) {
|
if ($script:Failed -eq 0) {
|
||||||
Write-Host "$Title 全部通过:$($script:Passed) 项" -ForegroundColor Green
|
Write-Host "$Title 全部通过:$($script:Passed) 项" -ForegroundColor Green
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host "$Title 通过 $($script:Passed) 项,失败 $($script:Failed) 项" -ForegroundColor Red
|
Write-Host "$Title 通过 $($script:Passed) 项,失败 $($script:Failed) 项" -ForegroundColor Red
|
||||||
foreach ($failure in $script:Failures) { Write-Host " - $failure" -ForegroundColor Red }
|
foreach ($failure in $script:Failures) { Write-Host " - $failure" -ForegroundColor Red }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
把测试用的 Pester 5 装进仓库内的 .tools\modules(不动机器上的全局模块)。
|
把测试与静态分析用的模块装进仓库内的 .tools\modules(不动机器上的全局模块)。
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
tests\BakNRet.Tests.ps1 需要 Pester 5.0+。本机常见的坑是:
|
装两样:
|
||||||
* Windows 自带的是 Pester 3.4.0,没有 `Should -Be`,套件会语法错误;
|
* Pester —— tests\BakNRet.Tests.ps1 需要 5.0+。本机常见的坑是 Windows 自带的是
|
||||||
* 直接 Install-Module 会把 5.x 装到全局,可能影响机器上别的、按 3.x 语法写的脚本。
|
3.4.0,没有 `Should -Be`,套件会语法错误;
|
||||||
|
* PSScriptAnalyzer —— tools\Invoke-Analyzer.ps1 用它做"按微软规范"的门禁。
|
||||||
|
|
||||||
所以这里用 Save-Module 把指定版本下载到仓库内的 .tools\modules,
|
两者都用 Save-Module 下载到仓库内,而不是 Install-Module:后者会装到全局,
|
||||||
tests\Run-Pester.ps1 会自动把该目录加进 PSModulePath。
|
可能影响机器上别的、按旧语法写的脚本。.tools\ 已进 .gitignore,不污染版本库。
|
||||||
.tools\ 已进 .gitignore,不会污染版本库。
|
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
pwsh -File .\tools\Install-TestDependencies.ps1
|
pwsh -File .\tools\Install-TestDependencies.ps1
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
pwsh -File .\tools\Install-TestDependencies.ps1 -PesterVersion 5.9.1 -Force
|
pwsh -File .\tools\Install-TestDependencies.ps1 -Force
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[version]$PesterVersion = [version]'5.9.1',
|
[version]$PesterVersion = [version]'5.9.1',
|
||||||
|
[string]$PSScriptAnalyzerVersion = 'latest',
|
||||||
[switch]$Force
|
[switch]$Force
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,40 +33,60 @@ $target = Join-Path $projectRoot '.tools\modules'
|
|||||||
$installed = Join-Path $target ("Pester\{0}" -f $PesterVersion)
|
$installed = Join-Path $target ("Pester\{0}" -f $PesterVersion)
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host ("目标目录 : {0}" -f $target)
|
Write-Host ("目标目录 : {0}" -f $target)
|
||||||
Write-Host ("Pester : {0}" -f $PesterVersion)
|
Write-Host ("Pester : {0}" -f $PesterVersion)
|
||||||
|
Write-Host ("PSScriptAnalyzer: {0}" -f $PSScriptAnalyzerVersion)
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|
||||||
if ((Test-Path -LiteralPath $installed) -and -not $Force) {
|
|
||||||
Write-Host "已经装好了,无需重复下载(要重装加 -Force)。" -ForegroundColor Green
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Force -and (Test-Path -LiteralPath (Join-Path $target 'Pester'))) {
|
|
||||||
Remove-Item -LiteralPath (Join-Path $target 'Pester') -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
|
||||||
|
|
||||||
if (-not (Get-Command Save-Module -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Save-Module -ErrorAction SilentlyContinue)) {
|
||||||
Write-Error '当前环境没有 Save-Module(需要 PowerShellGet 2.x)。请改用:Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.0'
|
Write-Error '当前环境没有 Save-Module(需要 PowerShellGet 2.x)。请改用:Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.0'
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
||||||
Save-Module -Name Pester -RequiredVersion $PesterVersion -Path $target -Force -ErrorAction Stop
|
|
||||||
} catch {
|
$needPester = $Force -or -not (Test-Path -LiteralPath $installed)
|
||||||
Write-Error "下载失败(多半是访问不到 PSGallery):$_"
|
|
||||||
exit 1
|
if ($needPester) {
|
||||||
|
if ($Force -and (Test-Path -LiteralPath (Join-Path $target 'Pester'))) {
|
||||||
|
Remove-Item -LiteralPath (Join-Path $target 'Pester') -Recurse -Force
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Save-Module -Name Pester -RequiredVersion $PesterVersion -Path $target -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "Pester 下载失败(多半是访问不到 PSGallery):$_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host ("已安装:Pester {0}" -f $PesterVersion) -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Pester {0} 已经装好了,跳过(要重装加 -Force)。" -f $PesterVersion -ForegroundColor DarkGray
|
||||||
}
|
}
|
||||||
|
|
||||||
$module = Get-Module -ListAvailable Pester | Where-Object { [version]$_.Version -eq $PesterVersion }
|
# PSScriptAnalyzer:版本目录带具体版本号,所以"装没装过"按目录是否存在判断
|
||||||
if (-not $module) {
|
$analyzerInstalled = @(Test-Path -LiteralPath (Join-Path $target 'PSScriptAnalyzer'))
|
||||||
Write-Error "下载结束但找不到 Pester $PesterVersion,请检查 $target。"
|
if ($Force -or -not $analyzerInstalled) {
|
||||||
exit 1
|
if ($Force -and $analyzerInstalled) {
|
||||||
|
Remove-Item -LiteralPath (Join-Path $target 'PSScriptAnalyzer') -Recurse -Force
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Save-Module -Name PSScriptAnalyzer -Path $target -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "PSScriptAnalyzer 下载失败(多半是访问不到 PSGallery):$_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$analyzerVersion = (Get-ChildItem (Join-Path $target 'PSScriptAnalyzer') -Directory | Select-Object -First 1).Name
|
||||||
|
Write-Host ("已安装:PSScriptAnalyzer {0}" -f $analyzerVersion) -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host 'PSScriptAnalyzer 已经装好了,跳过(要重装加 -Force)。' -ForegroundColor DarkGray
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ("已安装:Pester {0} -> {1}" -f $module.Version, $module.ModuleBase) -ForegroundColor Green
|
Write-Host ''
|
||||||
Write-Host '现在可以跑:.\tests\Run-Pester.ps1' -ForegroundColor Cyan
|
Write-Host '现在可以跑:' -ForegroundColor Cyan
|
||||||
|
Write-Host ' .\tests\Run-Pester.ps1 (单元套件)' -ForegroundColor Gray
|
||||||
|
Write-Host ' .\tools\Invoke-Analyzer.ps1 (静态分析门禁)' -ForegroundColor Gray
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
exit 0
|
exit 0
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
对全仓跑 PSScriptAnalyzer(默认规则集 + 格式规则集),按规则汇总。
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
为什么要有这个入口,而不是直接敲 Invoke-ScriptAnalyzer:
|
||||||
|
|
||||||
|
* 分析器装在**仓库内**(.tools\modules),不能指望机器上全局有一份;
|
||||||
|
* 格式规则默认是 Disabled —— 不带 -Settings 的调用会静默漏掉全部排版问题,
|
||||||
|
那会让"按微软规范检查过了"变成一句空话;
|
||||||
|
* 结果要能一眼看出"哪条规则、几个文件、几行",而不是几百行原始输出;
|
||||||
|
* 路径过滤要与验收门槛一致:.tools\ / Backups\ / logs\ / dist\ 不进分析范围。
|
||||||
|
|
||||||
|
与测试的分工:这是**独立的门禁**,不塞进 Pester 用例。那个套件跑一次二十多秒,
|
||||||
|
把静态分析混进去会让"测试红了"这句话失去分辨力。
|
||||||
|
|
||||||
|
.PARAMETER Severity
|
||||||
|
只报这些级别,默认 Error 与 Warning。
|
||||||
|
|
||||||
|
.PARAMETER Quiet
|
||||||
|
只打印按规则汇总的计数,不逐条列出。
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\tools\Invoke-Analyzer.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\tools\Invoke-Analyzer.ps1 -Quiet
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string[]]$Severity = @('Error', 'Warning'),
|
||||||
|
|
||||||
|
[switch]$Quiet
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$localModules = Join-Path $projectRoot '.tools\modules'
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $localModules 'PSScriptAnalyzer')) {
|
||||||
|
$env:PSModulePath = $localModules + [System.IO.Path]::PathSeparator + $env:PSModulePath
|
||||||
|
}
|
||||||
|
|
||||||
|
$analyzer = Get-Module -ListAvailable PSScriptAnalyzer |
|
||||||
|
Sort-Object { [version]$_.Version } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $analyzer) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '找不到 PSScriptAnalyzer。把它装进仓库(不动机器上的全局模块):' -ForegroundColor Red
|
||||||
|
Write-Host ' .\tools\Install-TestDependencies.ps1' -ForegroundColor Cyan
|
||||||
|
Write-Host ''
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $analyzer.Path -Force
|
||||||
|
|
||||||
|
# 分析范围与验收门槛的 Encode / Parse 两层保持一致:只分析仓库自己的源码
|
||||||
|
$excluded = '\\(\.git|\.tools|\.scratch|Backups|logs|dist|snapshots)\\'
|
||||||
|
$targets = @(Get-ChildItem -LiteralPath $projectRoot -Recurse -File |
|
||||||
|
Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1' } |
|
||||||
|
Where-Object { $_.FullName -notmatch $excluded } |
|
||||||
|
Select-Object -ExpandProperty FullName)
|
||||||
|
|
||||||
|
$settings = Join-Path $projectRoot 'PSScriptAnalyzerSettings.psd1'
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ("PSScriptAnalyzer {0}({1})" -f $analyzer.Version, $analyzer.ModuleBase) -ForegroundColor DarkGray
|
||||||
|
Write-Host ("分析 {0} 个文件,配置 {1}" -f $targets.Count, $settings) -ForegroundColor DarkGray
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# 逐个文件调用:-Path 在这个版本上只接受单个路径(传数组会报 Cannot convert
|
||||||
|
# 'System.Object[]' to the type 'System.String'),而我们要的正是「只分析仓库自己的
|
||||||
|
# 源码」这个范围 —— 自己枚举文件反而更准。
|
||||||
|
$results = @(foreach ($file in $targets) {
|
||||||
|
Invoke-ScriptAnalyzer -Path $file -Settings $settings -Severity $Severity
|
||||||
|
})
|
||||||
|
|
||||||
|
if ($results.Count -eq 0) {
|
||||||
|
Write-Host ("没有 {0} 级别的告警。" -f ($Severity -join '/')) -ForegroundColor Green
|
||||||
|
Write-Host ''
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$byRule = $results | Group-Object RuleName | Sort-Object Count -Descending
|
||||||
|
Write-Host ("共 {0} 条,按规则汇总:" -f $results.Count) -ForegroundColor Yellow
|
||||||
|
foreach ($group in $byRule) {
|
||||||
|
Write-Host (" {0,4} {1}" -f $group.Count, $group.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Quiet) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '逐条:' -ForegroundColor Yellow
|
||||||
|
foreach ($item in ($results | Sort-Object ScriptName, Line)) {
|
||||||
|
$relative = $item.ScriptName.Replace($projectRoot, '').TrimStart('\')
|
||||||
|
Write-Host (" {0}:{1} [{2}] {3}" -f $relative, $item.Line, $item.RuleName, $item.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
exit 1
|
||||||
@@ -78,7 +78,8 @@ if ($Remove) {
|
|||||||
try {
|
try {
|
||||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
||||||
Write-Host "已移除计划任务:$TaskName" -ForegroundColor Green
|
Write-Host "已移除计划任务:$TaskName" -ForegroundColor Green
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Error "移除失败(多半是权限不足,请用管理员终端):$_"
|
Write-Error "移除失败(多半是权限不足,请用管理员终端):$_"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,8 @@ try {
|
|||||||
Write-Host "已注册计划任务:$TaskName(每天 $At)" -ForegroundColor Green
|
Write-Host "已注册计划任务:$TaskName(每天 $At)" -ForegroundColor Green
|
||||||
Write-Host "查看上次运行结果:Get-ScheduledTaskInfo -TaskName '$TaskName'" -ForegroundColor DarkGray
|
Write-Host "查看上次运行结果:Get-ScheduledTaskInfo -TaskName '$TaskName'" -ForegroundColor DarkGray
|
||||||
Write-Host "手动跑一次验证 :Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor DarkGray
|
Write-Host "手动跑一次验证 :Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor DarkGray
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Error "注册失败(多半是权限不足,请用管理员终端):$_"
|
Write-Error "注册失败(多半是权限不足,请用管理员终端):$_"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ foreach ($entry in $plan) {
|
|||||||
|
|
||||||
Write-Host "已重命名: $($entry.Old.Name) -> $($entry.New)" -ForegroundColor Green
|
Write-Host "已重命名: $($entry.Old.Name) -> $($entry.New)" -ForegroundColor Green
|
||||||
$ok++
|
$ok++
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Host "重命名失败: $($entry.Old.Name) —— $_" -ForegroundColor Red
|
Write-Host "重命名失败: $($entry.Old.Name) —— $_" -ForegroundColor Red
|
||||||
$failed++
|
$failed++
|
||||||
}
|
}
|
||||||
@@ -323,7 +324,7 @@ Write-BaknretManifest -Path $manifestPath -Manifest $manifestNew | Out-Null
|
|||||||
|
|
||||||
$referenced = @($rebuilt.Keys)
|
$referenced = @($rebuilt.Keys)
|
||||||
$orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
$orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Extension.ToLower() -in $supportedFormats -and $_.BaseName -notin $referenced })
|
Where-Object { $_.Extension.ToLower() -in $supportedFormats -and $_.BaseName -notin $referenced })
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host "重命名完成:成功 $ok,失败 $failed;manifest 已重建,含 $($rebuilt.Count) 个条目" -ForegroundColor Green
|
Write-Host "重命名完成:成功 $ok,失败 $failed;manifest 已重建,含 $($rebuilt.Count) 个条目" -ForegroundColor Green
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ Write-Host ("受管文件 {0} 个" -f $targets.Count)
|
|||||||
if ($addedBom.Count -gt 0) {
|
if ($addedBom.Count -gt 0) {
|
||||||
Write-Host ("补上 BOM {0} 个:" -f $addedBom.Count) -ForegroundColor Yellow
|
Write-Host ("补上 BOM {0} 个:" -f $addedBom.Count) -ForegroundColor Yellow
|
||||||
$addedBom | ForEach-Object { Write-Host " $_" }
|
$addedBom | ForEach-Object { Write-Host " $_" }
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host '所有文件都已经带 BOM。' -ForegroundColor Green
|
Write-Host '所有文件都已经带 BOM。' -ForegroundColor Green
|
||||||
}
|
}
|
||||||
if ($normalizedLf.Count -gt 0) {
|
if ($normalizedLf.Count -gt 0) {
|
||||||
|
|||||||
+24
-21
@@ -15,20 +15,20 @@
|
|||||||
|
|
||||||
|
|
||||||
$script:LabConfig = [ordered]@{
|
$script:LabConfig = [ordered]@{
|
||||||
VmName = 'BakNRet-Lab'
|
VmName = 'BakNRet-Lab'
|
||||||
LabRoot = 'D:\VMs\BakNRet-Lab'
|
LabRoot = 'D:\VMs\BakNRet-Lab'
|
||||||
VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx'
|
VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx'
|
||||||
VhdxSizeGB = 80
|
VhdxSizeGB = 80
|
||||||
IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso'
|
IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso'
|
||||||
ImageIndex = 4 # Windows 11 专业版
|
ImageIndex = 4 # Windows 11 专业版
|
||||||
SwitchName = 'Default Switch'
|
SwitchName = 'Default Switch'
|
||||||
MemoryStartupGB = 8
|
MemoryStartupGB = 8
|
||||||
CpuCount = 8
|
CpuCount = 8
|
||||||
GuestRepoPath = 'C:\BakNRet'
|
GuestRepoPath = 'C:\BakNRet'
|
||||||
GuestLabPath = 'C:\BakNRet-Lab'
|
GuestLabPath = 'C:\BakNRet-Lab'
|
||||||
GuestUser = 'lab'
|
GuestUser = 'lab'
|
||||||
CheckpointName = 'clean-baseline'
|
CheckpointName = 'clean-baseline'
|
||||||
RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
|
RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-LabConfig { return $script:LabConfig }
|
function Get-LabConfig { return $script:LabConfig }
|
||||||
@@ -44,11 +44,11 @@ function Get-LabPath {
|
|||||||
|
|
||||||
function Write-LabLog {
|
function Write-LabLog {
|
||||||
<# .SYNOPSIS 统一日志:同时进控制台与 $LabRoot\logs\lab.log。 #>
|
<# .SYNOPSIS 统一日志:同时进控制台与 $LabRoot\logs\lab.log。 #>
|
||||||
param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO','WARN','ERROR','STEP')][string]$Level = 'INFO')
|
param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'STEP')][string]$Level = 'INFO')
|
||||||
$line = "[{0}] [{1,-5}] {2}" -f (Get-Date).ToString('HH:mm:ss'), $Level, $Message
|
$line = "[{0}] [{1,-5}] {2}" -f (Get-Date).ToString('HH:mm:ss'), $Level, $Message
|
||||||
switch ($Level) {
|
switch ($Level) {
|
||||||
'STEP' { Write-Host $line -ForegroundColor Cyan }
|
'STEP' { Write-Host $line -ForegroundColor Cyan }
|
||||||
'WARN' { Write-Host $line -ForegroundColor Yellow }
|
'WARN' { Write-Host $line -ForegroundColor Yellow }
|
||||||
'ERROR' { Write-Host $line -ForegroundColor Red }
|
'ERROR' { Write-Host $line -ForegroundColor Red }
|
||||||
default { Write-Host $line }
|
default { Write-Host $line }
|
||||||
}
|
}
|
||||||
@@ -65,8 +65,8 @@ function Assert-LabElevated {
|
|||||||
param([Parameter(Mandatory)][string]$Why)
|
param([Parameter(Mandatory)][string]$Why)
|
||||||
if (Test-LabElevated) { return }
|
if (Test-LabElevated) { return }
|
||||||
$gsudo = (Get-Command gsudo -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
$gsudo = (Get-Command gsudo -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
||||||
$self = $MyInvocation.PSCommandPath
|
$self = $MyInvocation.PSCommandPath
|
||||||
$hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' }
|
$hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' }
|
||||||
throw "需要管理员权限:$Why$hint"
|
throw "需要管理员权限:$Why$hint"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,8 @@ function New-LabSession {
|
|||||||
$s = New-PSSession -VMName $script:LabConfig.VmName -Credential $cred -ErrorAction Stop
|
$s = New-PSSession -VMName $script:LabConfig.VmName -Credential $cred -ErrorAction Stop
|
||||||
Write-LabLog "PowerShell Direct 会话已建立(等待 $([math]::Round($sw.Elapsed.TotalSeconds)) 秒)"
|
Write-LabLog "PowerShell Direct 会话已建立(等待 $([math]::Round($sw.Elapsed.TotalSeconds)) 秒)"
|
||||||
return $s
|
return $s
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$lastError = $_.Exception.Message
|
$lastError = $_.Exception.Message
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,8 @@ function Invoke-LabCommand {
|
|||||||
$s = New-LabSession -RetrySeconds $RetrySeconds
|
$s = New-LabSession -RetrySeconds $RetrySeconds
|
||||||
try {
|
try {
|
||||||
return Invoke-Command -Session $s -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
|
return Invoke-Command -Session $s -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
|
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,5 +180,6 @@ function Test-LabGuestReady {
|
|||||||
try {
|
try {
|
||||||
$r = Invoke-LabCommand -ScriptBlock { Test-Path 'C:\BakNRet-Lab\state\provision.ok' } -RetrySeconds 60
|
$r = Invoke-LabCommand -ScriptBlock { Test-Path 'C:\BakNRet-Lab\state\provision.ok' } -RetrySeconds 60
|
||||||
return [bool]$r
|
return [bool]$r
|
||||||
} catch { return $false }
|
}
|
||||||
|
catch { return $false }
|
||||||
}
|
}
|
||||||
+25
-22
@@ -38,10 +38,10 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory, Position = 0)]
|
[Parameter(Mandatory, Position = 0)]
|
||||||
[ValidateSet('status','start','stop','wait','sync','seed','backup','restore','acl-test','test','shell','console','checkpoint','reset','destroy')]
|
[ValidateSet('status', 'start', 'stop', 'wait', 'sync', 'seed', 'backup', 'restore', 'acl-test', 'test', 'shell', 'console', 'checkpoint', 'reset', 'destroy')]
|
||||||
[string]$Verb,
|
[string]$Verb,
|
||||||
|
|
||||||
[ValidateSet('all','pester','zero','e2e')][string]$Suite = 'all',
|
[ValidateSet('all', 'pester', 'zero', 'e2e')][string]$Suite = 'all',
|
||||||
|
|
||||||
# 恢复演练要处理的条目(写法同 BackupList.txt 的一行)
|
# 恢复演练要处理的条目(写法同 BackupList.txt 的一行)
|
||||||
[string[]]$Entries,
|
[string[]]$Entries,
|
||||||
@@ -63,10 +63,10 @@ $ErrorActionPreference = 'Stop'
|
|||||||
. (Join-Path $PSScriptRoot 'Lab-Common.ps1')
|
. (Join-Path $PSScriptRoot 'Lab-Common.ps1')
|
||||||
$cfg = Get-LabConfig
|
$cfg = Get-LabConfig
|
||||||
|
|
||||||
$guestSandbox = "$($cfg.GuestRepoPath)\tools\lab\payload\sandbox"
|
$guestSandbox = "$($cfg.GuestRepoPath)\tools\lab\payload\sandbox"
|
||||||
$guestList = "$guestSandbox\BackupList.txt"
|
$guestList = "$guestSandbox\BackupList.txt"
|
||||||
$guestConfig = "$guestSandbox\BackupConfig.psd1"
|
$guestConfig = "$guestSandbox\BackupConfig.psd1"
|
||||||
$guestFixture = "$($cfg.GuestRepoPath)\tools\lab\payload\lab-fixtures.ps1"
|
$guestFixture = "$($cfg.GuestRepoPath)\tools\lab\payload\lab-fixtures.ps1"
|
||||||
$guestBackupDir = 'C:\BakNRet-Lab\Backups'
|
$guestBackupDir = 'C:\BakNRet-Lab\Backups'
|
||||||
|
|
||||||
Assert-LabElevated -Why "Hyper-V 操作与 PowerShell Direct 都需要管理员(动词:$Verb)"
|
Assert-LabElevated -Why "Hyper-V 操作与 PowerShell Direct 都需要管理员(动词:$Verb)"
|
||||||
@@ -80,12 +80,12 @@ function Get-VmSummary {
|
|||||||
if (-not $vm) { return $null }
|
if (-not $vm) { return $null }
|
||||||
$mem = Get-VMMemory -VMName $cfg.VmName
|
$mem = Get-VMMemory -VMName $cfg.VmName
|
||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
Name = $vm.Name
|
Name = $vm.Name
|
||||||
State = $vm.State
|
State = $vm.State
|
||||||
Uptime = [int]$vm.Uptime.TotalSeconds
|
Uptime = [int]$vm.Uptime.TotalSeconds
|
||||||
Cpu = $vm.ProcessorCount
|
Cpu = $vm.ProcessorCount
|
||||||
MemoryGB = [math]::Round($mem.Startup / 1GB, 1)
|
MemoryGB = [math]::Round($mem.Startup / 1GB, 1)
|
||||||
Gen = $vm.Generation
|
Gen = $vm.Generation
|
||||||
UptimeText = "$([int]$vm.Uptime.TotalMinutes) 分钟"
|
UptimeText = "$([int]$vm.Uptime.TotalMinutes) 分钟"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ function Invoke-GuestScriptFile {
|
|||||||
$text = if ($badUtf8 -le $badAnsi) { $asUtf8 } else { $asAnsi }
|
$text = if ($badUtf8 -le $badAnsi) { $asUtf8 } else { $asAnsi }
|
||||||
return @($text -split "`r?`n" | Where-Object { $_ -ne '' } | Select-Object -Last $lines)
|
return @($text -split "`r?`n" | Where-Object { $_ -ne '' } | Select-Object -Last $lines)
|
||||||
}
|
}
|
||||||
$all = @('-NoProfile','-ExecutionPolicy','Bypass','-File',$script) + $scriptArgs
|
$all = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $script) + $scriptArgs
|
||||||
$out = $logPath
|
$out = $logPath
|
||||||
$err = "$logPath.err"
|
$err = "$logPath.err"
|
||||||
$p = Start-Process -FilePath 'pwsh.exe' -ArgumentList $all -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err
|
$p = Start-Process -FilePath 'pwsh.exe' -ArgumentList $all -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err
|
||||||
@@ -147,7 +147,8 @@ function Invoke-LabSync {
|
|||||||
Push-Location $cfg.RepoRoot
|
Push-Location $cfg.RepoRoot
|
||||||
try {
|
try {
|
||||||
& $sevenZip a -tzip $zip '.\*' '-xr!Backups' '-xr!logs' '-xr!.git' '-xr!.tools' '-xr!*.tmp.7z' '-xr!*.tmp.zip' -y | Out-Null
|
& $sevenZip a -tzip $zip '.\*' '-xr!Backups' '-xr!logs' '-xr!.git' '-xr!.tools' '-xr!*.tmp.7z' '-xr!*.tmp.zip' -y | Out-Null
|
||||||
} finally { Pop-Location }
|
}
|
||||||
|
finally { Pop-Location }
|
||||||
Write-LabLog ("快照大小 {0} MB" -f [math]::Round((Get-Item -LiteralPath $zip).Length / 1MB, 2))
|
Write-LabLog ("快照大小 {0} MB" -f [math]::Round((Get-Item -LiteralPath $zip).Length / 1MB, 2))
|
||||||
|
|
||||||
Write-LabLog '推送到 VM(Copy-VMFile,走 VMBus)' 'STEP'
|
Write-LabLog '推送到 VM(Copy-VMFile,走 VMBus)' 'STEP'
|
||||||
@@ -214,7 +215,7 @@ switch ($Verb) {
|
|||||||
$arch = @()
|
$arch = @()
|
||||||
if (Test-Path 'C:\BakNRet-Lab\Backups') {
|
if (Test-Path 'C:\BakNRet-Lab\Backups') {
|
||||||
$arch = @(Get-ChildItem 'C:\BakNRet-Lab\Backups' -Filter *.7z -ErrorAction SilentlyContinue |
|
$arch = @(Get-ChildItem 'C:\BakNRet-Lab\Backups' -Filter *.7z -ErrorAction SilentlyContinue |
|
||||||
ForEach-Object { [pscustomobject]@{ Name = $_.BaseName; MB = [math]::Round($_.Length / 1MB, 2) } })
|
ForEach-Object { [pscustomobject]@{ Name = $_.BaseName; MB = [math]::Round($_.Length / 1MB, 2) } })
|
||||||
}
|
}
|
||||||
$src = 'C:\BakNRet-Lab\sources'
|
$src = 'C:\BakNRet-Lab\sources'
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
@@ -226,7 +227,7 @@ switch ($Verb) {
|
|||||||
SourceMB = $(if (Test-Path $src) { [math]::Round(((Get-ChildItem $src -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum) / 1MB, 1) } else { 0 })
|
SourceMB = $(if (Test-Path $src) { [math]::Round(((Get-ChildItem $src -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum) / 1MB, 1) } else { 0 })
|
||||||
Archives = $arch
|
Archives = $arch
|
||||||
LastLog = (Get-ChildItem 'C:\BakNRet-Lab\logs' -Filter 'backup-*.log' -ErrorAction SilentlyContinue |
|
LastLog = (Get-ChildItem 'C:\BakNRet-Lab\logs' -Filter 'backup-*.log' -ErrorAction SilentlyContinue |
|
||||||
Sort-Object LastWriteTime | Select-Object -Last 1 -ExpandProperty Name)
|
Sort-Object LastWriteTime | Select-Object -Last 1 -ExpandProperty Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Write-Host ("VM 内 : 供给={0} {1} (build {2}) PS={3}" -f $g.Provisioned, $g.OsCaption, $g.OsBuild, $g.GuestPS)
|
Write-Host ("VM 内 : 供给={0} {1} (build {2}) PS={3}" -f $g.Provisioned, $g.OsCaption, $g.OsBuild, $g.GuestPS)
|
||||||
@@ -235,9 +236,11 @@ switch ($Verb) {
|
|||||||
if ($g.Archives.Count -gt 0) {
|
if ($g.Archives.Count -gt 0) {
|
||||||
Write-Host ("沙盒归档 : {0} 个({1} MB 合计)" -f $g.Archives.Count, [math]::Round((($g.Archives | Measure-Object MB -Sum).Sum), 1))
|
Write-Host ("沙盒归档 : {0} 个({1} MB 合计)" -f $g.Archives.Count, [math]::Round((($g.Archives | Measure-Object MB -Sum).Sum), 1))
|
||||||
$g.Archives | Sort-Object MB -Descending | ForEach-Object { Write-Host (" {0,-52} {1,8} MB" -f $_.Name, $_.MB) }
|
$g.Archives | Sort-Object MB -Descending | ForEach-Object { Write-Host (" {0,-52} {1,8} MB" -f $_.Name, $_.MB) }
|
||||||
} else { Write-Host '沙盒归档 : (还没有,跑 Lab.ps1 backup)' }
|
}
|
||||||
|
else { Write-Host '沙盒归档 : (还没有,跑 Lab.ps1 backup)' }
|
||||||
if ($g.LastLog) { Write-Host ("最近备份日志: {0}" -f $g.LastLog) }
|
if ($g.LastLog) { Write-Host ("最近备份日志: {0}" -f $g.LastLog) }
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Host ("VM 内查询失败(可能还没起来):{0}" -f $_.Exception.Message) -ForegroundColor Yellow
|
Write-Host ("VM 内查询失败(可能还没起来):{0}" -f $_.Exception.Message) -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,8 +340,8 @@ switch ($Verb) {
|
|||||||
'test' {
|
'test' {
|
||||||
$map = [ordered]@{
|
$map = [ordered]@{
|
||||||
pester = @{ Path = 'tests\Run-Pester.ps1'; Args = @(); Name = 'Pester 套件' }
|
pester = @{ Path = 'tests\Run-Pester.ps1'; Args = @(); Name = 'Pester 套件' }
|
||||||
zero = @{ Path = 'tests\Run-Tests.ps1'; Args = @(); Name = '零依赖套件' }
|
zero = @{ Path = 'tests\Run-Tests.ps1'; Args = @(); Name = '零依赖套件' }
|
||||||
e2e = @{ Path = 'tests\Run-E2E.ps1'; Args = @(); Name = '端到端验收' }
|
e2e = @{ Path = 'tests\Run-E2E.ps1'; Args = @(); Name = '端到端验收' }
|
||||||
}
|
}
|
||||||
$pick = if ($Suite -eq 'all') { @($map.Keys) } else { @($Suite) }
|
$pick = if ($Suite -eq 'all') { @($map.Keys) } else { @($Suite) }
|
||||||
|
|
||||||
@@ -367,7 +370,7 @@ switch ($Verb) {
|
|||||||
$color = if ($_.ExitCode -eq 0) { 'Green' } else { 'Red' }
|
$color = if ($_.ExitCode -eq 0) { 'Green' } else { 'Red' }
|
||||||
Write-Host (" {0,-14} 退出码 {1} 日志 {2}" -f $_.Suite, $_.ExitCode, $_.Log) -ForegroundColor $color
|
Write-Host (" {0,-14} 退出码 {1} 日志 {2}" -f $_.Suite, $_.ExitCode, $_.Log) -ForegroundColor $color
|
||||||
}
|
}
|
||||||
$bad = @($results | Where-Object ExitCode -ne 0)
|
$bad = @($results | Where-Object ExitCode -NE 0)
|
||||||
if ($bad.Count -gt 0) { throw ("有 {0} 套件失败" -f $bad.Count) }
|
if ($bad.Count -gt 0) { throw ("有 {0} 套件失败" -f $bad.Count) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +401,7 @@ switch ($Verb) {
|
|||||||
|
|
||||||
'reset' {
|
'reset' {
|
||||||
if (-not $CheckpointName) { $CheckpointName = $cfg.CheckpointName }
|
if (-not $CheckpointName) { $CheckpointName = $cfg.CheckpointName }
|
||||||
$snap = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -eq $CheckpointName
|
$snap = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -EQ $CheckpointName
|
||||||
if (-not $snap) { throw "找不到检查点 $CheckpointName" }
|
if (-not $snap) { throw "找不到检查点 $CheckpointName" }
|
||||||
Write-LabLog "回到检查点 $CheckpointName" 'STEP'
|
Write-LabLog "回到检查点 $CheckpointName" 'STEP'
|
||||||
Restore-VMSnapshot -VMSnapshot $snap -Confirm:$false
|
Restore-VMSnapshot -VMSnapshot $snap -Confirm:$false
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[ValidateSet('all','disk','vm','provision')][string]$Stage = 'all',
|
[ValidateSet('all', 'disk', 'vm', 'provision')][string]$Stage = 'all',
|
||||||
[switch]$Recreate,
|
[switch]$Recreate,
|
||||||
[switch]$ListImages,
|
[switch]$ListImages,
|
||||||
[int]$ImageIndex = 0
|
[int]$ImageIndex = 0
|
||||||
@@ -54,14 +54,14 @@ function Get-IsoVolume {
|
|||||||
|
|
||||||
function Get-ImageList {
|
function Get-ImageList {
|
||||||
param([Parameter(Mandatory)][string]$IsoLetter)
|
param([Parameter(Mandatory)][string]$IsoLetter)
|
||||||
$wim = @('install.wim','install.esd') |
|
$wim = @('install.wim', 'install.esd') |
|
||||||
ForEach-Object { Join-Path "$IsoLetter`:\sources" $_ } |
|
ForEach-Object { Join-Path "$IsoLetter`:\sources" $_ } |
|
||||||
Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||||
if (-not $wim) { throw "ISO ($IsoLetter`:) 里找不到 sources\install.wim|esd" }
|
if (-not $wim) { throw "ISO ($IsoLetter`:) 里找不到 sources\install.wim|esd" }
|
||||||
$info = & dism.exe /English /Get-WimInfo /WimFile:"$wim" 2>&1
|
$info = & dism.exe /English /Get-WimInfo /WimFile:"$wim" 2>&1
|
||||||
$list = @(); $cur = $null
|
$list = @(); $cur = $null
|
||||||
foreach ($line in $info) {
|
foreach ($line in $info) {
|
||||||
if ($line -match '^Index\s*:\s*(\d+)') { if ($cur) { $list += $cur }; $cur = [ordered]@{ Index = [int]$Matches[1]; Name = ''; Size = '' } }
|
if ($line -match '^Index\s*:\s*(\d+)') { if ($cur) { $list += $cur }; $cur = [ordered]@{ Index = [int]$Matches[1]; Name = ''; Size = '' } }
|
||||||
elseif ($cur -and $line -match '^Name\s*:\s*(.+?)\s*$') { $cur.Name = $Matches[1] }
|
elseif ($cur -and $line -match '^Name\s*:\s*(.+?)\s*$') { $cur.Name = $Matches[1] }
|
||||||
elseif ($cur -and $line -match '^Size\s*:\s*(.+?)\s*$') { $cur.Size = $Matches[1] }
|
elseif ($cur -and $line -match '^Size\s*:\s*(.+?)\s*$') { $cur.Size = $Matches[1] }
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ function New-LabSystemDisk {
|
|||||||
Write-LabLog "已创建动态 VHDX($($cfg.VhdxSizeGB) GB):$($cfg.VhdxPath)" 'STEP'
|
Write-LabLog "已创建动态 VHDX($($cfg.VhdxSizeGB) GB):$($cfg.VhdxPath)" 'STEP'
|
||||||
}
|
}
|
||||||
|
|
||||||
$vhd = Mount-VHD -Path $cfg.VhdxPath -Passthru
|
$vhd = Mount-VHD -Path $cfg.VhdxPath -Passthru
|
||||||
$disk = $vhd | Get-Disk
|
$disk = $vhd | Get-Disk
|
||||||
|
|
||||||
if ($disk.PartitionStyle -eq 'RAW') {
|
if ($disk.PartitionStyle -eq 'RAW') {
|
||||||
@@ -115,7 +115,7 @@ function New-LabSystemDisk {
|
|||||||
Write-LabLog '已完成 GPT 分区(ESP 类型已按 EFI System Partition 建立)与格式化' 'STEP'
|
Write-LabLog '已完成 GPT 分区(ESP 类型已按 EFI System Partition 建立)与格式化' 'STEP'
|
||||||
}
|
}
|
||||||
|
|
||||||
$efiPart = Get-Partition -DiskNumber $disk.Number | Where-Object GptType -eq $espGuid
|
$efiPart = Get-Partition -DiskNumber $disk.Number | Where-Object GptType -EQ $espGuid
|
||||||
$winPart = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -and $_.Size -gt 1GB }
|
$winPart = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -and $_.Size -gt 1GB }
|
||||||
if (-not $efiPart -or -not $winPart) { throw '分区布局不符合预期(ESP / Windows 分区没找到)' }
|
if (-not $efiPart -or -not $winPart) { throw '分区布局不符合预期(ESP / Windows 分区没找到)' }
|
||||||
$efiLetter = $efiPart.DriveLetter
|
$efiLetter = $efiPart.DriveLetter
|
||||||
@@ -129,30 +129,32 @@ function New-LabSystemDisk {
|
|||||||
$di = Get-IsoVolume
|
$di = Get-IsoVolume
|
||||||
$isoLetter = ($di | Get-Volume).DriveLetter
|
$isoLetter = ($di | Get-Volume).DriveLetter
|
||||||
$il = Get-ImageList -IsoLetter $isoLetter
|
$il = Get-ImageList -IsoLetter $isoLetter
|
||||||
$pick = $il.Images | Where-Object Index -eq $cfg.ImageIndex
|
$pick = $il.Images | Where-Object Index -EQ $cfg.ImageIndex
|
||||||
if (-not $pick) { throw "ISO 里没有索引 $($cfg.ImageIndex);可用:$($il.Images.Index -join ', ')" }
|
if (-not $pick) { throw "ISO 里没有索引 $($cfg.ImageIndex);可用:$($il.Images.Index -join ', ')" }
|
||||||
Write-LabLog "展开映像 [$($pick.Index)] $($pick.Name) -> $winLetter`:(需要十几分钟)" 'STEP'
|
Write-LabLog "展开映像 [$($pick.Index)] $($pick.Name) -> $winLetter`:(需要十几分钟)" 'STEP'
|
||||||
$scratch = Get-LabPath 'scratch'
|
$scratch = Get-LabPath 'scratch'
|
||||||
$out = Get-LabPath 'logs\dism-apply.out'
|
$out = Get-LabPath 'logs\dism-apply.out'
|
||||||
$err = Get-LabPath 'logs\dism-apply.err'
|
$err = Get-LabPath 'logs\dism-apply.err'
|
||||||
$proc = Start-Process -FilePath 'dism.exe' -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err `
|
$proc = Start-Process -FilePath 'dism.exe' -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err `
|
||||||
-ArgumentList @('/English','/Apply-Image',"/ImageFile:$($il.WimPath)", "/Index:$($pick.Index)", "/ApplyDir:$winLetter`:\", "/ScratchDir:$scratch")
|
-ArgumentList @('/English', '/Apply-Image', "/ImageFile:$($il.WimPath)", "/Index:$($pick.Index)", "/ApplyDir:$winLetter`:\", "/ScratchDir:$scratch")
|
||||||
if ($proc.ExitCode -ne 0) { throw "DISM 展开失败,退出码 $($proc.ExitCode),见 $out / $err" }
|
if ($proc.ExitCode -ne 0) { throw "DISM 展开失败,退出码 $($proc.ExitCode),见 $out / $err" }
|
||||||
Write-LabLog '映像展开完成' 'STEP'
|
Write-LabLog '映像展开完成' 'STEP'
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-LabLog '系统盘上已有 Windows,跳过展开' 'WARN'
|
Write-LabLog '系统盘上已有 Windows,跳过展开' 'WARN'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- 注入负载与无人值守应答文件 ----
|
# ---- 注入负载与无人值守应答文件 ----
|
||||||
Write-LabLog '注入 7-Zip / PowerShell 7 / Pester / 供给脚本' 'STEP'
|
Write-LabLog '注入 7-Zip / PowerShell 7 / Pester / 供给脚本' 'STEP'
|
||||||
$payloadSrc = Join-Path $PSScriptRoot 'payload'
|
$payloadSrc = Join-Path $PSScriptRoot 'payload'
|
||||||
$guestLab = Join-Path "$winLetter`:\" ($cfg.GuestLabPath.TrimStart('\'))
|
$guestLab = Join-Path "$winLetter`:\" ($cfg.GuestLabPath.TrimStart('\'))
|
||||||
foreach ($item in '7zip','pwsh','Pester','provision.ps1') {
|
foreach ($item in '7zip', 'pwsh', 'Pester', 'provision.ps1') {
|
||||||
$src = Join-Path $payloadSrc $item
|
$src = Join-Path $payloadSrc $item
|
||||||
$dst = Join-Path $guestLab ('payload\' + $item)
|
$dst = Join-Path $guestLab ('payload\' + $item)
|
||||||
if (Test-Path -LiteralPath $src) {
|
if (Test-Path -LiteralPath $src) {
|
||||||
$null = robocopy $src $dst /MIR /NFL /NDL /NJH /NJS /NP /R:1 /W:1
|
$null = robocopy $src $dst /MIR /NFL /NDL /NJH /NJS /NP /R:1 /W:1
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-LabLog "负载缺失(跳过):$src" 'WARN'
|
Write-LabLog "负载缺失(跳过):$src" 'WARN'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,16 +193,17 @@ function New-LabVM {
|
|||||||
if (-not $vm) {
|
if (-not $vm) {
|
||||||
Write-LabLog "创建虚拟机 $($cfg.VmName)(Gen2 / $($cfg.MemoryStartupGB) GB / $($cfg.CpuCount) vCPU)" 'STEP'
|
Write-LabLog "创建虚拟机 $($cfg.VmName)(Gen2 / $($cfg.MemoryStartupGB) GB / $($cfg.CpuCount) vCPU)" 'STEP'
|
||||||
$vm = New-VM -Name $cfg.VmName -Generation 2 -MemoryStartupBytes ($cfg.MemoryStartupGB * 1GB) `
|
$vm = New-VM -Name $cfg.VmName -Generation 2 -MemoryStartupBytes ($cfg.MemoryStartupGB * 1GB) `
|
||||||
-VHDPath $cfg.VhdxPath -SwitchName $cfg.SwitchName
|
-VHDPath $cfg.VhdxPath -SwitchName $cfg.SwitchName
|
||||||
Set-VMProcessor -VMName $cfg.VmName -Count $cfg.CpuCount
|
Set-VMProcessor -VMName $cfg.VmName -Count $cfg.CpuCount
|
||||||
Set-VMFirmware -VMName $cfg.VmName -EnableSecureBoot Off
|
Set-VMFirmware -VMName $cfg.VmName -EnableSecureBoot Off
|
||||||
Set-VM -Name $cfg.VmName -AutomaticStopAction TurnOff -AutomaticStartAction Nothing
|
Set-VM -Name $cfg.VmName -AutomaticStopAction TurnOff -AutomaticStartAction Nothing
|
||||||
# 集成服务名随系统语言变化(中文是「来宾服务接口」),按状态启用而不是按名字找
|
# 集成服务名随系统语言变化(中文是「来宾服务接口」),按状态启用而不是按名字找
|
||||||
Get-VMIntegrationService -VMName $cfg.VmName | Where-Object { -not $_.Enabled } |
|
Get-VMIntegrationService -VMName $cfg.VmName | Where-Object { -not $_.Enabled } |
|
||||||
ForEach-Object { Enable-VMIntegrationService -VMName $cfg.VmName -Name $_.Name }
|
ForEach-Object { Enable-VMIntegrationService -VMName $cfg.VmName -Name $_.Name }
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-LabLog "虚拟机 $($cfg.VmName) 已存在(状态 $($vm.State))" 'WARN'
|
Write-LabLog "虚拟机 $($cfg.VmName) 已存在(状态 $($vm.State))" 'WARN'
|
||||||
if (-not (Get-VMHardDiskDrive -VMName $cfg.VmName | Where-Object Path -eq $cfg.VhdxPath)) {
|
if (-not (Get-VMHardDiskDrive -VMName $cfg.VmName | Where-Object Path -EQ $cfg.VhdxPath)) {
|
||||||
Add-VMHardDiskDrive -VMName $cfg.VmName -Path $cfg.VhdxPath
|
Add-VMHardDiskDrive -VMName $cfg.VmName -Path $cfg.VhdxPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +238,7 @@ function Wait-LabProvision {
|
|||||||
|
|
||||||
function New-LabCheckpoint {
|
function New-LabCheckpoint {
|
||||||
Assert-LabElevated -Why '创建 Hyper-V 检查点'
|
Assert-LabElevated -Why '创建 Hyper-V 检查点'
|
||||||
$existing = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -eq $cfg.CheckpointName
|
$existing = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -EQ $cfg.CheckpointName
|
||||||
if ($existing) { Write-LabLog "检查点 $($cfg.CheckpointName) 已存在,跳过" 'WARN'; return }
|
if ($existing) { Write-LabLog "检查点 $($cfg.CheckpointName) 已存在,跳过" 'WARN'; return }
|
||||||
Checkpoint-VM -Name $cfg.VmName -SnapshotName $cfg.CheckpointName
|
Checkpoint-VM -Name $cfg.VmName -SnapshotName $cfg.CheckpointName
|
||||||
Write-LabLog "已创建检查点 $($cfg.CheckpointName)(Lab.ps1 -Verb reset 可回到此状态)" 'STEP'
|
Write-LabLog "已创建检查点 $($cfg.CheckpointName)(Lab.ps1 -Verb reset 可回到此状态)" 'STEP'
|
||||||
@@ -245,8 +248,8 @@ function New-LabCheckpoint {
|
|||||||
# 主流程
|
# 主流程
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
if ($Stage -in @('all','disk')) { New-LabSystemDisk }
|
if ($Stage -in @('all', 'disk')) { New-LabSystemDisk }
|
||||||
if ($Stage -in @('all','vm')) { New-LabVM }
|
if ($Stage -in @('all', 'vm')) { New-LabVM }
|
||||||
if ($Stage -in @('all','provision')) { Wait-LabProvision; New-LabCheckpoint }
|
if ($Stage -in @('all', 'provision')) { Wait-LabProvision; New-LabCheckpoint }
|
||||||
|
|
||||||
Write-LabLog '搭建流程结束' 'STEP'
|
Write-LabLog '搭建流程结束' 'STEP'
|
||||||
@@ -34,7 +34,8 @@ function New-TextFile {
|
|||||||
if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
||||||
if ($Count -le 1) {
|
if ($Count -le 1) {
|
||||||
Set-Content -LiteralPath $Path -Value $Content -Encoding UTF8
|
Set-Content -LiteralPath $Path -Value $Content -Encoding UTF8
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Set-Content -LiteralPath $Path -Value (1..$Count | ForEach-Object { "$Content #$_" }) -Encoding UTF8
|
Set-Content -LiteralPath $Path -Value (1..$Count | ForEach-Object { "$Content #$_" }) -Encoding UTF8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,7 @@ New-TextFile (Join-Path $appC '数据 备份\记录.txt') '记录内容' -Count
|
|||||||
|
|
||||||
# --- 4. 真 NTFS 连接点 -------------------------------------------------------
|
# --- 4. 真 NTFS 连接点 -------------------------------------------------------
|
||||||
$realTarget = Join-Path $Root 'AppMultiSlot\Data'
|
$realTarget = Join-Path $Root 'AppMultiSlot\Data'
|
||||||
$junction = Join-Path $Root 'JunctionToData'
|
$junction = Join-Path $Root 'JunctionToData'
|
||||||
if (-not (Test-Path -LiteralPath $junction)) {
|
if (-not (Test-Path -LiteralPath $junction)) {
|
||||||
$null = New-Item -ItemType Junction -Path $junction -Target $realTarget -ErrorAction SilentlyContinue
|
$null = New-Item -ItemType Junction -Path $junction -Target $realTarget -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -86,21 +87,22 @@ New-TextFile (Join-Path $cursor 'bottom.txt') 'deep content'
|
|||||||
Write-Host ("最长路径长度:{0} 字符" -f (Join-Path $cursor 'bottom.txt').Length)
|
Write-Host ("最长路径长度:{0} 字符" -f (Join-Path $cursor 'bottom.txt').Length)
|
||||||
|
|
||||||
# --- 6. 50 MB 大文件 ---------------------------------------------------------
|
# --- 6. 50 MB 大文件 ---------------------------------------------------------
|
||||||
$bigDir = Join-Path $Root 'AppBig'
|
$bigDir = Join-Path $Root 'AppBig'
|
||||||
$bigFile = Join-Path $bigDir 'blob-50mb.bin'
|
$bigFile = Join-Path $bigDir 'blob-50mb.bin'
|
||||||
if (-not (Test-Path -LiteralPath $bigFile)) {
|
if (-not (Test-Path -LiteralPath $bigFile)) {
|
||||||
New-Item -ItemType Directory -Force -Path $bigDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $bigDir | Out-Null
|
||||||
$fs = [IO.File]::Create($bigFile)
|
$fs = [IO.File]::Create($bigFile)
|
||||||
try {
|
try {
|
||||||
$rng = [Random]::new(20260926)
|
$rng = [Random]::new(20260926)
|
||||||
$chunk = [byte[]]::new(1MB)
|
$chunk = [byte[]]::new(1MB)
|
||||||
for ($i = 0; $i -lt 50; $i++) { $rng.NextBytes($chunk); $fs.Write($chunk, 0, $chunk.Length) }
|
for ($i = 0; $i -lt 50; $i++) { $rng.NextBytes($chunk); $fs.Write($chunk, 0, $chunk.Length) }
|
||||||
} finally { $fs.Dispose() }
|
}
|
||||||
|
finally { $fs.Dispose() }
|
||||||
}
|
}
|
||||||
Write-Host ("大文件:{0} MB" -f [math]::Round((Get-Item $bigFile).Length/1MB,1))
|
Write-Host ("大文件:{0} MB" -f [math]::Round((Get-Item $bigFile).Length / 1MB, 1))
|
||||||
|
|
||||||
# --- 7. 被占用文件(后台进程持句柄 90 秒后释放)-----------------------------
|
# --- 7. 被占用文件(后台进程持句柄 90 秒后释放)-----------------------------
|
||||||
$lockDir = Join-Path $Root 'AppLocked'
|
$lockDir = Join-Path $Root 'AppLocked'
|
||||||
$lockFile = Join-Path $lockDir 'locked.bin'
|
$lockFile = Join-Path $lockDir 'locked.bin'
|
||||||
New-Item -ItemType Directory -Force -Path $lockDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $lockDir | Out-Null
|
||||||
New-TextFile $lockFile 'this file is held open by another process'
|
New-TextFile $lockFile 'this file is held open by another process'
|
||||||
@@ -111,7 +113,7 @@ $holderLines = @(
|
|||||||
)
|
)
|
||||||
$holderPath = 'C:\BakNRet-Lab\state\hold-lock.ps1'
|
$holderPath = 'C:\BakNRet-Lab\state\hold-lock.ps1'
|
||||||
Set-Content -LiteralPath $holderPath -Value $holderLines -Encoding UTF8
|
Set-Content -LiteralPath $holderPath -Value $holderLines -Encoding UTF8
|
||||||
Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',$holderPath,$lockFile) -WindowStyle Hidden
|
Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $holderPath, $lockFile) -WindowStyle Hidden
|
||||||
Write-Host "已启动占用者进程(持句柄 90 秒):$lockFile"
|
Write-Host "已启动占用者进程(持句柄 90 秒):$lockFile"
|
||||||
|
|
||||||
# --- 8. 「源不存在」条目对应的目录:故意不建 ---------------------------------
|
# --- 8. 「源不存在」条目对应的目录:故意不建 ---------------------------------
|
||||||
@@ -121,6 +123,6 @@ Write-Host ''
|
|||||||
Write-Host '--- 沙盒源清单 ---'
|
Write-Host '--- 沙盒源清单 ---'
|
||||||
Get-ChildItem -LiteralPath $Root -Force | ForEach-Object {
|
Get-ChildItem -LiteralPath $Root -Force | ForEach-Object {
|
||||||
$files = @(Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue)
|
$files = @(Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue)
|
||||||
$mb = [math]::Round((($files | Measure-Object Length -Sum).Sum) / 1MB, 2)
|
$mb = [math]::Round((($files | Measure-Object Length -Sum).Sum) / 1MB, 2)
|
||||||
" {0,-24} {1,4} 个文件 {2,8} MB 连接点={3}" -f $_.Name, $files.Count, $mb, [bool]($_.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
" {0,-24} {1,4} 个文件 {2,8} MB 连接点={3}" -f $_.Name, $files.Count, $mb, [bool]($_.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
#>
|
#>
|
||||||
|
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
$lab = 'C:\BakNRet-Lab'
|
$lab = 'C:\BakNRet-Lab'
|
||||||
$logDir = Join-Path $lab 'logs'
|
$logDir = Join-Path $lab 'logs'
|
||||||
$stateDir = Join-Path $lab 'state'
|
$stateDir = Join-Path $lab 'state'
|
||||||
New-Item -ItemType Directory -Force -Path $logDir, $stateDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $logDir, $stateDir | Out-Null
|
||||||
|
|
||||||
@@ -40,24 +40,24 @@ try {
|
|||||||
Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy Bypass -Force
|
Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy Bypass -Force
|
||||||
|
|
||||||
Step '3/7 工具上机器 PATH:7-Zip 与 PowerShell 7'
|
Step '3/7 工具上机器 PATH:7-Zip 与 PowerShell 7'
|
||||||
$zipSrc = Join-Path $lab 'payload\7zip'
|
$zipSrc = Join-Path $lab 'payload\7zip'
|
||||||
$zipDst = 'C:\Program Files\7-Zip'
|
$zipDst = 'C:\Program Files\7-Zip'
|
||||||
$pwshSrc = Join-Path $lab 'payload\pwsh'
|
$pwshSrc = Join-Path $lab 'payload\pwsh'
|
||||||
$pwshDst = 'C:\Program Files\PowerShell\7'
|
$pwshDst = 'C:\Program Files\PowerShell\7'
|
||||||
if (-not (Test-Path $zipDst)) { robocopy $zipSrc $zipDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
if (-not (Test-Path $zipDst)) { robocopy $zipSrc $zipDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
||||||
if (-not (Test-Path $pwshDst)) { robocopy $pwshSrc $pwshDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
if (-not (Test-Path $pwshDst)) { robocopy $pwshSrc $pwshDst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
||||||
|
|
||||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
foreach ($p in @($zipDst, $pwshDst)) {
|
foreach ($p in @($zipDst, $pwshDst)) {
|
||||||
if ($machinePath -notlike "*$p*") { $machinePath = $machinePath.TrimEnd(';') + ';' + $p }
|
if ($machinePath -notlike "*$p*") { $machinePath = $machinePath.TrimEnd(';') + ';' + $p }
|
||||||
if ($env:Path -notlike "*$p*") { $env:Path = $env:Path.TrimEnd(';') + ';' + $p }
|
if ($env:Path -notlike "*$p*") { $env:Path = $env:Path.TrimEnd(';') + ';' + $p }
|
||||||
}
|
}
|
||||||
[Environment]::SetEnvironmentVariable('Path', $machinePath, 'Machine')
|
[Environment]::SetEnvironmentVariable('Path', $machinePath, 'Machine')
|
||||||
|
|
||||||
Step '4/7 安装 Pester 5.9.1(Windows PowerShell 与 pwsh 各一份)'
|
Step '4/7 安装 Pester 5.9.1(Windows PowerShell 与 pwsh 各一份)'
|
||||||
$pesterSrc = Join-Path $lab 'payload\Pester\5.9.1'
|
$pesterSrc = Join-Path $lab 'payload\Pester\5.9.1'
|
||||||
foreach ($dst in @("$env:ProgramFiles\WindowsPowerShell\Modules\Pester\5.9.1",
|
foreach ($dst in @("$env:ProgramFiles\WindowsPowerShell\Modules\Pester\5.9.1",
|
||||||
"$env:ProgramFiles\PowerShell\Modules\Pester\5.9.1")) {
|
"$env:ProgramFiles\PowerShell\Modules\Pester\5.9.1")) {
|
||||||
if (-not (Test-Path $dst)) { robocopy $pesterSrc $dst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
if (-not (Test-Path $dst)) { robocopy $pesterSrc $dst /MIR /NFL /NDL /NJH /NJS /NP | Out-Null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ try {
|
|||||||
New-ItemProperty -Path $scoobe -Name 'ScoobeSystemSettingEnabled' -Value 0 -PropertyType DWord -Force | Out-Null
|
New-ItemProperty -Path $scoobe -Name 'ScoobeSystemSettingEnabled' -Value 0 -PropertyType DWord -Force | Out-Null
|
||||||
|
|
||||||
Step '7/7 采集真机事实并落盘'
|
Step '7/7 采集真机事实并落盘'
|
||||||
$zipExe = Join-Path $zipDst '7z.exe'
|
$zipExe = Join-Path $zipDst '7z.exe'
|
||||||
$pwshExe = Join-Path $pwshDst 'pwsh.exe'
|
$pwshExe = Join-Path $pwshDst 'pwsh.exe'
|
||||||
$pwshVer = '缺失'
|
$pwshVer = '缺失'
|
||||||
if (Test-Path $pwshExe) { $pwshVer = (& $pwshExe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>&1) -join ' ' }
|
if (Test-Path $pwshExe) { $pwshVer = (& $pwshExe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>&1) -join ' ' }
|
||||||
@@ -100,8 +100,8 @@ try {
|
|||||||
CpuCount = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
|
CpuCount = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
|
||||||
RamGB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1)
|
RamGB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1)
|
||||||
Drives = @(Get-Volume | Where-Object DriveLetter | ForEach-Object {
|
Drives = @(Get-Volume | Where-Object DriveLetter | ForEach-Object {
|
||||||
[ordered]@{ Letter = "$($_.DriveLetter):"; Fs = $_.FileSystemType; SizeGB = [math]::Round($_.Size / 1GB, 1) }
|
[ordered]@{ Letter = "$($_.DriveLetter):"; Fs = $_.FileSystemType; SizeGB = [math]::Round($_.Size / 1GB, 1) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
$facts | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $stateDir 'provisioned.json') -Encoding UTF8
|
$facts | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $stateDir 'provisioned.json') -Encoding UTF8
|
||||||
$facts.GetEnumerator() | ForEach-Object { Write-Host (" {0,-16} {1}" -f $_.Key, $_.Value) }
|
$facts.GetEnumerator() | ForEach-Object { Write-Host (" {0,-16} {1}" -f $_.Key, $_.Value) }
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ function Invoke-NativeTolerant {
|
|||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
try {
|
try {
|
||||||
return @(& $FilePath @ArgumentList 2>&1)
|
return @(& $FilePath @ArgumentList 2>&1)
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
$ErrorActionPreference = $previous
|
$ErrorActionPreference = $previous
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,8 @@ function Test-Scenario {
|
|||||||
if ($Ok) {
|
if ($Ok) {
|
||||||
$script:Passed++
|
$script:Passed++
|
||||||
Write-Host (" [PASS] {0}" -f $Name) -ForegroundColor Green
|
Write-Host (" [PASS] {0}" -f $Name) -ForegroundColor Green
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$script:Failures += $Name
|
$script:Failures += $Name
|
||||||
Write-Host (" [FAIL] {0}{1}" -f $Name, $(if ($Detail) { ' —— ' + $Detail } else { '' })) -ForegroundColor Red
|
Write-Host (" [FAIL] {0}{1}" -f $Name, $(if ($Detail) { ' —— ' + $Detail } else { '' })) -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -94,8 +96,8 @@ function Get-SecurityFingerprint {
|
|||||||
$acl = Get-Acl -LiteralPath $Path
|
$acl = Get-Acl -LiteralPath $Path
|
||||||
$sid = [System.Security.Principal.SecurityIdentifier]
|
$sid = [System.Security.Principal.SecurityIdentifier]
|
||||||
$aces = @($acl.GetAccessRules($true, $true, $sid) |
|
$aces = @($acl.GetAccessRules($true, $true, $sid) |
|
||||||
ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } |
|
ForEach-Object { '{0}|{1}|{2}' -f $_.AccessControlType, $_.IdentityReference.Value, [int]$_.FileSystemRights } |
|
||||||
Sort-Object)
|
Sort-Object)
|
||||||
return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' '))
|
return ('O={0} G={1} P={2} [{3}]' -f $acl.GetOwner($sid).Value, $acl.GetGroup($sid).Value, $acl.AreAccessRulesProtected, ($aces -join ' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +158,8 @@ function Stop-VscodeProcesses {
|
|||||||
if ($path -and $path.StartsWith($AppRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
if ($path -and $path.StartsWith($AppRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
} catch { }
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Start-Sleep -Milliseconds 700
|
Start-Sleep -Milliseconds 700
|
||||||
@@ -184,7 +187,7 @@ function Remove-TreeHard {
|
|||||||
if (-not (Test-Path -LiteralPath $Path)) { return }
|
if (-not (Test-Path -LiteralPath $Path)) { return }
|
||||||
|
|
||||||
$links = @(Get-ChildItem -LiteralPath $Path -Recurse -Force -Directory -ErrorAction SilentlyContinue |
|
$links = @(Get-ChildItem -LiteralPath $Path -Recurse -Force -Directory -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
|
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
|
||||||
foreach ($link in $links) {
|
foreach ($link in $links) {
|
||||||
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir "{0}"' -f $link.FullName))
|
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir "{0}"' -f $link.FullName))
|
||||||
Remove-BaknretJunction -Path $link.FullName
|
Remove-BaknretJunction -Path $link.FullName
|
||||||
@@ -207,7 +210,8 @@ function Remove-TreeHard {
|
|||||||
|
|
||||||
if (Test-Path -LiteralPath $WorkRoot) {
|
if (Test-Path -LiteralPath $WorkRoot) {
|
||||||
Get-ChildItem -LiteralPath $WorkRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { Remove-TreeHard -Path $_.FullName }
|
Get-ChildItem -LiteralPath $WorkRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { Remove-TreeHard -Path $_.FullName }
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
|
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
|
||||||
}
|
}
|
||||||
$BackupDir = Join-Path $WorkRoot 'backups'
|
$BackupDir = Join-Path $WorkRoot 'backups'
|
||||||
@@ -242,10 +246,12 @@ if (-not $SkipScoop) {
|
|||||||
try {
|
try {
|
||||||
Invoke-Expression "& {$(Invoke-RestMethod -Uri 'https://get.scoop.sh')} -RunAsAdmin"
|
Invoke-Expression "& {$(Invoke-RestMethod -Uri 'https://get.scoop.sh')} -RunAsAdmin"
|
||||||
Write-Host ('[A] 安装器退出码:{0}' -f $LASTEXITCODE)
|
Write-Host ('[A] 安装器退出码:{0}' -f $LASTEXITCODE)
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Test-Scenario 'A: 安装 scoop' $false $_.Exception.Message
|
Test-Scenario 'A: 安装 scoop' $false $_.Exception.Message
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host '[A] scoop 已存在,跳过安装'
|
Write-Host '[A] scoop 已存在,跳过安装'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +305,11 @@ $vscodeReady = [bool]$codeCmd
|
|||||||
|
|
||||||
if ($vscodeReady) {
|
if ($vscodeReady) {
|
||||||
Test-Scenario 'A: vscode 已安装(找到 CLI)' $true $codeCmd
|
Test-Scenario 'A: vscode 已安装(找到 CLI)' $true $codeCmd
|
||||||
} elseif ($SkipScoop) {
|
}
|
||||||
|
elseif ($SkipScoop) {
|
||||||
Write-Host (' [SKIP] 按 -SkipScoop 跳过 vscode({0} 不存在)' -f $vscodeCli) -ForegroundColor Yellow
|
Write-Host (' [SKIP] 按 -SkipScoop 跳过 vscode({0} 不存在)' -f $vscodeCli) -ForegroundColor Yellow
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Test-Scenario 'A: vscode 已安装(找到 CLI)' $false $vscodeCli
|
Test-Scenario 'A: vscode 已安装(找到 CLI)' $false $vscodeCli
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +365,7 @@ $sourceOwner = (Get-Acl -LiteralPath $bData).GetOwner([System.Security.Principal
|
|||||||
Write-Host ('[B] 备份前 data 的属主:{0}' -f (Get-Acl -LiteralPath $bData).Owner)
|
Write-Host ('[B] 备份前 data 的属主:{0}' -f (Get-Acl -LiteralPath $bData).Owner)
|
||||||
$currentSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value
|
$currentSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value
|
||||||
Test-Scenario 'B: 现场造对了 —— 属主既不是当前账户、也不是新建对象的默认属主' `
|
Test-Scenario 'B: 现场造对了 —— 属主既不是当前账户、也不是新建对象的默认属主' `
|
||||||
(($sourceOwner -ne $currentSid) -and ($sourceOwner -ne $creatorOwner)) `
|
(($sourceOwner -ne $currentSid) -and ($sourceOwner -ne $creatorOwner)) `
|
||||||
"owner=$sourceOwner current=$currentSid creatorDefault=$creatorOwner"
|
"owner=$sourceOwner current=$currentSid creatorDefault=$creatorOwner"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -398,7 +406,7 @@ $backup = Invoke-BaknretChild -Script (Join-Path $RepoPath 'Backup.ps1') -Parame
|
|||||||
$backup.LastLog | ForEach-Object { ' ' + $_ }
|
$backup.LastLog | ForEach-Object { ' ' + $_ }
|
||||||
Test-Scenario '备份退出码 0' ($backup.ExitCode -eq 0) ('exit=' + $backup.ExitCode)
|
Test-Scenario '备份退出码 0' ($backup.ExitCode -eq 0) ('exit=' + $backup.ExitCode)
|
||||||
Test-Scenario '每个条目都写了 .acl.json' (@(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count -ge $entries.Count) `
|
Test-Scenario '每个条目都写了 .acl.json' (@(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count -ge $entries.Count) `
|
||||||
('acl.json=' + @(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count + ' 条目=' + $entries.Count)
|
('acl.json=' + @(Get-ChildItem -LiteralPath $BackupDir -Filter '*.acl.json').Count + ' 条目=' + $entries.Count)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 删源 → 恢复
|
# 删源 → 恢复
|
||||||
@@ -445,7 +453,8 @@ if ($vscodeReady) {
|
|||||||
[System.IO.File]::WriteAllText($probeFile, 'write probe')
|
[System.IO.File]::WriteAllText($probeFile, 'write probe')
|
||||||
$writeOk = (Test-Path -LiteralPath $probeFile)
|
$writeOk = (Test-Path -LiteralPath $probeFile)
|
||||||
Remove-Item -LiteralPath $probeFile -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $probeFile -Force -ErrorAction SilentlyContinue
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
$detail = $_.Exception.Message
|
$detail = $_.Exception.Message
|
||||||
}
|
}
|
||||||
Test-Scenario 'A: vscode 的数据目录可写(新建文件成功)' $writeOk $detail
|
Test-Scenario 'A: vscode 的数据目录可写(新建文件成功)' $writeOk $detail
|
||||||
@@ -455,9 +464,10 @@ if ($vscodeReady) {
|
|||||||
$expectedNormalized = $expected[$pair[1]] -replace ' P=(True|False) ', ' P='
|
$expectedNormalized = $expected[$pair[1]] -replace ' P=(True|False) ', ' P='
|
||||||
$actualNormalized = (Get-SecurityFingerprint -Path $pair[0]) -replace ' P=(True|False) ', ' P='
|
$actualNormalized = (Get-SecurityFingerprint -Path $pair[0]) -replace ' P=(True|False) ', ' P='
|
||||||
Test-Scenario ("A: {0} 的安全指纹与备份前一致" -f $pair[1]) ($actualNormalized -eq $expectedNormalized) `
|
Test-Scenario ("A: {0} 的安全指纹与备份前一致" -f $pair[1]) ($actualNormalized -eq $expectedNormalized) `
|
||||||
("want: " + $expectedNormalized + " / got: " + $actualNormalized)
|
("want: " + $expectedNormalized + " / got: " + $actualNormalized)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host ' [SKIP] vscode 没装上,A 段的功能断言跳过(-SkipScoop 或下载失败)' -ForegroundColor Yellow
|
Write-Host ' [SKIP] vscode 没装上,A 段的功能断言跳过(-SkipScoop 或下载失败)' -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +498,7 @@ $negative = Join-Path $WorkRoot 'negative-data'
|
|||||||
& robocopy.exe $bData $negative /E /COPY:DAT /NFL /NDL /NJH /NJS /NP | Out-Null
|
& robocopy.exe $bData $negative /E /COPY:DAT /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||||
$negativeOwner = (Get-Acl -LiteralPath $negative).GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
$negativeOwner = (Get-Acl -LiteralPath $negative).GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
Test-Scenario 'B 负对照: 只搬文件时,属主变成"跑脚本的账户"而不再是原账户' `
|
Test-Scenario 'B 负对照: 只搬文件时,属主变成"跑脚本的账户"而不再是原账户' `
|
||||||
(($negativeOwner -ne $sourceOwner) -and ($negativeOwner -eq $creatorOwner)) `
|
(($negativeOwner -ne $sourceOwner) -and ($negativeOwner -eq $creatorOwner)) `
|
||||||
"negative=$negativeOwner creatorDefault=$creatorOwner"
|
"negative=$negativeOwner creatorDefault=$creatorOwner"
|
||||||
Write-Host (' 原属主 = {0}' -f $sourceOwner)
|
Write-Host (' 原属主 = {0}' -f $sourceOwner)
|
||||||
Write-Host (' 恢复后属主 = {0}(应与原属主相同)' -f $restoredOwner)
|
Write-Host (' 恢复后属主 = {0}(应与原属主相同)' -f $restoredOwner)
|
||||||
@@ -501,14 +511,16 @@ Write-Host ''
|
|||||||
$total = $script:Passed + $script:Failures.Count
|
$total = $script:Passed + $script:Failures.Count
|
||||||
if ($script:Failures.Count -eq 0) {
|
if ($script:Failures.Count -eq 0) {
|
||||||
Write-Host ('ACL 演练:全部通过 {0} 项' -f $total) -ForegroundColor Green
|
Write-Host ('ACL 演练:全部通过 {0} 项' -f $total) -ForegroundColor Green
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host ('ACL 演练:通过 {0} 项,失败 {1} 项' -f $script:Passed, $script:Failures.Count) -ForegroundColor Red
|
Write-Host ('ACL 演练:通过 {0} 项,失败 {1} 项' -f $script:Passed, $script:Failures.Count) -ForegroundColor Red
|
||||||
foreach ($failure in $script:Failures) { Write-Host (' - ' + $failure) -ForegroundColor Red }
|
foreach ($failure in $script:Failures) { Write-Host (' - ' + $failure) -ForegroundColor Red }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($KeepWorkRoot) {
|
if ($KeepWorkRoot) {
|
||||||
Write-Host ('临时目录保留:{0}' -f $WorkRoot) -ForegroundColor Yellow
|
Write-Host ('临时目录保留:{0}' -f $WorkRoot) -ForegroundColor Yellow
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Remove-TreeHard -Path $bRoot
|
Remove-TreeHard -Path $bRoot
|
||||||
Remove-TreeHard -Path (Join-Path $WorkRoot 'negative-data')
|
Remove-TreeHard -Path (Join-Path $WorkRoot 'negative-data')
|
||||||
# 备份与日志留着,便于事后核对(归档可能有几百 MB,要腾空间就手工删这个目录)
|
# 备份与日志留着,便于事后核对(归档可能有几百 MB,要腾空间就手工删这个目录)
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ $drillParams = [ordered]@{
|
|||||||
ConfigPath = $ConfigPath
|
ConfigPath = $ConfigPath
|
||||||
}
|
}
|
||||||
if ($entries.Count -gt 0) { $drillParams['Entries'] = $entries }
|
if ($entries.Count -gt 0) { $drillParams['Entries'] = $entries }
|
||||||
if ($KeepWorkRoot) { $drillParams['KeepWorkRoot'] = $true }
|
if ($KeepWorkRoot) { $drillParams['KeepWorkRoot'] = $true }
|
||||||
if ($AllowChanged) { $drillParams['AllowChanged'] = $true }
|
if ($AllowChanged) { $drillParams['AllowChanged'] = $true }
|
||||||
|
|
||||||
Write-Host ("[lab] 恢复演练:{0} 个条目 -> {1}" -f $entries.Count, ($entries -join ' | '))
|
Write-Host ("[lab] 恢复演练:{0} 个条目 -> {1}" -f $entries.Count, ($entries -join ' | '))
|
||||||
& 'C:\BakNRet\tests\Restore-Drill.ps1' @drillParams
|
& 'C:\BakNRet\tests\Restore-Drill.ps1' @drillParams
|
||||||
@@ -29,8 +29,8 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
Write-Host ("[lab] 包装器:PS {0},[Console]::OutputEncoding = {1}" -f $PSVersionTable.PSVersion, [Console]::OutputEncoding.WebName)
|
Write-Host ("[lab] 包装器:PS {0},[Console]::OutputEncoding = {1}" -f $PSVersionTable.PSVersion, [Console]::OutputEncoding.WebName)
|
||||||
Write-Host ("[lab] 套件:{0} {1}" -f $Suite, ($SuiteArgs -join ' '))
|
Write-Host ("[lab] 套件:{0} {1}" -f $Suite, ($SuiteArgs -join ' '))
|
||||||
|
|||||||
@@ -21,5 +21,5 @@
|
|||||||
ToolOutput = 'quiet'
|
ToolOutput = 'quiet'
|
||||||
Snapshot = @{ Enabled = $false; KeepCount = 3; KeepDays = 30 }
|
Snapshot = @{ Enabled = $false; KeepCount = 3; KeepDays = 30 }
|
||||||
Encryption = @{ Enabled = $false; PasswordFile = 'C:\BakNRet-Lab\state\baknret.key'; EncryptHeaders = $true }
|
Encryption = @{ Enabled = $false; PasswordFile = 'C:\BakNRet-Lab\state\baknret.key'; EncryptHeaders = $true }
|
||||||
DefaultExcludes = @('!Thumbs.db','!desktop.ini')
|
DefaultExcludes = @('!Thumbs.db', '!desktop.ini')
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,18 @@
|
|||||||
带连字符的键必须加引号(PSD1 会把它当减法);这里用中文键名,也统一加引号。
|
带连字符的键必须加引号(PSD1 会把它当减法);这里用中文键名,也统一加引号。
|
||||||
#>
|
#>
|
||||||
@{
|
@{
|
||||||
AppMultiSlot = @{
|
AppMultiSlot = @{
|
||||||
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Data'; Description = '主数据(含 node_modules、session.log、空目录)' }
|
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Data'; Description = '主数据(含 node_modules、session.log、空目录)' }
|
||||||
DefaultConfig = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Config'; Description = '配置(含 .bak)' }
|
DefaultConfig = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Config'; Description = '配置(含 .bak)' }
|
||||||
CacheSlot = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Cache'; Description = '缓存(清单里再排除一条 cache-01.tmp)' }
|
CacheSlot = @{ Path = 'C:\BakNRet-Lab\sources\AppMultiSlot\Cache'; Description = '缓存(清单里再排除一条 cache-01.tmp)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
AppFileSlot = @{
|
AppFileSlot = @{
|
||||||
Profile = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\profile.json'; Description = '单文件 Slot:归档内是名为 Profile 的文件' }
|
Profile = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\profile.json'; Description = '单文件 Slot:归档内是名为 Profile 的文件' }
|
||||||
Readme = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\readme.txt'; Description = '另一个单文件 Slot' }
|
Readme = @{ Path = 'C:\BakNRet-Lab\sources\AppFileSlot\readme.txt'; Description = '另一个单文件 Slot' }
|
||||||
}
|
}
|
||||||
|
|
||||||
'软件目录甲' = @{
|
'软件目录甲' = @{
|
||||||
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\软件 目录.甲'; Description = '中文 + 空格 + 点的路径' }
|
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\软件 目录.甲'; Description = '中文 + 空格 + 点的路径' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\JunctionToData'; Description = '真 NTFS 连接点(指向 AppMultiSlot\Data)' }
|
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\JunctionToData'; Description = '真 NTFS 连接点(指向 AppMultiSlot\Data)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
MissingApp = @{
|
MissingApp = @{
|
||||||
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\MissingApp'; Description = '源不存在:备份跳过并记 missing-source,恢复仍知道目标位置' }
|
DefaultData = @{ Path = 'C:\BakNRet-Lab\sources\MissingApp'; Description = '源不存在:备份跳过并记 missing-source,恢复仍知道目标位置' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user