Files
BakNRet/SoftwareCatalog.psd1
T
Shuery 43fa4e52dd 清单格式:两种写法(软件名 / 手写目录)都支持 :+ 追加与 :- 排除;名录改用对象数组并逐条介绍目录
需求
- 支持两种条目写法:1) 直接写软件名录里的软件名;2) 用户手写目录。
- 两种写法都必须支持追加(:+)与排除(:-)。
- 软件名录要改进:scoop 合并成"一个软件 + 一个目录数组"。
- 运行时要把"分别是哪些目录、每个目录是干什么的、排除/追加的理由"讲清楚。

实现
- 名录(SoftwareCatalog.psd1 / Get-SoftwareCatalog)
  * 一个软件挂多个目录时写成**对象数组**:@{ Path = '...'; Description = '...' };
    也接受纯字符串数组与旧的 @{ Dirs = ... } / @{ Variants = ... }。
  * 目录说明(Description)一路带到运行日志里。
  * "声明了但当前不存在"的目录不再被丢掉:备份跳过,恢复仍然知道它该回到哪个位置。
  * scoop 合并成一个数组条目(%UserProfile%\scoop\persist + %UserProfile%\.config\scoop);
    ScoopApps-persist 保持独立条目 —— 它和前者末级名同为 persist,并进同一个归档会在包里撞名。
- 解析与解析结果(ConvertFrom-BackupListLine / Resolve-BackupEntry)
  * `:+` 以前只对"软件名且能解析出目录"的写法生效,**手写目录的 :+ 会被整段丢掉**;
    现在统一生效,且 :+ 后面写软件名会按名录展开。
  * 行尾 `# 说明` 解析成 Comment,运行时打印。
- 归档与恢复
  * 同一条目里两个同名目录:打包前明确报错(退出码 1),不再静默混成一棵树。
    (7z 命令行没有"入库改名"的能力,归档内顶层名只能是文件系统上的那个名字。)
  * 恢复时每个源只解出**它自己那棵子树**,不会再往别的父目录里复制兄弟目录。
- 可解释性
  * 新增 Write-BackupEntryPlan:打包前打印条目的目录(含来源与介绍)以及排除/追加的出处;
    Restore.ps1 同样打印"哪棵子树还原到哪、会新建还是覆盖"。
- 孤儿归档审计修正:判据只看当前清单,不再把 manifest 的历史记录当成"已知"。
  否则"条目被合并/改名后留下的旧归档"会被历史记录遮住,永远不会报警。

验证
- Pester 84 项、零依赖单元 49 项、端到端 23 项,全部通过。
- 真实机器:合并后的 scoop.7z 233 MB / 29478 项 / 7z t 通过,manifest.roots=[persist|scoop];
  恢复演练 26982/26982 逐字节一致(.ssh、legendary、Aria 同批通过)。
- 迁移提醒:scoop-config.7z 与 scoop-persist.7z 已无清单条目指向,会出现在孤儿审计里;
  确认 scoop.7z 无误后可以自行删除。
2026-09-22 08:18:16 +08:00

