引入软件名录:清单写软件名,归档名也用软件名
新功能 - 新增 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:
1 parent
dbc0c00554
commit
045d51ac9c
9 files changed
+1330
-143
No files matched your search
@@ -0,0 +1,260 @@
|
||||
<#
|
||||
.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
|
||||
Reference in new issue
Block a user