README 里 24 处过期入口名(13 处 Backup.ps1、11 处 Restore.ps1)全部更新为新名字 —— 这正是 ADR-0012 里垫片存在的理由:内部实现改名断了会当场报错,而 README 里的入口名过期是**静默没用**,所以垫片留一轮等文档跟上。现在文档跟上了,删垫片的时机也就到了。 README 新增「交互界面(TUI)」一节:怎么进三个编辑器、每个编辑器能改什么(以及为什么有些字段刻意不列 —— 值跨行或本身是集合时"改一行"没有明确含义)、-Quiet 与 -InputScript 的用途、以及"只有真终端才画界面,否则明确报错并给退出码 2"。入口表从"两个入口"扩成四个(主入口 / 动作 / 配置 / 垫片)。 CHANGELOG 增加「新增:交互界面与入口重组」一节,含三项编辑器共用的外科式改写保证与那条往返断言;并写明旧命令照旧可用。 CONTEXT.md 增加「模块与入口」一节:BakNRet/ 四层的分工与规矩(清单是显式白名单、加载器是唯一点源顺序声明处、Public 一个函数一个文件、Private 放内部实现与 State),以及四个入口脚本的职责与"编排尚未下沉"这个已知的下一步。 验收:test.ps1 9/9 全绿(5.1 与 7)。
84 lines
9.8 KiB
Markdown
84 lines
9.8 KiB
Markdown
# 变更日志
|
||
|
||
面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。
|
||
|
||
## 2026-09 强化与重构
|
||
|
||
本次改造的每一处修复都有实测证据,不是"看起来更规范了"。
|
||
### 新增:交互界面(TUI)与入口重组
|
||
|
||
| 变化 | 说明 |
|
||
| --- | --- |
|
||
| 主入口 `Manage-Backup.ps1` | 不带参数进菜单(备份 / 恢复 / 配置);带 `-Action` 直接做该动作,可配 `-Quiet` 走无头 |
|
||
| 配置入口 `Edit-Config.ps1` | 三个编辑器:清单(改方向)、设置(改单行标量)、名录(改 `Encrypt` / `Description`) |
|
||
| 改名:`Backup.ps1` → `Backup-Data.ps1`、`Restore.ps1` → `Restore-Data.ps1` | 旧名字留**垫片**转发,退出码与输出原样传递;只留一轮 |
|
||
| 零依赖 TUI | 只用 `RawUI.ReadKey` / `[Console]` / `Write-Host`:不装模块、不带 DLL,两个 PowerShell 版本都能跑 |
|
||
|
||
编辑器改配置是**外科式改写**:只动被编辑的那一行(注释、对齐、`$( )` 表达式、跨行拼接一字节不动),
|
||
先校验再原子替换,落盘前留时间戳副本到 `logs\config-backups\`。三项都写成了判据(含"把每个字段设成它
|
||
当前的值、文件必须逐字节相同"这条往返断言),并跑在 Windows PowerShell 5.1 与 PowerShell 7 上。
|
||
|
||
旧命令(`.\Backup.ps1 -DryRun` 等)**照旧可用** —— 垫片会转发并原样带出退出码。
|
||
|
||
### 修复
|
||
|
||
| 问题 | 现象 | 现在 |
|
||
| --- | --- | --- |
|
||
| 源文件是无 BOM 的 UTF-8 | **6/6 个文件在 Windows PowerShell 5.1 上连语法都过不去** —— 5.1 没有 BOM 就按 ANSI 解码源码,中文变乱码、全角问号吃掉引号 | 全仓改存 UTF-8 with BOM;100 个文件在 5.1 与 7 上解析零错 |
|
||
| 带 `[CmdletBinding()]` 的脚本在 5.1 上,`param()` 默认值拿不到 `$PSScriptRoot` | `Backup.ps1` 在 5.1 上不传路径参数直接报错 —— 而计划任务恰恰不传 | 8 处默认值移到 `param()` 之后解析 |
|
||
| 行首方向标记贴在目标上时不被识别(`+WindowsTerminal`) | 清单 28 条里 **27 条被当成"源不存在"静默跳过**,退出码仍是 0;只有不写标记的 `Scoop` 真被备份 | 两种写法都认;干跑从"重打 1 个"变成"重打 11 个" |
|
||
| 暂存目录半途失败会留下 junction | `%TEMP%` 里留下**指向真实数据**的连接点,而临时目录迟早会被 `Remove-Item -Recurse` 扫到 | 建暂存的函数自己带 try/catch 自清理,不依赖调用方 |
|
||
| 口令随 `-Verbose` 落进日志 | DEBUG 级打印整条命令行,而 7z 只接受命令行口令 | 打印前把 `-p` 参数换成占位符;新增断言 + 红绿证明 |
|
||
| `manifest.json` 先删后移 | `Move-Item` 一失败,旧账本已经没了 | 复用原子写;目标存在时用 `File.Replace` |
|
||
| 5.1 上拿不到原子替换 | 三参数 `File.Move` 是 .NET Core 3.0+ 才有的重载,5.1 上必然退化成"先删后移" | 改用 `File.Replace`(.NET Framework 上同样可用) |
|
||
| 空目录的 `TotalSize` 是 `$null` | `$null / 1GB` 得 0,而空间守卫判的是 `-gt 0` —— **空间不足时不再拦截** | 补成整数 0 |
|
||
| 计划任务与手动运行撞车 | 两边互踩 `manifest.json`,还会抢同一个 `<归档>.tmp` | 备份目录上加一把跨进程锁(独占文件句柄,进程崩溃自动释放) |
|
||
| 口令文件的出厂默认值指向仓库内 | 一次 `git add -A` 就把口令提交进版本库 | 默认留空;口令只来自 `-Password` / 环境变量 / 仓库外的文件 / 交互询问 |
|
||
| lab 的 ACL 场景在 5.1 上自爆 | 清理函数偏偏在它要处理的"悬空连接点"场景里被原生命令的 stderr 中断 | 原生命令统一走 `Invoke-NativeTolerant` |
|
||
|
||
### 结构与规范
|
||
|
||
| 变化 | 说明 |
|
||
| --- | --- |
|
||
| 模块源码拆分 | 3152 行、66 个函数的 `Common.psm1` → `BakNRet/{Public,Private}` 一函数一文件 + 薄加载器 + 显式导出白名单;`tools/Build-BakNRetModule.ps1` 可随时合回单文件 |
|
||
| 函数命名 | 16 个公共函数补 `BakNRet` 前缀、全仓大小写统一。这不是审美:静态分析拓出 `Write-Log` 与本机某个已装模块**重名**,后果是导入两个模块时一方的命令被静默遮蔽 |
|
||
| 静态分析 | 装上 PSScriptAnalyzer 并全仓重排:**706 → 71 条告警**。注意那 6 条格式规则默认是 Disabled —— 不带 `-Settings` 的调用会静默漏掉全部排版问题 |
|
||
| 编码与行尾 | `.editorconfig`(含 `charset = utf-8-bom`)与 `.gitattributes` 把约定钉成可执行的文件 |
|
||
| 验收入口 | `test.ps1` 是唯一入口(Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍);`tests/Run-RealSmoke.ps1` 拿**真实清单**跑只读冒烟 |
|
||
| 术语与决策 | `CONTEXT.md` 术语表 + `docs/adr/` 8 条决策记录 |
|
||
|
||
# 相对旧版修了什么(更早的历史)
|
||
|
||
| 问题 | 旧行为 | 现行为 |
|
||
| --- | --- | --- |
|
||
| `Start-Process -PassThru` 的 `ExitCode` 在 PowerShell 7.7.0-preview.4 上恒为 `$null` | 压缩明明成功却报"压缩失败",`exit 2 → 删档重试` 的自愈分支永远不可达 | 用 `.NET Process` 继承控制台启动,退出码可靠 |
|
||
| 排除模式写成 `-x!"路径"` | 引号成为模式的一部分,**排除对所有条目都失效** | 不再嵌引号;含空格自动转 `?`,`!` 前缀走 `-xr!` |
|
||
| 解析器用 `;` 分隔,清单里写的是 `,` | 整串被当成一个模式,等于没有排除 | `,` 与 `;` 都支持 |
|
||
| `^"([^"]+)"` 贪婪匹配 | 整行加引号的写法把排除表吞进路径 → 该条目被静默跳过,2.8 GB 归档成了孤儿 | 先按空白分词切出修饰符,再处理引号 |
|
||
| 归档名由路径拼出 | 加一条备份要自己算名字,名字随路径变动 | 清单写软件名,归档名就是软件名 |
|
||
| 一个软件里两个同名目录(例如两个 `persist`) | 静默混成一棵树,两边的数据都错 | 名录改成 **Slot 结构**,每个 Slot 是归档内的一层目录,同名不再冲突 |
|
||
| 清单只能"备份 + 恢复"一把抓 | 想只备份的、只恢复的条目得另开文件 | 行首 `+` / `-` 直接标方向,两条路径共用一份清单 |
|
||
| `::` 既是"排除"又是历史别名 | 语义含糊:`::` 一会儿是排除、一会儿是路径 | `::` 只表示**覆盖 Path**,排除一律写 `:-` |
|
||
| 加密只能靠裸标记 `@encrypt` | 名录里的加密意图没法表达 | `:encrypt` / `:!encrypt` / `@ Encrypt='$false'`,名录的 Slot 也能写 `Encrypt` |
|
||
| 排除/追加只能写在清单行里 | 名录里的 Slot 光有路径,规则全堆在清单里 | `Exclude` / `Include` / `Encrypt` 都可以写在 Slot 上,清单按需覆盖 |
|
||
| `!` 只能按通配符匹配 | 想按正则排除做不到 | 新增 `!re:<正则>`(脚本遍历源目录翻译成精确排除项) |
|
||
| 名录路径只支持 `%变量%` | `scoop prefix xxx` 这类动态路径写不出来 | `Path` 支持 `$( ... )` 子表达式,并在一次运行内缓存求值结果 |
|
||
| 名录每解析一个条目就重新 Import 一次 | 同一个文件被反复读取、`$( ... )` 被反复执行 | 按内容指纹缓存,一次运行只读一次 |
|
||
| 直接更新已有归档(7z `u`) | 固实归档下收益极小,且排除规则与"源里已删的文件"永远反映不到归档里 | 临时文件 → `7z t` 校验 → 原子替换 |
|
||
| 没有校验、没有记录 | 中断留下的半个归档会被下次 `u` 续写;跳过/失败只有一行滚过去的 WARN | 校验 + 原子替换 + `manifest.json` + 日志文件 |
|
||
| 结尾不 `exit` | 全部失败也返回 0,计划任务永远显示成功 | 有失败返回 1 |
|
||
| 恢复用 `-Filter "$baseName.*"` | 含 `[` `]` 的路径会失配 | 精确比较 `BaseName`,且优先查 manifest |
|
||
| tar 分支 `$LASTEXITCODE -ne 0 -and $proc.ExitCode -ne 0` | `$LASTEXITCODE` 是上一条原生命令的残留值,恰为 0 时把解压失败吞掉 | 三条分支统一走同一个取退出码的封装 |
|
||
| 恢复没有干跑 | 直接覆盖 `E:\CodeSpace`、Edge User Data 这类真实目录 | `-WhatIf` / `-DryRun` / `-VerifyOnly` / `-Only` |
|
||
| `manifest.json` 的 `roots` | 记的是软件名,与归档里真实的顶层目录对不上(`Edge` vs `User Data`) | 记归档内真实的顶层条目名,并且和归档内容对账过 |
|
||
| `-DryRun` / `-WhatIf` / `-VerifyOnly` | 仍然写回 `manifest.json`,违背"不会写入任何文件" | 只有真的恢复成功了才写回(用 manifest 的 SHA256 前后对比验证) |
|
||
| 孤儿归档 | 只在恢复时列一下;带 `-Only` 时还会把未选中的归档误报成孤儿,吓得人不敢删 | 备份端也做孤儿审计;`-Only` / `-Skip` 时不再误报;`+` / `-` 的条目也算"有主" |
|
||
| 手写目录的 `:+` 追加 | 被整段丢掉(只有软件名写法才生效),既没人报错也没人知道 | 两种写法都生效,追加项还会标出来源(名录 / 追加项) |
|
||
| 软件名录的多目录写法 | 一个软件可以挂多个目录,但目录名不能重复,否则包内混成一棵树 | 改成 **Slot 结构**:每个 Slot 是包内一层目录,同名目录(两个 `persist`)不再冲突 |
|
||
| 一个条目挂多个目录的恢复 | 把整包解压到每个位置的父目录,会在别的父目录下凭空冒出兄弟目录 | 每个归档项只解出**它自己那棵子树** |
|
||
| 归档内路径冲突 | 静默混成一棵树,两边的数据都错 | 打包前明确报错(退出码 1)并提示改 Slot 名 / 归档内相对路径 |
|
||
| 运行时的可解释性 | 只有一行"开始备份: X" | 逐条打印目录、来源、介绍、排除/追加的出处与理由;备份前还会预估所需空间并判断够不够 |
|
||
| manifest 的 `archive` 字段 | 源不存在的条目也留着归档名,指向一个根本不存在的文件;恢复时白报"归档不存在" | 只在文件真的存在时才写;删掉归档后同步一次就自我纠正 |
|
||
| 没有名录、manifest、测试、README,不是 git 仓库 | — | 都有 |
|
||
|