Files
BakNRet/tools/Rename-Archives.ps1
T
Shuery 045d51ac9c 引入软件名录:清单写软件名,归档名也用软件名
新功能
- 新增 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 才能验,已实跑确认。
2026-09-21 20:55:19 +08:00

261 lines
9.8 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
把按路径命名的旧归档重命名成软件名,并重建 manifest.json。
.DESCRIPTION
重构前的归档名是 `<末级名>_from_<上级路径>`(如 FooClolor_from_C_+Programs.7z)。
引入软件名录后,归档名默认就是软件名(FooClolor.7z)。这个脚本负责把存量归档搬过去。
做法:
1. 遍历清单条目,算出"旧名"(路径命名算法)与"新名"(当前规则);
2. 只在两者不同、且旧名归档确实存在时才处理;
3. 重命名(不是复制,同卷上是元数据操作,不搬数据);
4. 重建 manifest.json,把旧记录的历史字段(成功次数、SHA256 等)迁过去;
5. 比对重命名前后的文件大小做完整性自检。
默认是**试运行**,只打印计划。确认无误后加 -Apply 才真正执行。
.EXAMPLE
# 先看会发生什么(不改任何文件)
.\tools\Rename-Archives.ps1
.EXAMPLE
# 真正执行
.\tools\Rename-Archives.ps1 -Apply
#>
[CmdletBinding()]
param(
[string]$BackupListPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'BackupList.txt'),
[string]$BackupDir,
[string]$ConfigPath,
[switch]$Apply
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
Import-Module (Join-Path $projectRoot 'Common.psm1') -Force
if (-not $ConfigPath) { $ConfigPath = Join-Path $projectRoot 'BackupConfig.psd1' }
$config = Get-BaknretConfig -Path $ConfigPath
if (-not $BackupDir) {
$BackupDir = $config.BackupDir
if (-not [System.IO.Path]::IsPathRooted($BackupDir)) { $BackupDir = Join-Path $projectRoot $BackupDir }
}
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$manifestPath = Join-Path $BackupDir 'manifest.json'
$supportedFormats = @('.7z', '.rar', '.zip', '.tar')
if (-not (Test-Path -LiteralPath $BackupDir)) {
Write-Host "备份目录不存在:$BackupDir" -ForegroundColor Red
exit 1
}
Write-Host ''
Write-Host "备份目录 : $BackupDir"
Write-Host "软件名录 : $catalogPath"
Write-Host "清单 : $BackupListPath"
Write-Host "模式 : $(if ($Apply) { '执行(-Apply)' } else { '试运行(默认,不改任何文件)' })"
Write-Host ''
function Find-ArchiveByBaseName {
param([string]$BaseName, [string]$Directory, [string[]]$Formats)
return Get-ChildItem -LiteralPath $Directory -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.BaseName -eq $BaseName -and $_.Extension.ToLower() -in $Formats } |
Select-Object -First 1
}
# ---------------------------------------------------------------------------
# 阶段一:规划
# ---------------------------------------------------------------------------
$manifestOld = Read-BaknretManifest -Path $manifestPath
$plan = @()
$unchanged = 0
$missingOld = 0
$conflicts = @()
$seenNew = @{}
$entries = @()
foreach ($line in (Get-Content -LiteralPath $BackupListPath)) {
$item = ConvertFrom-BackupListLine -Line $line
if (-not $item) { continue }
# 旧名 = 对"真实源路径"跑路径命名算法。
# 注意:清单里现在写的是软件名,直接把它丢给 Get-BackupBaseName 会得到一个
# 恰好和软件名一模一样的"旧名"(legendary -> legendary),于是永远算不出
# 真正的旧名。必须先解析出真实路径。
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$newName = $resolved.BaseName
$oldNameSource = $item.Path
if ($resolved.IsName -and $resolved.CatalogEntry) { $oldNameSource = $resolved.CatalogEntry.Path }
$oldName = Get-BackupBaseName -RawPath $oldNameSource
if (-not $oldName -or -not $newName) { continue }
if ($seenNew.ContainsKey($newName)) {
$conflicts += "归档名 '$newName' 被 '$($seenNew[$newName])' 和 '$($item.Path)' 同时使用"
continue
}
$seenNew[$newName] = $item.Path
$entries += [pscustomobject]@{ Item = $item; OldName = $oldName; NewName = $newName; Resolved = $resolved }
if ($oldName -eq $newName) { $unchanged++; continue }
$oldFile = Find-ArchiveByBaseName -BaseName $oldName -Directory $BackupDir -Formats $supportedFormats
if (-not $oldFile) { $missingOld++; continue }
$plan += [pscustomobject]@{
Old = $oldFile
New = ($newName + $oldFile.Extension)
Item = $item
Resolved = $resolved
}
}
Write-Host "待重命名 $($plan.Count) 个;命名未变 $unchanged 个;清单里有但归档不存在 $missingOld 个"
if ($plan.Count -gt 0) {
Write-Host ''
foreach ($entry in $plan) {
Write-Host (" {0,10:N1} MB {1}" -f ($entry.Old.Length / 1MB), $entry.Old.Name)
Write-Host (" -> {0}" -f $entry.New)
}
}
if ($conflicts.Count -gt 0) {
Write-Host ''
Write-Host '存在归档名冲突,必须先解决(否则会互相覆盖):' -ForegroundColor Red
$conflicts | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
exit 1
}
Write-Host ''
if (-not $Apply) {
Write-Host '试运行结束,未改动任何文件。确认无误后加 -Apply 执行。' -ForegroundColor Yellow
exit 0
}
# ---------------------------------------------------------------------------
# 阶段二:重命名
# ---------------------------------------------------------------------------
$ok = 0
$failed = 0
foreach ($entry in $plan) {
$target = Join-Path $BackupDir $entry.New
try {
if (Test-Path -LiteralPath $target) {
Write-Host "跳过(目标已存在): $($entry.New)" -ForegroundColor Yellow
$failed++
continue
}
$oldSize = $entry.Old.Length
Rename-Item -LiteralPath $entry.Old.FullName -NewName $entry.New -ErrorAction Stop
$newFile = Get-Item -LiteralPath $target -ErrorAction Stop
if ($newFile.Length -ne $oldSize) {
throw "重命名后大小不一致($oldSize -> $($newFile.Length))"
}
Write-Host "已重命名: $($entry.Old.Name) -> $($entry.New)" -ForegroundColor Green
$ok++
} catch {
Write-Host "重命名失败: $($entry.Old.Name) —— $_" -ForegroundColor Red
$failed++
}
}
# ---------------------------------------------------------------------------
# 阶段三:重建 manifest
# ---------------------------------------------------------------------------
$rebuilt = [ordered]@{}
$now = (Get-Date).ToString('o')
foreach ($entry in $entries) {
$file = Find-ArchiveByBaseName -BaseName $entry.NewName -Directory $BackupDir -Formats $supportedFormats
# 历史字段优先从新键取,其次从旧键(路径命名)取
$previous = $null
if ($manifestOld.items.Contains($entry.NewName)) { $previous = $manifestOld.items[$entry.NewName] }
elseif ($manifestOld.items.Contains($entry.OldName)) { $previous = $manifestOld.items[$entry.OldName] }
$getPrevious = {
param([string]$Field)
if ($previous -and ($previous.PSObject.Properties.Name -contains $Field)) { return $previous.$Field }
return $null
}
$record = [ordered]@{
baseName = $entry.NewName
source = $entry.Item.Path
resolvedSource = [Environment]::ExpandEnvironmentVariables($entry.Item.Path)
roots = @($entry.Resolved.Sources | ForEach-Object { $_.RootName })
catalog = $(if ($entry.Resolved.CatalogEntry) { $entry.Resolved.CatalogEntry.Path } else { $null })
archive = $(if ($file) { $file.Name } else { $entry.NewName + '.7z' })
action = $(if ($file) { 'backed-up' } else { 'missing-source' })
reason = '由 tools/Rename-Archives.ps1 迁移生成'
phase = 'migrate'
attemptedAt = $now
finishedAt = $now
durationSec = $null
exitCode = $null
verified = $false
warnings = $false
attemptWarnings = $false
encrypted = ($entry.Item.Flags -contains 'encrypt')
sourceFiles = (& $getPrevious 'sourceFiles')
sourceBytes = (& $getPrevious 'sourceBytes')
archiveBytes = $(if ($file) { $file.Length } else { $null })
sha256 = (& $getPrevious 'sha256')
lastSuccessAt = $(if ($file) { (& $getPrevious 'lastSuccessAt') } else { (& $getPrevious 'lastSuccessAt') })
successCount = $([int](& $getPrevious 'successCount'))
failCount = $([int](& $getPrevious 'failCount'))
}
$lastRestore = & $getPrevious 'lastRestoreAt'
if ($lastRestore) { $record['lastRestoreAt'] = $lastRestore }
if ($file) { $rebuilt[$entry.NewName] = $record }
}
$manifestNew = [pscustomobject]@{
schemaVersion = 1
tool = 'BakNRet'
updatedAt = $null
compressor = $manifestOld.compressor
items = $rebuilt
}
Write-BaknretManifest -Path $manifestPath -Manifest $manifestNew | Out-Null
# ---------------------------------------------------------------------------
# 阶段四:报告
# ---------------------------------------------------------------------------
$referenced = @($rebuilt.Keys)
$orphans = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension.ToLower() -in $supportedFormats -and $_.BaseName -notin $referenced })
Write-Host ''
Write-Host "重命名完成:成功 $ok,失败 $failed;manifest 已重建,含 $($rebuilt.Count) 个条目" -ForegroundColor Green
if ($orphans.Count -gt 0) {
Write-Host ''
Write-Host '以下归档没有任何清单条目指向(恢复不到,注意别误删):' -ForegroundColor Yellow
foreach ($orphan in $orphans) {
Write-Host (" - {0}({1:N1} MB)" -f $orphan.Name, ($orphan.Length / 1MB)) -ForegroundColor Yellow
}
}
if ($failed -gt 0) { exit 1 }
exit 0