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

新功能
- 新增 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

+88 -1
View File
@@ -253,7 +253,94 @@ Test-Case 'Backup -DryRun:退出码 0、不写归档也不写 manifest' {
}
# ============================================================================
# 6. 失败路径:源不存在时必须留下可核对的记录
# 6. 软件名录:清单里写软件名,归档名就是软件名
# ============================================================================
$catRoot = Join-Path $WorkRoot 'catalog'
$catSource = Join-Path $catRoot 'src'
$catTarget = Join-Path $catSource 'My App' # 真实目录名与软件名刻意不同
$catBackupDir = Join-Path $catRoot 'Backups'
$catFile = Join-Path $catRoot 'SoftwareCatalog.psd1'
$catList = Join-Path $catRoot 'list.txt'
$catConfig = Join-Path $catRoot 'config.psd1'
New-Item -ItemType Directory -Path $catTarget -Force | Out-Null
Set-Content -LiteralPath (Join-Path $catTarget 'data.txt') -Value 'catalog-test' -Encoding UTF8
Set-Content -LiteralPath (Join-Path $catTarget 'skip.bin') -Value 'nope' -Encoding UTF8
[System.IO.File]::WriteAllText($catFile, "@{`n 'my-app' = '$catTarget'`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($catList, "my-app :: skip.bin`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($catConfig, "@{ SoftwareCatalog = '$catFile' }`n", [System.Text.UTF8Encoding]::new($false))
& $backupScript -BackupListPath $catList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
$catExitCode = $LASTEXITCODE
$catArchive = Join-Path $catBackupDir 'my-app.7z'
Test-Case '清单里写软件名 -> 归档名就是软件名' {
Assert-Equal 0 $catExitCode
Assert-FileExists $catArchive
}
Test-Case '软件名条目的归档内容与历史布局一致(根目录仍是源目录名)' {
$extract = Join-Path $catRoot 'verify'
New-Item -ItemType Directory -Path $extract -Force | Out-Null
$sevenZip = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
if ($sevenZip) {
$null = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('x', '-bso0', '-bsp0', '-y', "-o$extract", $catArchive)
# 归档文件名是软件名 my-app,但包内根目录是源目录名 My App
Assert-FileExists (Join-Path $extract 'My App\data.txt')
Assert-FileMissing (Join-Path $extract 'my-app') '包内不应多出一层软件名'
Assert-FileMissing (Join-Path $extract 'My App\skip.bin') '排除模式以源目录名为前缀,仍然生效'
}
}
Test-Case '@pathname 覆盖:强制用路径命名算法(用独立备份目录,避免污染共享 manifest)' {
$pathList = Join-Path $catRoot 'list-pathname.txt'
$pathNameDir = Join-Path $catRoot 'Backups-pathname'
[System.IO.File]::WriteAllText($pathList, "my-app @pathname`n", [System.Text.UTF8Encoding]::new($false))
& $backupScript -BackupListPath $pathList -BackupDir $pathNameDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
# 名录里存的是绝对路径,所以路径命名结果也基于它
$expectedBase = Get-BackupBaseName -RawPath $catTarget
Assert-FileExists (Join-Path $pathNameDir ($expectedBase + '.7z'))
}
Test-Case '软件名录条目:删源后能按原路径恢复' {
Remove-Item -LiteralPath $catTarget -Recurse -Force
& $restoreScript -BackupListPath $catList -BackupDir $catBackupDir -ConfigPath $catConfig -Force
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $catTarget 'data.txt')
Assert-Equal 'catalog-test' (Get-Content -LiteralPath (Join-Path $catTarget 'data.txt') -Raw).Trim()
}
Test-Case '归档名重复时直接报失败,不静默互相覆盖' {
$dupList = Join-Path $catRoot 'dup.txt'
[System.IO.File]::WriteAllText($dupList, "my-app`nmy-app`n", [System.Text.UTF8Encoding]::new($false))
& $backupScript -BackupListPath $dupList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 1 $LASTEXITCODE '重复条目应让退出码为 1'
}
Test-Case '字面路径不受名录影响,仍走路径命名' {
$literalList = Join-Path $catRoot 'list-literal.txt'
[System.IO.File]::WriteAllText($literalList, "$catTarget`n", [System.Text.UTF8Encoding]::new($false))
& $backupScript -BackupListPath $literalList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
}
Test-Case '名录里没有该软件名时记为 missing-source,而不是崩掉' {
$badList = Join-Path $catRoot 'bad.txt'
[System.IO.File]::WriteAllText($badList, "no-such-app`n", [System.Text.UTF8Encoding]::new($false))
& $backupScript -BackupListPath $badList -BackupDir $catBackupDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE '跳过不算失败'
$rec = (Read-BaknretManifest -Path (Join-Path $catBackupDir 'manifest.json')).items['no-such-app']
Assert-True ($null -ne $rec) '应留下记录'
Assert-Equal 'missing-source' $rec.action
}
# ============================================================================
# 7. 失败路径:源不存在时必须留下可核对的记录
# ============================================================================
$missingList = Join-Path $WorkRoot 'missing.txt'