158 lines
7.3 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.
<#
软件名录:维护"软件名 -> 目录"的映射。
有这个文件之后,BackupList.txt 里可以直接写软件名:
FooClolor
Kazumi :: !*Cache
Edge :: !*Cache,component_crx_cache
.ssh @encrypt
归档包的名字也就是软件名(`FooClolor.7z`),不再是
`FooClolor_from_C_+Programs.7z` 这种由路径拼出来的名字。
写法:
<软件名> = '<目录>'
软件名的限制:
* 必须是合法的文件名(不能含 \ / : * ? " < > |),因为它就是归档名;
* 不能含 `\` 或 `/` 或 `%`,否则会被当作字面路径而不是软件名;
* **含 `-` 或 `.` 的名字必须写成带引号的键**,否则 PowerShell 会把
`a-b` 解析成减法表达式并报 "Missing '=' operator":
'scoop-config' = '...' # 正确
scoop-config = '...' # 报错
* 建议用英文/数字,但中文也可以。
目录可以写环境变量,例如 '%UserProfile%\.ssh'。
两个便利特性:
1. 目录不存在时会按前缀补全:写 'D:\Programs\legendary',实际目录是
'D:\Programs\legendary_2.0.4',会自动匹配(只认 `<名>_*` 与 `<名>-*`,
不会把 Legendary 误配成 LegendarySomething)。
2. **一个软件包含多个目录**时,写成**对象数组**(每个目录带自己的说明),全部打进同一个归档:
scoop = @{ Dirs = @(
'%UserProfile%\scoop\persist'
'C:\Programs\ScoopApps\persist'
'%UserProfile%\.config\scoop'
) }
归档里每个目录仍是自己的名字与层级,恢复时会**只解出该目录自己那棵子树**,
各自还原回原位,不会把兄弟目录也复制过去。
纯字符串数组、以及旧的 `@{ Dirs = @(...) }` / `@{ Variants = @(...) }` 写法继续可用。
注意:归档内的顶层名就是目录自己的名字,所以**同一个软件里不能有两个同名目录**
(典型例子是两个都叫 persist 的目录)。那种情况脚本会明确报错并让你拆成两个条目,
而不是把两棵树悄悄混在一起。
分文件维护:用 Includes 引入其它名录文件(路径相对本文件):
@{
Includes = @('SoftwareCatalog.games.psd1')
...
}
#>
@{
# 每个条目有两种写法:
# 1. 只写一个目录字符串: legendary = '%UserProfile%\.config\legendary'
# 2. 带目录介绍(推荐):
# legendary = @{
# Path = '%UserProfile%\.config\legendary'
# Description = 'Legendary(Epic 的开源客户端)的配置与已安装记录'
# }
# 一个软件包含**多个目录**时,写成对象数组(见下面的 scoop)。
# 运行时会把"这个条目打包哪些目录、每个目录是干什么的、排除了什么、为什么"
# 逐条打印出来,说明就来自这里。
# ---- 用户配置 / 开发环境 ----
# 含 `-` 或 `.` 的键必须加引号,否则会被当成减法表达式(见文件开头说明)
legendary = @{
Path = '%UserProfile%\.config\legendary'
Description = 'Legendary(Epic 的开源客户端)的配置与已安装记录'
}
opencode = @{
Path = '%UserProfile%\.config\opencode'
Description = 'opencode 的配置'
}
# 一个软件 = 一个归档;多个目录写成**对象数组**,每个目录各自带说明。
# 注意:归档内的顶层名字就是**目录自己的名字**,所以同一个软件里不能有两个同名目录
# (例如两个 persist)—— 那会在包里混成一棵树,脚本会明确报错让你拆成两个条目。
scoop = @(
@{
Path = '%UserProfile%\scoop\persist'
Description = 'scoop 里各应用的持久化数据(重装应用就会丢,必须备份)'
}
@{
Path = '%UserProfile%\.config\scoop'
Description = 'scoop 自身的配置(源、代理、已安装清单)'
}
)
'.ssh' = @{
Path = '%UserProfile%\.ssh'
Description = 'SSH 私钥 / 公钥 / known_hosts(不可再生;要加密就给清单里那行加 @encrypt)'
}
CodeSpace = @{
Path = 'D:\UserData\Documents\CodeSpace'
Description = '开发代码目录'
}
PowerShell = @{
Path = '%UserProfile%\Documents\PowerShell'
Description = 'PowerShell 7 的用户配置与模块'
}
WindowsPowerShell = @{
Path = '%UserProfile%\Documents\WindowsPowerShell'
Description = 'Windows PowerShell 5.1 的用户配置与模块'
}
# ---- 应用数据 ----
AutoDarkMode = @{ Path = '%AppData%\AutoDarkMode'; Description = 'AutoDarkMode 的主题/时间设置' }
Kazumi = @{ Path = '%AppData%\com.example\Kazumi'; Description = 'Kazumi 的观看记录与设置' }
piliplus = @{ Path = '%AppData%\com.example\piliplus'; Description = 'piliplus 的设置与账号数据' }
fnm = @{ Path = '%AppData%\fnm'; Description = 'fnm(Node 版本管理器)的版本记录' }
'twinkle-tray' = @{ Path = '%AppData%\twinkle-tray'; Description = 'Twinkle Tray 的显示器亮度设置' }
# ---- 浏览器与终端 ----
# Edge 的缓存/扩展本体等可再生内容由 BackupList.txt 的 :- 排除规则挡掉
Edge = @{
Path = '%LocalAppData%\Microsoft\Edge\User Data'
Description = 'Edge 用户数据:书签、密码、Cookies、历史、站点数据'
}
WindowsTerminal = @{
Path = '%LocalAppData%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'
Description = 'Windows Terminal 的设置文件'
}
# ---- 系统 ----
Startup = @{
Path = '%ProgramData%\Microsoft\Windows\Start Menu\Programs\Startup'
Description = '全局开机启动项(快捷方式)'
}
# ---- C:\Programs ----
BaiduNetdisk = @{ Path = 'C:\Programs\BaiduNetdisk'; Description = '百度网盘客户端' }
# FooClolor 的具体用途不明确,先不加介绍(没有 Description 也不会影响打包)
FooClolor = 'C:\Programs\FooClolor'
March7thAssistant = @{
Path = 'C:\Programs\March7thAssistant'
Description = '三月七助手(WebBrowser 用户目录里的缓存由 BackupList.txt 排除)'
}
MiFlash = @{ Path = 'C:\Programs\MiFlash'; Description = '小米刷机工具 MiFlash' }
MiFlash_Unlock = @{ Path = 'C:\Programs\MiFlash_Unlock'; Description = '小米解锁工具' }
QuarkCloudDrive = @{ Path = 'C:\Programs\QuarkCloudDrive'; Description = '夸克网盘客户端' }
translucenttb = @{
Path = 'C:\Programs\ScoopApps\apps\translucenttb\current\settings.json'
Description = 'TranslucentTB 的设置文件'
}
'ScoopApps-persist' = @{
Path = 'C:\Programs\ScoopApps\persist'
Description = 'ScoopApps 安装位置上那份 persist。它和 scoop 数组里的 %UserProfile%\scoop\persist 是两个不同目录、末级名却同为 persist,所以不能并进同一个归档'
}
# ---- 其它盘 ----
Aria = @{ Path = 'D:\UserData\Documents\Aria'; Description = 'Aria 下载器的配置与任务' }
}