Files
Shuery d72fe63c02 docs: 跟上 TUI 与入口重组(README 入口名 + TUI 一节 + CHANGELOG + CONTEXT 模块与入口)
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)。
2026-09-27 22:40:54 +08:00

84 lines
9.8 KiB
Markdown
Raw Permalink 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.
# 变更日志
面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。
## 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 仓库 | — | 都有 |