16 个没有前缀的公共函数补上 BakNRet(Write-Log → Write-BakNRetLog、Resolve-BackupEntry → Resolve-BakNRetBackupEntry、Find-ChildDirectoryByName → Find-BakNRetChildDirectoryByName 等), 另外把全仓的 Baknret 统一成 BakNRet(47 个文件、940 处、65 个定义文件重命名)。 这不是审美问题:静态分析直接拓出一条实据 —— Write-Log 与本机某个已装模块导出的命令 **重名**(PSAvoidOverwritingBuiltInCmdlets),而重名的后果是导入两个模块时有一方的命令被 静默遮蔽。补前缀正是这条规则的解法,改名后它归零。 为什么敢做这个规模:PowerShell 的函数名解析大小写不敏感,所以 Baknret → BakNRet 在功能 上是零风险;真正要验证的是 16 个补前缀的调用点,而 276 个断言几乎覆盖了每个函数。另外 "名字与文件名一致"这条不变式有断言盯着(加载器点源的文件集合 vs 磁盘)。 踩到并记下的坑:Windows 文件系统大小写不敏感,所以**只改大小写**的重命名会被 Move-Item 当成同一个文件而静默跳过 —— 同一批里同时改了名字的那 16 个文件却成功了,于是"看起来能跑"。 最后用"先移到临时名、再移到目标名"的两步走解决,判断与替换全部改用显式大小写敏感的形式 (-creplace / -cmatch)。 顺带把名录指纹缓存从 MD5 换成 SHA256(PSAvoidUsingBrokenHashAlgorithms):它只是缓存键, 没有兼容负担。 验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、276 个断言全过、 构建工具仍能合回单文件(3300 行)。
74 lines
3.4 KiB
PowerShell
74 lines
3.4 KiB
PowerShell
function New-BakNRetArchiveStaging {
|
|
<#
|
|
.SYNOPSIS
|
|
建一个暂存目录,把每个归档项按"归档内的名字"挂进去,供压缩工具直接打包。
|
|
|
|
.DESCRIPTION
|
|
7z 没有"入库时改名"的能力:加进去的名字就是文件系统上的名字。Slot 要成为归档内的一层
|
|
目录,就得让它在暂存目录里真的叫那个名字:
|
|
|
|
* 目录项 -> 建 junction(不复制数据,等于零成本改名);
|
|
* 文件项 -> 先试硬链接(同卷),失败再复制(配置文件都很小)。
|
|
|
|
返回暂存目录路径;调用方用完必须调 Remove-BakNRetArchiveStaging 清理。
|
|
建不出连接点时**明确抛错**,绝不悄悄退化成另一种归档布局 —— 布局一变,恢复就对不上。
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory = $true)][array]$Items,
|
|
[string]$Root = $null
|
|
)
|
|
|
|
if (-not $Root) { $Root = Join-Path $env:TEMP ('bnr-stage-' + [guid]::NewGuid().ToString('N')) }
|
|
if (-not (Test-Path -LiteralPath $Root)) {
|
|
New-Item -ItemType Directory -Path $Root -Force | Out-Null
|
|
}
|
|
|
|
# 半途失败必须在这里自己清干净,不能把责任留给调用方。
|
|
#
|
|
# 原因:调用方拿到的是**返回值**,而抛错时根本没有返回值 —— Backup.ps1 的 finally 里
|
|
# `$stagingRoot` 还是 $null,而 Remove-BakNRetArchiveStaging 对 $null 是直接 return。
|
|
# 结果是已经建好的 junction 与临时目录永久留在 %TEMP%,而那些 junction 指向的是真实
|
|
# 数据;临时目录迟早会被某次 Remove-Item -Recurse 扫到,那一下就会走进真实数据。
|
|
try {
|
|
foreach ($item in $Items) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$item.ArchivePath)) {
|
|
throw "归档项缺少归档内路径:$($item.RealPath)"
|
|
}
|
|
|
|
$linkPath = Join-Path $Root $item.ArchivePath
|
|
$parent = Split-Path -Path $linkPath -Parent
|
|
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
|
|
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
|
}
|
|
if (Test-Path -LiteralPath $linkPath) { Remove-BakNRetJunction -Path $linkPath }
|
|
|
|
if ($item.IsFile) {
|
|
try {
|
|
New-Item -ItemType HardLink -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
|
}
|
|
catch {
|
|
Write-BakNRetLog ("暂存:硬链接不可用({0}),改为复制文件 {1}" -f $_.Exception.Message, $item.RealPath) -Level DEBUG
|
|
Copy-Item -LiteralPath $item.RealPath -Destination $linkPath -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
else {
|
|
New-Item -ItemType Junction -Path $linkPath -Target $item.RealPath -ErrorAction Stop | Out-Null
|
|
}
|
|
|
|
Write-BakNRetLog ("暂存:{0} -> {1}" -f $item.ArchivePath, $item.RealPath) -Level DEBUG
|
|
}
|
|
|
|
return $Root
|
|
}
|
|
catch {
|
|
try {
|
|
Remove-BakNRetArchiveStaging -Root $Root
|
|
}
|
|
catch {
|
|
# 清理失败不能盖掉真正的失败原因(那才是排查需要的),所以只告警并点名残留路径
|
|
Write-BakNRetLog ("暂存目录自清理失败,需要手工删除:{0} —— {1}" -f $Root, $_.Exception.Message) -Level WARN
|
|
}
|
|
throw
|
|
}
|
|
}
|