chore: 记录改造前基线

改造开始前的完整状态,作为可回退的基点。此提交之后:Pester 175 项、零依赖套件 101 项全绿;PowerShell 5.1 尚不可用(源文件无 BOM)。

包含此前未提交的在制品:安全描述符套件、Hyper-V 实验环境(tools/lab)、agent 约定(AGENTS.md 与 docs/agents)。

.gitignore 增加 *.key / *.pfx:BackupConfig.psd1 的 PasswordFile 此前默认指向仓库内的 baknret.key,一次 git add -A 就会把口令提交进版本库。默认值在后续提交中改为空。
This commit is contained in:
Shuery committed 2026-09-26 21:46:55 +08:00
1 parent 7173e8ae10
commit 2937eb6652
32 files changed
+8775 -1691

No files matched your search

+95 -27
View File
@@ -1,14 +1,20 @@
<#
.SYNOPSIS
把按路径命名的旧归档重命名成软件名,并重建 manifest.json。
把归档名对齐到当前清单规则,并重建 manifest.json。
.DESCRIPTION
重构前的归档名是 `<末级名>_from_<上级路径>`(如 FooClolor_from_C_+Programs.7z)。
引入软件名录后,归档名默认就是软件名(FooClolor.7z)。这个脚本负责把存量归档搬过去。
归档名由清单条目决定:
* 软件名条目 -> 归档名 = 软件名(`Edge.7z`);
* 手写路径条目 -> 归档名 = `<末级名>_from_<上级路径>`(`FooClolor_from_C_+Programs.7z`)。
条目写法变过(把软件名改成手写路径、改名、合并条目……)之后,磁盘上的旧归档名就与当前
规则对不上了 —— 那样的归档恢复不到,会被当成孤儿。这个脚本负责把它们搬过去。
做法:
1. 遍历清单条目,算出"旧名"(路径命名算法)与"新名"(当前规则);
2. 只在两者不同、且旧名归档确实存在时才处理;
1. 遍历清单条目,算出**当前规则下的目标名**,以及一组**候选旧名**
(路径命名算法 / 名录里的软件名 / manifest 里记过的归档名);
2. 目标名已经存在就跳过;否则在候选旧名里找实际存在的归档;
3. 重命名(不是复制,同卷上是元数据操作,不搬数据);
4. 重建 manifest.json,把旧记录的历史字段(成功次数、SHA256 等)迁过去;
5. 比对重命名前后的文件大小做完整性自检。
@@ -74,6 +80,29 @@ function Find-ArchiveByBaseName {
$manifestOld = Read-BaknretManifest -Path $manifestPath
# manifest 里的历史归档名按 source / resolvedSource 建索引:
# 条目写法改过(软件名 -> 手写路径、改名、合并)之后,键对不上了,
# 但"这条清单行原本指向哪儿"通常还留在这两个字段里,靠它才能把旧归档接上。
$manifestBySource = @{}
foreach ($key in @($manifestOld.items.Keys)) {
$record = $manifestOld.items[$key]
if (-not $record) { continue }
$archiveName = $key
if (($record.PSObject.Properties.Name -contains 'archive') -and $record.archive) {
$archiveName = [System.IO.Path]::GetFileNameWithoutExtension([string]$record.archive)
}
foreach ($field in 'source', 'resolvedSource', 'catalog') {
if (-not ($record.PSObject.Properties.Name -contains $field)) { continue }
$value = [string]$record.$field
if ([string]::IsNullOrWhiteSpace($value)) { continue }
$mapKey = $value.Trim().ToLower()
if (-not $manifestBySource.ContainsKey($mapKey)) { $manifestBySource[$mapKey] = @() }
$manifestBySource[$mapKey] += $archiveName
}
}
$plan = @()
$unchanged = 0
$missingOld = 0
@@ -85,23 +114,9 @@ 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 }
if ([string]::IsNullOrWhiteSpace([string]$oldNameSource)) {
# 数组形式的名录条目没有唯一的"原路径",推不出旧归档名,跳过即可
Write-Host (" 跳过 {0}:名录条目是数组形式,算不出旧归档名" -f $item.Path) -ForegroundColor DarkGray
continue
}
$oldName = Get-BackupBaseName -RawPath $oldNameSource
if (-not $oldName -or -not $newName) { continue }
if (-not $newName) { continue }
if ($seenNew.ContainsKey($newName)) {
$conflicts += "归档名 '$newName' 被 '$($seenNew[$newName])' 和 '$($item.Path)' 同时使用"
@@ -109,11 +124,57 @@ foreach ($line in (Get-Content -LiteralPath $BackupListPath)) {
}
$seenNew[$newName] = $item.Path
$entries += [pscustomobject]@{ Item = $item; OldName = $oldName; NewName = $newName; Resolved = $resolved }
# 候选旧名(按可能性排序):
# 1. 路径命名算法(对名录条目要用 Slot 的 Path,直接拿软件名算出来的是错的);
# 2. 名录里的软件名(旧规则:归档名 = 软件名);
# 3. manifest 里为这条记录记过的归档名。
$candidates = @()
if ($oldName -eq $newName) { $unchanged++; continue }
$pathSource = $item.Path
if ($resolved.IsName) {
$slots = @($resolved.CatalogEntry.Slots)
$pathSource = if ($slots.Count -eq 1) { $slots[0].Declared } else { $null }
}
if ($pathSource) {
$derived = Get-BackupBaseName -RawPath $pathSource
if ($derived) { $candidates += $derived }
}
if ($resolved.IsName) {
$candidates += (Format-CatalogName -Name $item.Path)
}
if ($manifestOld.items.Contains($newName)) {
$recorded = $manifestOld.items[$newName]
if (($recorded.PSObject.Properties.Name -contains 'archive') -and $recorded.archive) {
$candidates += [System.IO.Path]::GetFileNameWithoutExtension([string]$recorded.archive)
}
}
$oldFile = Find-ArchiveByBaseName -BaseName $oldName -Directory $BackupDir -Formats $supportedFormats
# manifest 里"指向过同一个源"的历史归档名
$lookupKeys = @([string]$item.Path)
foreach ($entryItem in @($resolved.Items)) {
if ($entryItem.Declared) { $lookupKeys += [string]$entryItem.Declared }
if ($entryItem.RealPath) { $lookupKeys += [string]$entryItem.RealPath }
}
foreach ($lookupKey in $lookupKeys) {
if ([string]::IsNullOrWhiteSpace($lookupKey)) { continue }
$mapKey = $lookupKey.Trim().ToLower()
if ($manifestBySource.ContainsKey($mapKey)) { $candidates += @($manifestBySource[$mapKey]) }
}
$candidates = @($candidates | Where-Object { $_ -and $_ -ne $newName } | Select-Object -Unique)
$entries += [pscustomobject]@{ Item = $item; NewName = $newName; Resolved = $resolved; Candidates = $candidates }
if (Find-ArchiveByBaseName -BaseName $newName -Directory $BackupDir -Formats $supportedFormats) {
$unchanged++
continue
}
$oldFile = $null
foreach ($candidate in $candidates) {
$foundCandidate = Find-ArchiveByBaseName -BaseName $candidate -Directory $BackupDir -Formats $supportedFormats
if ($foundCandidate) { $oldFile = $foundCandidate; break }
}
if (-not $oldFile) { $missingOld++; continue }
$plan += [pscustomobject]@{
@@ -190,10 +251,14 @@ $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] }
else {
foreach ($candidate in $entry.Candidates) {
if ($manifestOld.items.Contains($candidate)) { $previous = $manifestOld.items[$candidate]; break }
}
}
$getPrevious = {
param([string]$Field)
@@ -205,7 +270,10 @@ foreach ($entry in $entries) {
baseName = $entry.NewName
source = $entry.Item.Path
resolvedSource = [Environment]::ExpandEnvironmentVariables($entry.Item.Path)
roots = @($entry.Resolved.Sources | ForEach-Object { $_.RootName })
roots = @($entry.Resolved.Items | ForEach-Object { $_.TopName } | Select-Object -Unique)
layouts = @($entry.Resolved.Items | ForEach-Object {
[ordered]@{ name = $_.ArchivePath; kind = $(if ($_.IsFile) { 'file' } else { 'dir' }) }
})
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' })
@@ -218,7 +286,7 @@ foreach ($entry in $entries) {
verified = $false
warnings = $false
attemptWarnings = $false
encrypted = ($entry.Item.Flags -contains 'encrypt')
encrypted = [bool]$entry.Resolved.Encrypt
sourceFiles = (& $getPrevious 'sourceFiles')
sourceBytes = (& $getPrevious 'sourceBytes')
archiveBytes = $(if ($file) { $file.Length } else { $null })