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

9.8 KiB
Raw Permalink Blame History

变更日志

面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。

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 仓库 — 都有