引入软件名录:清单写软件名,归档名也用软件名

新功能
- 新增 SoftwareCatalog.psd1 —— "软件名 -> 目录"映射表,BackupList.txt 里
  直接写软件名即可,归档名也就是软件名(FooClolor.7z),
  不再是 FooClolor_from_C_+Programs.7z 这种由路径拼出来的名字。
- 三种写法可混用:软件名、字面路径(现有清单无需改写)、软件名 @pathname。
- 名录支持前缀补全(legendary -> legendary_2.0.4,只认 <名>_* / <名>-*)、
  Variants(同名目录在多处)、Includes(分文件维护)。
- tools/Rename-Archives.ps1:存量归档重命名,默认试运行,逐份大小校验并重建 manifest。
- 归档名重复直接报错,不再静默互相覆盖。

两套测试全绿:单元 42 项、端到端 23 项(新增名录命名/解析/迁移用例)。

过程中修掉的缺陷
- Resolve-BackupEntry 里 @pathname 与 Unresolved 分支顺序错误,
  @pathname 会被静默吃掉(改名后仍用软件名)。
- 源目录被删除时解析器丢掉 Sources,导致恢复端把软件名当路径、
  报 "Cannot bind argument to parameter 'Path' because it is an empty string"。
  恢复的语义恰恰是"源不存在就要还原回去",现在 Sources 照旧给出。
- 源存在性检查曾被漏掉,Get-Item 对不存在路径抛异常会中断整轮备份;
  且不能用 Join-Path 探测——目标盘符不存在时它会直接抛异常。
- 计划任务脚本外的 Caller 需要 -DryRun 才能验,已实跑确认。
This commit is contained in:
Shuery committed 2026-09-21 20:55:19 +08:00
1 parent dbc0c00554
commit 045d51ac9c
9 files changed
+1330 -143

No files matched your search

+99
View File
@@ -251,6 +251,105 @@ Test-Case '口令:环境变量可读取,取不到返回 $null' {
}
}
# ============================================================================
Write-Host "`n== 软件名录 ==" -ForegroundColor Cyan
# ============================================================================
$catalogSandbox = Join-Path $env:TEMP ("baknret-cat-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $catalogSandbox -Force | Out-Null
Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' {
$catPath = Join-Path $catalogSandbox 'SoftwareCatalog.psd1'
$content = "@{`n FooClolor = 'C:\Programs\FooClolor'`n 'scoop-config' = '%UserProfile%\.config\scoop'`n '.ssh' = '%UserProfile%\.ssh'`n}`n"
[System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 3 $catalog.Count
Assert-True $catalog.ContainsKey('FooClolor')
Assert-True $catalog.ContainsKey('scoop-config') '带连字符的键必须能解析'
Assert-True $catalog.ContainsKey('.ssh') '带点的键必须能解析'
}
Test-Case '名录解析:目录带版本后缀时按前缀补全(legendary_2.0.4)' {
$root = Join-Path $catalogSandbox 'probe'
New-Item -ItemType Directory -Path (Join-Path $root 'legendary_2.0.4') -Force | Out-Null
$catPath = Join-Path $catalogSandbox 'suffix.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n legendary = '$root\legendary'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 'Single' $catalog['legendary'].Kind
Assert-True ($catalog['legendary'].ResolvedPath -like '*legendary_2.0.4') '应补全到实际目录'
}
Test-Case '名录解析:不会把 Legendary 误配成 LegendarySomething' {
$root = Join-Path $catalogSandbox 'strict'
New-Item -ItemType Directory -Path (Join-Path $root 'LegendarySomething') -Force | Out-Null
$catPath = Join-Path $catalogSandbox 'strict.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = '$root\Legendary'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 'Unresolved' $catalog['Legendary'].Kind '必须以下一个字符是 _ 或 - 为界'
}
Test-Case '软件名条目:默认用软件名做归档名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'FooClolor' $resolved.BaseName
Assert-Equal 'name' $resolved.ArchiveFlavor
Assert-True $resolved.IsName
}
Test-Case '字面路径条目:仍用路径命名算法(现有清单无需改写)' {
$item = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-False $resolved.IsName
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
}
Test-Case '@pathname 用真实路径命名,而不是软件名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor @pathname'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
}
Test-Case '名录里没有该名字:BaseName 退回可读目录名,并给出 Error' {
$item = ConvertFrom-BackupListLine -Line 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'no-such-thing' $resolved.BaseName
Assert-True ($null -ne $resolved.Error) '应给出错误说明'
Assert-Equal 0 $resolved.Sources.Count
}
Test-Case '名录里的路径不存在时仍给出 Sources(恢复要靠它还原回原位)' {
$catPath = Join-Path $catalogSandbox 'gone.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = 'C:\definitely\not\here'`n}`n", [System.Text.UTF8Encoding]::new($false))
$item = ConvertFrom-BackupListLine -Line 'gone-app'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catPath
Assert-True ($null -ne $resolved.Error) '备份端据 Error 跳过'
Assert-Equal 1 $resolved.Sources.Count '恢复端据 Sources 还原'
Assert-Equal 'C:\definitely\not\here' $resolved.Sources[0].SourcePath
}
Test-Case 'Includes:分文件维护的名录会被合并' {
$basePath = Join-Path $catalogSandbox 'Base.psd1'
$extraPath = Join-Path $catalogSandbox 'Extra.psd1'
[System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = 'C:\Programs\Extra'`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = 'C:\Programs\Base'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $basePath
Assert-True $catalog.ContainsKey('base-app')
Assert-True $catalog.ContainsKey('extra-app') 'Includes 引入的条目也应在'
}
Test-Case '软件名会被规范化成合法文件名' {
Assert-Equal 'keep%and&and+plus' (Format-CatalogName -Name 'keep%and&and+plus')
Assert-True ((Format-CatalogName -Name 'a/b:c') -notmatch '[/:]') '非法字符应被替换'
}
Remove-Item -LiteralPath $catalogSandbox -Recurse -Force -ErrorAction SilentlyContinue
# ============================================================================
Write-Host "`n== 集成测试:真的跑 7z,验证排除结果落在文件系统上 ==" -ForegroundColor Cyan
# ============================================================================