P0-P3 全量重构:退出码 / 解析修复、manifest 与 7z t 校验、干跑、排除规则、日志、测试与计划任务
P0 正确性 - 退出码:改用 .NET Process 直接启动、让子进程继承控制台,不再用 Start-Process -PassThru (在 7.7.0-preview.4 上 ExitCode 恒为 $null,会把成功的压缩判成失败); 7z / RAR / tar 三条解压分支统一走同一个取退出码的封装。 - BackupList 解析:先按第一个 :: 切段再处理引号(整行被一对引号包住的写法不再把排除表 吞进路径);排除表同时接受 , 与 ;(旧实现只认 ;,导致排除从未生效);支持 :- / :+ / @flag。 - 补回 .ssh 与孤儿归档:.ssh 进清单;孤儿归档在备份端也做审计并点名; 带 -Only / -Skip 时不再把未选中的归档误报成孤儿。 - Resolve-BackupEntry 里 $rootName 在赋值前被引用(会读到外层作用域残留值),已提前赋值。 P1 归档可靠性 - 每个条目写进 manifest.json:源、归档、时间、退出码、校验结果、失败原因, 并区分 warnings(在位归档)与 attemptWarnings(本次尝试)。 - 归档后做 7z t 内容校验,先写 .tmp、校验通过再原子替换(File.Move overwrite)。 - manifest.roots 记录归档内**真实**的顶层条目名(原先记的是软件名,Edge 实际是 "User Data")。 P2 可用性 - Restore 支持 -WhatIf / -DryRun / -VerifyOnly / -Only / -Skip; 这三种"只看不写"的模式一个字节都不写(原先会写回 manifest.json)。 - Edge 等高缓存条目加排除规则并实测:1781 MB / 27961 项 -> 72 MB / 2294 项; 书签、密码、Cookies、偏好、历史、IndexedDB、Local Storage 全部保留。 普通模式是相对归档根目录锚定的,嵌套的那些(如 OneAuth\WebView2 里的 Crashpad) 改用 ! 组件形式才会命中。 - 日志落盘 logs/<backup|restore>-<时间戳>.log;退出码按失败数返回。 - tools/Register-BackupTask.ps1 注册每日计划任务;tools/Rename-Archives.ps1 迁移旧归档名。 - root= 标记此前静默失效,现在明确告警(该功能尚未实现)。 P3 测试与验证 - tests/BakNRet.Tests.ps1:Pester 5 套件 62 项(含用子进程跑 Backup.ps1 / Restore.ps1 的端到端与针对上述缺陷的回归)。 - tests/Run-Pester.ps1 + tools/Install-TestDependencies.ps1:把 Pester 装到仓库内 .tools/, 不动机器上的全局模块(系统自带的 3.4.0 缺 Should -Be)。 - tests/Restore-Drill.ps1:真实归档恢复演练,明确区分"源在备份后变过"与"归档/解压有问题"。 - tests/Run-Tests.ps1(49 项,零依赖)与 tests/Run-E2E.ps1(23 项)继续可用;三套共 134 项全通过。 真实机器验证 - 生产归档 22/22 通过 7z t;-VerifyOnly 不再改动 manifest.json(SHA256 前后一致)。 - 真实恢复演练 12/12 通过,27,670 个文件与活源逐字节一致。 - 修复了生产 scoop-persist.7z:原先只有 90 字节(空归档)而源有 1.3 GB, 重打包后 233 MB,恢复演练 26981/26981 全部一致。
This commit is contained in:
1 parent
045d51ac9c
commit
e114cae8c8
13 files changed
+1761
-142
No files matched your search
+235
-57
@@ -392,52 +392,89 @@ function ConvertFrom-BackupListLine {
|
||||
return $null
|
||||
}
|
||||
|
||||
# `:` 在 Windows 路径里只可能是盘符,`::` 不可能出现在真实路径中,
|
||||
# 因此可以安全地按第一个 `::` 切分,不受引号位置影响。
|
||||
$separatorIndex = $content.IndexOf('::')
|
||||
if ($separatorIndex -ge 0) {
|
||||
$pathPart = $content.Substring(0, $separatorIndex)
|
||||
$tailPart = $content.Substring($separatorIndex + 2)
|
||||
} else {
|
||||
$pathPart = $content
|
||||
$tailPart = ''
|
||||
# 行内记号(都用到 `:`,因为 `:` 在 Windows 路径里只可能是盘符,
|
||||
# 而 `::` `:+` `:-` 都不可能出现在真实路径里,所以切分不受引号位置影响):
|
||||
# :: 排除模式(历史写法,等价于 :-)
|
||||
# :+ 追加一个目录(等价于名录里的 Dirs 数组)
|
||||
# :- 排除模式
|
||||
# 记号可以出现多次、顺序任意:`Foo :+ D:\a D:\b :- logs\ !*Cache`
|
||||
# 第一段(第一个记号之前)是主路径/软件名。
|
||||
$segments = [System.Collections.Generic.List[object]]::new()
|
||||
$cursor = 0
|
||||
$currentKind = 'main'
|
||||
$currentText = ''
|
||||
$contentLength = $content.Length
|
||||
|
||||
while ($cursor -lt $contentLength) {
|
||||
$colonIndex = $content.IndexOf(':', $cursor)
|
||||
if ($colonIndex -lt 0) {
|
||||
$currentText += $content.Substring($cursor)
|
||||
break
|
||||
}
|
||||
|
||||
$currentText += $content.Substring($cursor, $colonIndex - $cursor)
|
||||
|
||||
# 记号必须是 `:` 后紧跟 `:` `+` `-` 之一
|
||||
if ($colonIndex + 1 -lt $contentLength -and $content[$colonIndex + 1] -in @(':', '+', '-')) {
|
||||
$kind = switch ($content[$colonIndex + 1]) {
|
||||
'+' { 'add' }
|
||||
'-' { 'exclude' }
|
||||
default { 'exclude' } # `::` 等同排除
|
||||
}
|
||||
$segments.Add([pscustomobject]@{ Kind = $currentKind; Text = $currentText.Trim() })
|
||||
$currentKind = $kind
|
||||
$currentText = ''
|
||||
$cursor = $colonIndex + 2
|
||||
} else {
|
||||
# 单个 `:`(盘符)属于内容
|
||||
$currentText += ':'
|
||||
$cursor = $colonIndex + 1
|
||||
}
|
||||
}
|
||||
$segments.Add([pscustomobject]@{ Kind = $currentKind; Text = $currentText.Trim() })
|
||||
|
||||
$pathPart = ''
|
||||
$addedPaths = @()
|
||||
$excludes = @()
|
||||
$flags = @()
|
||||
|
||||
# 标记(@encrypt 等)可能挂在任意段的末尾,所以每一段都先摘标记:
|
||||
# 历史写法 `C:\x :: a,b @encrypt` 里标记就是跟在排除表后面的。
|
||||
foreach ($segment in $segments) {
|
||||
if ([string]::IsNullOrWhiteSpace($segment.Text)) { continue }
|
||||
|
||||
$split = Split-TrailingFlags -Text $segment.Text
|
||||
if ($split.Flags.Count -gt 0) { $flags = @($flags) + @($split.Flags) }
|
||||
$body = $split.Remainder
|
||||
if ([string]::IsNullOrWhiteSpace($body)) { continue }
|
||||
|
||||
switch ($segment.Kind) {
|
||||
'main' { $pathPart = $body }
|
||||
'add' { $addedPaths += $body }
|
||||
'exclude' {
|
||||
$excludes += @($body -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 引号只应包住路径。整行被一对引号包住时(历史写法),
|
||||
# 上面的切分已经把排除表摘出去了,此时路径这半只剩开引号、
|
||||
# 闭引号留在了 tail 末尾,因此两侧各剥一次,不要求成对。
|
||||
# 切分后主路径这半只剩开引号、闭引号留在了尾段,因此两侧各剥一次,不要求成对。
|
||||
$pathPart = $pathPart.Trim()
|
||||
if ($pathPart.StartsWith('"')) { $pathPart = $pathPart.Substring(1) }
|
||||
if ($pathPart.EndsWith('"')) { $pathPart = $pathPart.Substring(0, $pathPart.Length - 1) }
|
||||
$pathPart = $pathPart.Trim()
|
||||
if ([string]::IsNullOrEmpty($pathPart)) { return $null }
|
||||
|
||||
$tailPart = $tailPart.Trim()
|
||||
if ($tailPart.EndsWith('"')) { $tailPart = $tailPart.Substring(0, $tailPart.Length - 1).Trim() }
|
||||
# 尾段可能残留闭引号(历史 `"路径 :: 排除表"` 写法)
|
||||
$excludes = @($excludes | ForEach-Object { $_.TrimEnd('"').Trim() } | Where-Object { $_ })
|
||||
|
||||
# 从尾部摘出 @标记。没有 `::` 时标记直接跟在路径后面
|
||||
# (如 `%UserProfile%\.ssh @encrypt`),因此 tail 为空时还要从路径那半再摘一次。
|
||||
# 标记可能出现在任一侧,所以两边都要摘。
|
||||
$tailSplit = Split-TrailingFlags -Text $tailPart
|
||||
$flags = @($tailSplit.Flags)
|
||||
$tailPart = $tailSplit.Remainder
|
||||
|
||||
$pathSplit = Split-TrailingFlags -Text $pathPart
|
||||
if ($pathSplit.Flags.Count -gt 0) {
|
||||
$flags = @($pathSplit.Flags) + $flags
|
||||
$pathPart = $pathSplit.Remainder
|
||||
}
|
||||
|
||||
$excludes = @()
|
||||
if ($tailPart) {
|
||||
$excludes = @($tailPart -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Path = $pathPart
|
||||
# 目录名或文件名,需要靠 SoftwareCatalog 换成真实路径;
|
||||
# 带分隔符或 %变量% 的写法按字面路径处理(并给出警告)。
|
||||
IsName = (-not (Test-LiteralPath -Path $pathPart))
|
||||
AddedPaths = $addedPaths
|
||||
ExcludePatterns = $excludes
|
||||
Flags = $flags
|
||||
Raw = $Line
|
||||
@@ -620,29 +657,60 @@ function Get-SoftwareCatalog {
|
||||
$entry = if ($data -is [System.Collections.IDictionary]) { $data[$key] } else { $data.$key }
|
||||
|
||||
$rawPath = $null
|
||||
$variants = $null
|
||||
$dirList = @()
|
||||
if ($entry -is [System.Collections.IDictionary]) {
|
||||
# Dirs = 一个软件包含的多个目录(推荐写法)
|
||||
# Variants = 同名目录出现在多个位置(旧写法,等价于 Dirs,保留兼容)
|
||||
if ($entry.Contains('Dirs')) { $dirList = @($entry['Dirs']) }
|
||||
elseif ($entry.Contains('Variants')) { $dirList = @($entry['Variants']) }
|
||||
if ($entry.Contains('Path')) { $rawPath = [string]$entry['Path'] }
|
||||
if ($entry.Contains('Variants')) { $variants = @($entry['Variants']) }
|
||||
} else {
|
||||
$rawPath = [string]$entry
|
||||
}
|
||||
if (-not $rawPath) { continue }
|
||||
if (-not $rawPath -and $dirList.Count -eq 0) { continue }
|
||||
|
||||
$resolved = [Environment]::ExpandEnvironmentVariables($rawPath)
|
||||
$resolved = if ($rawPath) { [Environment]::ExpandEnvironmentVariables($rawPath) } else { $null }
|
||||
|
||||
# 多目录:逐个解析(每个都可以用前缀补全),任一解析不出来就整体 Unresolved
|
||||
if ($dirList.Count -gt 0) {
|
||||
$paths = @()
|
||||
$unresolved = @()
|
||||
foreach ($item in $dirList) {
|
||||
$candidate = [Environment]::ExpandEnvironmentVariables([string]$item)
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
$paths += $candidate
|
||||
continue
|
||||
}
|
||||
$parent = Split-Path -Path $candidate -Parent
|
||||
$leaf = Split-Path -Path $candidate -Leaf
|
||||
$found = $null
|
||||
if ($parent -and $leaf -and (Test-Path -LiteralPath $parent)) {
|
||||
$found = @(Find-ChildDirectoryByName -Parent $parent -Name $leaf -MaxDepth $MaxDepth)
|
||||
}
|
||||
if ($found -and $found.Count -gt 0) {
|
||||
$paths += $found[0]
|
||||
Write-Log "名录:$name 的 $candidate -> $($found[0])(按前缀补全)" -Level DEBUG
|
||||
} else {
|
||||
$unresolved += $candidate
|
||||
}
|
||||
}
|
||||
|
||||
$kind = if ($paths.Count -eq 0) { 'Unresolved' } elseif ($unresolved.Count -gt 0) { 'Partial' } else { 'Multi' }
|
||||
if ($unresolved.Count -gt 0) {
|
||||
Write-Log ("名录:{0} 有 {1} 个目录找不到:{2}" -f $name, $unresolved.Count, ($unresolved -join ';')) -Level WARN
|
||||
}
|
||||
|
||||
if ($variants -and $variants.Count -gt 0) {
|
||||
# 同名目录出现在多个位置:不猜,所有位置都作为归档根目录。
|
||||
$resolvedVariants = @($variants | ForEach-Object { [Environment]::ExpandEnvironmentVariables([string]$_) })
|
||||
$result[$name] = [pscustomobject]@{
|
||||
Name = $name
|
||||
Path = $rawPath
|
||||
ResolvedPath = $resolved
|
||||
Variants = $resolvedVariants
|
||||
Kind = 'Variant'
|
||||
ResolvedPath = $(if ($paths.Count -gt 0) { $paths[0] } else { $resolved })
|
||||
Variants = $paths
|
||||
Dirs = $paths
|
||||
Missing = $unresolved
|
||||
Kind = $kind
|
||||
Raw = $entry
|
||||
}
|
||||
Write-Log "名录:$name 有 $($resolvedVariants.Count) 个候选位置" -Level DEBUG
|
||||
Write-Log "名录:$name -> $($paths.Count) 个目录" -Level DEBUG
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -655,7 +723,7 @@ function Get-SoftwareCatalog {
|
||||
$found = $null
|
||||
if ($parent -and (Test-Path -LiteralPath $parent)) {
|
||||
$found = @(Find-ChildDirectoryByName -Parent $parent -Name $leaf -MaxDepth $MaxDepth)
|
||||
if ($found.Count -gt 1) { $kind = 'Variant' } elseif ($found.Count -eq 1) { $kind = 'Single' } else { $kind = 'Unresolved' }
|
||||
if ($found.Count -gt 1) { $kind = 'Multi' } elseif ($found.Count -eq 1) { $kind = 'Single' } else { $kind = 'Unresolved' }
|
||||
} else {
|
||||
$kind = 'Unresolved'
|
||||
}
|
||||
@@ -667,6 +735,8 @@ function Get-SoftwareCatalog {
|
||||
Path = $rawPath
|
||||
ResolvedPath = $resolved
|
||||
Variants = @($found)
|
||||
Dirs = @($found)
|
||||
Missing = @()
|
||||
Kind = $kind
|
||||
Raw = $entry
|
||||
Suffixed = $true
|
||||
@@ -681,6 +751,8 @@ function Get-SoftwareCatalog {
|
||||
Path = $rawPath
|
||||
ResolvedPath = $resolved
|
||||
Variants = @()
|
||||
Dirs = $(if ($kind -eq 'Single') { @($resolved) } else { @() })
|
||||
Missing = @()
|
||||
Kind = $kind
|
||||
Raw = $entry
|
||||
}
|
||||
@@ -689,6 +761,51 @@ function Get-SoftwareCatalog {
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-ArchiveTopLevelNames {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
列出归档内的顶层条目名(用于确认多目录打包时每个目录都真的进去了)。
|
||||
|
||||
.DESCRIPTION
|
||||
**刻意不解析 7z 的输出**:读取子进程 stdout 需要创建管道,本机沙箱会直接拒绝
|
||||
(Access to the path '\\.\pipe\LOCAL\dotnet_...' denied),文件重定向(> file)
|
||||
同样被拒。所以改成"把归档解到临时目录,再看文件系统上有哪些顶层条目",
|
||||
只依赖文件系统。代价是多一次解压(只在多目录条目上跑),
|
||||
好处是这个校验在受限环境里真的会执行,而不是静默退化成空数组。
|
||||
|
||||
解压失败或拿不到 7z 时返回空数组,调用方据此跳过顶层名核对。
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ArchivePath,
|
||||
[Parameter(Mandatory = $true)][string]$SevenZip,
|
||||
[string]$Password
|
||||
)
|
||||
|
||||
$staging = Join-Path $env:TEMP ("bnr-inspect-" + [guid]::NewGuid().ToString('N'))
|
||||
$names = @()
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $staging -Force | Out-Null
|
||||
|
||||
$argument = @('x', '-bso0', '-bsp0', '-y', "-o$staging")
|
||||
if ($Password) { $argument += "-p$Password" }
|
||||
$argument += $ArchivePath
|
||||
|
||||
$exitCode = Invoke-ExternalCommand -FilePath $SevenZip -ArgumentList $argument
|
||||
if ($exitCode -ne 0) { return @() }
|
||||
|
||||
# 先把名字读进变量,再在 finally 里删临时目录;
|
||||
# 直接在 return 里管道 Get-ChildItem 会被 finally 抢在前面,拿到空数组。
|
||||
$names = @(Get-ChildItem -LiteralPath $staging -Force -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty Name)
|
||||
} catch {
|
||||
Write-Log "无法清点归档内容(跳过顶层名核对):$($_.Exception.Message)" -Level DEBUG
|
||||
$names = @()
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
return $names
|
||||
}
|
||||
function Find-ChildDirectoryByName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -793,6 +910,11 @@ function Resolve-BackupEntry {
|
||||
$baseName = Get-ItemArchiveName -Entry $Entry -CatalogPath $CatalogPath -MaxDepth $MaxDepth
|
||||
$rootNames = @($Entry.Flags | Where-Object { $_ -like 'root=*' } | ForEach-Object { $_.Substring(5) })
|
||||
|
||||
# 必须在下面任何一个 return 之前算出来:名录里没有这个名字时也要走
|
||||
# "路径不存在" 分支,那条分支引用 $rootName。原先它写在后面,靠 PowerShell
|
||||
# 的隐式 $null 侥幸不报错,但会把**外层作用域**残留的 $rootName 带进来。
|
||||
$rootName = if ($rootNames.Count -gt 0) { $rootNames[0] } else { $baseName }
|
||||
|
||||
if (-not $isName) {
|
||||
$sourcePath = [Environment]::ExpandEnvironmentVariables($Entry.Path)
|
||||
return [pscustomobject]@{
|
||||
@@ -821,7 +943,6 @@ function Resolve-BackupEntry {
|
||||
}
|
||||
|
||||
$catalogEntry = $catalog[$Entry.Path]
|
||||
$rootName = if ($rootNames.Count -gt 0) { $rootNames[0] } else { $baseName }
|
||||
|
||||
# @pathname 必须排在 Unresolved 分支之前:否则路径不存在的条目会先被
|
||||
# 当作普通软件名条目返回 Flavor='name',把 @pathname 覆盖悄悄吃掉。
|
||||
@@ -882,15 +1003,21 @@ function Resolve-BackupEntry {
|
||||
|
||||
$sources = @()
|
||||
|
||||
if ($catalogEntry.Kind -eq 'Variant') {
|
||||
$parent = Split-Path -Path $catalogEntry.ResolvedPath -Parent
|
||||
$relatives = @($catalogEntry.Variants | ForEach-Object { Split-Path -Path $_ -Leaf })
|
||||
$sources = @([pscustomobject]@{
|
||||
RootName = $rootName
|
||||
ParentDir = $parent
|
||||
RelativePaths = $relatives
|
||||
SourcePath = $parent
|
||||
})
|
||||
if ($catalogEntry.Kind -in @('Multi', 'Partial')) {
|
||||
# 一个软件包含多个目录:每个目录各占归档里的一个根目录层。
|
||||
# 归档内层用**源目录自己的名字**(不用软件名),这样恢复时
|
||||
# 每个目录都能各自找到父目录与末级名,互不干扰。
|
||||
foreach ($dir in $catalogEntry.Dirs) {
|
||||
$dirParent = Split-Path -Path $dir -Parent
|
||||
$dirLeaf = Split-Path -Path $dir -Leaf
|
||||
if (-not $dirParent -or -not $dirLeaf) { continue }
|
||||
$sources += [pscustomobject]@{
|
||||
RootName = $null
|
||||
ParentDir = $dirParent
|
||||
RelativePaths = @($dirLeaf)
|
||||
SourcePath = $dir
|
||||
}
|
||||
}
|
||||
} elseif ($catalogEntry.Kind -eq 'Single') {
|
||||
$sources = @([pscustomobject]@{
|
||||
RootName = $rootName
|
||||
@@ -911,6 +1038,22 @@ function Resolve-BackupEntry {
|
||||
}
|
||||
}
|
||||
|
||||
# 清单里的 `:+ <路径>` 追加项,叠加在名录的目录之后
|
||||
if ($Entry.AddedPaths -and $Entry.AddedPaths.Count -gt 0) {
|
||||
foreach ($added in $Entry.AddedPaths) {
|
||||
$addedPath = [Environment]::ExpandEnvironmentVariables($added)
|
||||
$addedParent = Split-Path -Path $addedPath -Parent
|
||||
$addedLeaf = Split-Path -Path $addedPath -Leaf
|
||||
if (-not $addedParent -or -not $addedLeaf) { continue }
|
||||
$sources += [pscustomobject]@{
|
||||
RootName = $null
|
||||
ParentDir = $addedParent
|
||||
RelativePaths = @($addedLeaf)
|
||||
SourcePath = $addedPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
IsName = $true
|
||||
CatalogEntry = $catalogEntry
|
||||
@@ -1195,14 +1338,26 @@ function Get-BaknretConfig {
|
||||
function Get-BaknretPassword {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
从环境变量 BAKNRET_PASSWORD 或密码文件取加密口令;取不到返回 $null。
|
||||
取加密口令:命令行参数 > 环境变量 > 密码文件 > 交互式询问。
|
||||
|
||||
.DESCRIPTION
|
||||
口令**不写入仓库**。若清单要求加密但取不到口令,调用方必须失败退出,
|
||||
绝不能默默写出明文归档。
|
||||
#>
|
||||
param([string]$PasswordFile)
|
||||
口令**绝不写入仓库**。优先级:
|
||||
1. -Password(命令行传参,注意会短暂出现在进程列表里)
|
||||
2. $env:BAKNRET_PASSWORD
|
||||
3. PasswordFile 的首行(文件必须在仓库之外,脚本只记路径)
|
||||
4. 交互式询问(仅当 allowPrompt 且当前是交互式会话)
|
||||
全都拿不到就返回 $null,调用方必须失败退出,绝不能默默写明文归档。
|
||||
|
||||
交互式询问用的是 Read-Host -AsSecureString,输入不回显;但它需要真实控制台,
|
||||
在计划任务/CI 里会把用户晾在那里等输入,所以只在交互式会话里才提示。
|
||||
#>
|
||||
param(
|
||||
[string]$Password,
|
||||
[string]$PasswordFile,
|
||||
[switch]$AllowPrompt
|
||||
)
|
||||
|
||||
if ($Password) { return $Password }
|
||||
if ($env:BAKNRET_PASSWORD) { return $env:BAKNRET_PASSWORD }
|
||||
|
||||
if ($PasswordFile -and (Test-Path -LiteralPath $PasswordFile)) {
|
||||
@@ -1210,6 +1365,28 @@ function Get-BaknretPassword {
|
||||
if ($line) { return $line.Trim() }
|
||||
}
|
||||
|
||||
if ($AllowPrompt) {
|
||||
# 只有在真的会等人输入时才提示,避免计划任务里静默挂起
|
||||
$interactive = $true
|
||||
try { $interactive = -not [System.Console]::IsInputRedirected } catch { $interactive = $false }
|
||||
|
||||
if ($interactive) {
|
||||
Write-Log '需要加密口令,请在弹出的提示里输入(不会回显、不会落盘)' -Level WARN
|
||||
try {
|
||||
$secure = Read-Host -Prompt '请输入加密口令' -AsSecureString
|
||||
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||
try {
|
||||
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
||||
} finally {
|
||||
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||
}
|
||||
} catch {
|
||||
Write-Log "口令输入失败:$_" -Level ERROR
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -1219,6 +1396,7 @@ Export-ModuleMember -Function @(
|
||||
'ConvertTo-NativeArgumentString', 'Invoke-ExternalCommand', 'Resolve-CompressionTool', 'Get-Optimized7zArgument',
|
||||
'ConvertFrom-BackupListLine', 'Get-ArchiveExcludeArgument', 'Test-LiteralPath',
|
||||
'Resolve-CatalogPath', 'Get-SoftwareCatalog', 'Find-ChildDirectoryByName', 'Format-CatalogName',
|
||||
'Get-ArchiveTopLevelNames',
|
||||
'Get-ItemArchiveName', 'Resolve-BackupEntry', 'Get-BackupBaseName', 'Convert-BackupFileNameToPath',
|
||||
'Get-FolderSummary',
|
||||
'Read-BaknretManifest', 'Write-BaknretManifest', 'Move-BaknretArchiveIntoPlace',
|
||||
|
||||
Reference in new issue
Block a user