Shuery
|
e8f0a5ae19
|
docs: 更新 README(口令一节、测试计数、虚拟机验证)并修掉拆文档时留下的重复标题
README 是被人真读的那份文档,而这一路改动让它有三处不再准确:
1. 「口令放在哪里」整节是错的:它写着"默认留空、必须放在仓库之外",而按你的决定,出厂默认值就是仓库根的 baknret.key(靠 .gitignore 兜住不被提交)。整节重写:如实写明这是一次取舍(省事 vs "不提交"依赖一个规则文件),并补上"相对路径按仓库根解析、不按工作目录"这条 —— 计划任务的工作目录是 C:\Windows\System32,按工作目录解析会让加密条目以"拿不到口令"失败而配置看上去没问题。关键提醒也改了:已经在仓库根的人什么都不用做,想搬走才需要动。
2. 测试项数过期:Pester 150 -> 183(含安全描述符套件)、零依赖 101 -> 111。
3. 新增「在 Hyper-V 虚拟机里验证」:这一层本机跑不到,而它覆盖的正是最需要真环境的那条路径 —— 安全描述符回放(把属主改成 NT AUTHORITY\SYSTEM、再靠 CREATOR OWNER 判断恢复后归谁)。附最近一次四步的结果与命令。
另修一个我在拆分 README 时留下的缺陷:4 个二级标题各重复了一次(块替换保留了原标题、又插入了带同名标题的指针行)。markdown 不经过 test.ps1,所以当时没被拦住 —— 现在按"相邻同标题只留一行"折掉,并复查为 0。
验收:test.ps1 9/9 全绿(7 与 5.1)。
|
2026-09-27 14:10:04 +08:00 |
|
Shuery
|
2446c7c5c7
|
docs: 把"前缀补全只搜一层、且不提供深度开关"写成决策记录
深度递归不是"补上一个没实现的功能",而是引入一个具体的错。写成 ADR-0009 连同实测数据:26 条真实 Path 里 19 条直接命中、3 条补全救不了(软件没装)、1 条(%UserProfile%\fnm)在 5 层内会命中 AppData\Local\fnm_multishells 这个临时目录 —— 静默备份错的东西还报成功。
同时在 Find-BakNRetChildDirectoryByName 的注释里指回 ADR,免得下一个人把它当"漏了的功能"补回去。
顺带修掉 README 里一句与实现不符的话:原先写"前缀补全命中多个候选(同名目录分散在多处)",而只搜一层时多个候选只可能来自同一个父目录(既有 X 又有 X_后缀)。
全仓复查:MaxDepth / CatalogMaxDepth / 最大深度 / 向下找几层 除 ADR-0009 的历史叙述外 0 处。
验收:test.ps1 9/9 全绿(7 与 5.1)。
|
2026-09-27 11:34:34 +08:00 |
|
Shuery
|
120cf3584b
|
fix: 清掉改造过程留下的陈旧引用(代码改了、文档还写着旧的)
这一轮做的是交付一致性检查:把这一路改掉/移走/删掉的每个符号在全仓(含文档)扫一遍。42 处命中里 38 处是正当的 —— CHANGELOG 与 ADR 里讲“原来是什么”属于历史叙述,test 里的 Assert-FileExists 是因为我只抑制了误判而没有改名。剩下的 4 处是真陈旧:
* README 还在配置表里写着 CatalogMaxDepth,而那个配置项上一轮已经整条移除;
* Get-BakNRetItemArchiveName 的 param 里还留着一个内联的 [int]$MaxDepth = 5(上一轮的删除按行匹配,没覆盖到这种写在同一行的参数),它已经没有任何调用方传值;
* tools\lab\README.md 与 docs\agents\domain.md 还写着模块叫 Common.psm1。
这正是这次改造从头到尾在抓的毛病:文档承诺的东西,代码已经不做了。区别是多了一个可执行的检查 —— 符号改名/删除之后,全仓扫一遍旧名字。
验收:test.ps1 9/9 全绿(7 与 5.1)、真实清单只读冒烟 4/4。
|
2026-09-27 10:49:21 +08:00 |
|
Shuery
|
60ecbc2933
|
docs: README 拆出四篇主题文档,本文件留总览与操作
拆出软件名录(67 行)、清单语法(114 行)、归档布局与迁移(47 行)、安全描述符(69 行),共 297 行;README 从 653 降到 360 行,每节原位留一句摘要加链接。
为什么拆:这四块是「要查的资料」,README 剩下的是「要读的流程」。混在一起时,想查清单语法的人得先滚过一百多行总览;拆开后每篇也能被单独引用与单独评审。
搬运按标题抽取原文、不重打,并在写之前断言正文长度、写之后再读回断言一次(第一次尝试就是栽在没有断言上:@(a, b, $arr) 不会展开数组,而是把 $arr 拼成一行,结果四篇各只有 5 行、正文却已从 README 删除。那次已回滚)。
|
2026-09-27 10:20:08 +08:00 |
|
Shuery
|
fb0d93a25f
|
Revert "docs: README 拆出四篇主题文档,本文件留总览与操作"
This reverts commit 35aade63bc.
|
2026-09-27 10:19:34 +08:00 |
|
Shuery
|
35aade63bc
|
docs: README 拆出四篇主题文档,本文件留总览与操作
拆出去的是最大且自成体系的那四块参考资料:软件名录(67 行)、清单语法(114 行)、归档布局与迁移(47 行)、安全描述符(69 行),共 297 行。README 从 653 行降到 360 行,每一节原位留一句摘要加链接。
为什么拆:这四块是"要查的资料",而 README 剩下的(快速开始、文件说明、恢复语义、manifest、配置、加密、口令、计划任务、测试、验收、设计取舍、已知限制)是"要读的流程"。混在一起时,想查清单语法的人得先滚过 130 行总览。拆开之后每篇也能被单独引用与单独评审。
搬运用脚本按标题抽取原文,不重打,所以不存在抄漏的风险;拆完校验了每一条指向新文件的链接目标都存在。
|
2026-09-27 10:17:19 +08:00 |
|
Shuery
|
3a3a57a6a1
|
docs: 决策记录 8 条、变更日志、README 的验收与口令两节
0007 与 0008 是本次改造新产生的决策(原计划 6 条)。它们同样满足「后人会问为什么」:把运行锁改成互斥体、或给库里 27 个函数补上 SupportsShouldProcess,都是看起来更规范的错法。
CHANGELOG.md:把 README 里那 33 行「相对旧版修了什么」整节搬过去,并补上本次改造的记录(11 条修复 + 6 条结构与规范,每条修复都写明现象与现状)。README 那一节换成指针。
README 新增两节:「验收与静态分析」(六个层次、Run-RealSmoke 为什么独立存在、以及那 6 条格式规则默认 Disabled 这个容易漏掉的事实);「口令放在哪里」(默认留空不是遗漏,附迁移与验证命令)。
.markdownlint.json 照 PowerShell 主仓库的实践(default true、行长 240),只把 MD024 从关闭改成「仅同级不重复」,因为变更日志需要重复标题。顺带按 .editorconfig 把 .md 的 BOM 去掉。
|
2026-09-27 10:01:54 +08:00 |
|
Shuery
|
2937eb6652
|
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 就会把口令提交进版本库。默认值在后续提交中改为空。
|
2026-09-26 21:46:55 +08:00 |
|
Shuery
|
7173e8ae10
|
备份前空间预估;manifest 的 archive 字段只在文件真的存在时才写
需求
- 写满盘这件事只做一件事:**备份前预测本次所需大小,并提示用户存储够不够**,
不再往"更复杂的占用控制"方向做。
实现
- Backup.ps1 新增只读的"备份前空间预估",在动手之前按清单顺序模拟一遍:
* 逐个条目枚举源目录得到真实源大小/文件数,并读取现有归档大小;
* 沿用主循环那套"源未更新就跳过"的判断,所以列出来的就是**本次真的会重打**的条目;
* 估算模型:临时归档写完时旧归档还在,那一刻占用 = 当前累计净增量 + 本次预估;
原子替换后净增量 = 预估 − 旧归档大小;峰值取整个过程的最大值;
* 预估归档大小:有历史归档取 min(源大小, 旧归档 × 1.3),没有则按"不压缩"的悲观值;
* 打印:可用空间、要重打的条目数(及跳过/缺源的数量)、逐条目明细(前 15 条)、
预计峰值新增与净增量,最后给一句结论——"空间足够"或"空间可能不够!…差 Z GB"。
* 不够时**只告警、不中断**:真正放不下的条目仍由逐条目守卫跳过。
- 新增 Sync-BaknretManifestArchive(Common.psm1,Backup/Restore 都在写 manifest 前调用):
维持不变式"manifest 里写了 archive 的记录,磁盘上就一定有那个文件",
把指向不存在归档的 archive 字段清空,但保留 source / action / 历史计数。
这样"源不存在的条目"不会再让 Restore 反复报"manifest 记录的归档不存在",
人工删掉归档(例如把它并进别的条目)之后记录也会自我纠正。
整理(本机备份集)
- 按用户确认,删除了已被 scoop.7z 覆盖的 scoop-config.7z 与 scoop-persist.7z
(删除前先 7z t 复验 scoop.7z:233 MB / 29478 项 / 顶层 [persist, scoop]),
并清掉 manifest 里这两条历史记录;24 条记录的 archive 现在全部存在于磁盘上。
测试
- Pester 88 项、零依赖单元 49 项、端到端 23 项,全部通过。
新增覆盖:对象数组名录、两种写法 × :+/-、同名目录拒绝执行、行尾 # 说明、
孤儿审计(含"manifest 里还有历史记录"的说明)、archive 字段一致性、空间预估输出。
|
2026-09-22 08:26:09 +08:00 |
|
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 |
|
Shuery
|
e114cae8c8
|
P0-P3 全量重构:退出码 / 解析修复、manifest 与 7z t 校验、干跑、排除规则、日志、测试与计划任务
P0 正确性
- 退出码:改用 .NET Process 直接启动、让子进程继承控制台,不再用 Start-Process -PassThru
(在 7.7.0-preview.4 上 ExitCode 恒为 $null,会把成功的压缩判成失败);
7z / RAR / tar 三条解压分支统一走同一个取退出码的封装。
- BackupList 解析:先按第一个 :: 切段再处理引号(整行被一对引号包住的写法不再把排除表
吞进路径);排除表同时接受 , 与 ;(旧实现只认 ;,导致排除从未生效);支持 :- / :+ / @flag。
- 补回 .ssh 与孤儿归档:.ssh 进清单;孤儿归档在备份端也做审计并点名;
带 -Only / -Skip 时不再把未选中的归档误报成孤儿。
- Resolve-BackupEntry 里 $rootName 在赋值前被引用(会读到外层作用域残留值),已提前赋值。
P1 归档可靠性
- 每个条目写进 manifest.json:源、归档、时间、退出码、校验结果、失败原因,
并区分 warnings(在位归档)与 attemptWarnings(本次尝试)。
- 归档后做 7z t 内容校验,先写 .tmp、校验通过再原子替换(File.Move overwrite)。
- manifest.roots 记录归档内**真实**的顶层条目名(原先记的是软件名,Edge 实际是 "User Data")。
P2 可用性
- Restore 支持 -WhatIf / -DryRun / -VerifyOnly / -Only / -Skip;
这三种"只看不写"的模式一个字节都不写(原先会写回 manifest.json)。
- Edge 等高缓存条目加排除规则并实测:1781 MB / 27961 项 -> 72 MB / 2294 项;
书签、密码、Cookies、偏好、历史、IndexedDB、Local Storage 全部保留。
普通模式是相对归档根目录锚定的,嵌套的那些(如 OneAuth\WebView2 里的 Crashpad)
改用 ! 组件形式才会命中。
- 日志落盘 logs/<backup|restore>-<时间戳>.log;退出码按失败数返回。
- tools/Register-BackupTask.ps1 注册每日计划任务;tools/Rename-Archives.ps1 迁移旧归档名。
- root= 标记此前静默失效,现在明确告警(该功能尚未实现)。
P3 测试与验证
- tests/BakNRet.Tests.ps1:Pester 5 套件 62 项(含用子进程跑 Backup.ps1 / Restore.ps1
的端到端与针对上述缺陷的回归)。
- tests/Run-Pester.ps1 + tools/Install-TestDependencies.ps1:把 Pester 装到仓库内 .tools/,
不动机器上的全局模块(系统自带的 3.4.0 缺 Should -Be)。
- tests/Restore-Drill.ps1:真实归档恢复演练,明确区分"源在备份后变过"与"归档/解压有问题"。
- tests/Run-Tests.ps1(49 项,零依赖)与 tests/Run-E2E.ps1(23 项)继续可用;三套共 134 项全通过。
真实机器验证
- 生产归档 22/22 通过 7z t;-VerifyOnly 不再改动 manifest.json(SHA256 前后一致)。
- 真实恢复演练 12/12 通过,27,670 个文件与活源逐字节一致。
- 修复了生产 scoop-persist.7z:原先只有 90 字节(空归档)而源有 1.3 GB,
重打包后 233 MB,恢复演练 26981/26981 全部一致。
|
2026-09-21 23:02:47 +08:00 |
|
Shuery
|
045d51ac9c
|
引入软件名录:清单写软件名,归档名也用软件名
新功能
- 新增 SoftwareCatalog.psd1 —— "软件名 -> 目录"映射表,BackupList.txt 里
直接写软件名即可,归档名也就是软件名(FooClolor.7z),
不再是 FooClolor_from_C_+Programs.7z 这种由路径拼出来的名字。
- 三种写法可混用:软件名、字面路径(现有清单无需改写)、软件名 @pathname。
- 名录支持前缀补全(legendary -> legendary_2.0.4,只认 <名>_* / <名>-*)、
Variants(同名目录在多处)、Includes(分文件维护)。
- tools/Rename-Archives.ps1:存量归档重命名,默认试运行,逐份大小校验并重建 manifest。
- 归档名重复直接报错,不再静默互相覆盖。
两套测试全绿:单元 42 项、端到端 23 项(新增名录命名/解析/迁移用例)。
过程中修掉的缺陷
- Resolve-BackupEntry 里 @pathname 与 Unresolved 分支顺序错误,
@pathname 会被静默吃掉(改名后仍用软件名)。
- 源目录被删除时解析器丢掉 Sources,导致恢复端把软件名当路径、
报 "Cannot bind argument to parameter 'Path' because it is an empty string"。
恢复的语义恰恰是"源不存在就要还原回去",现在 Sources 照旧给出。
- 源存在性检查曾被漏掉,Get-Item 对不存在路径抛异常会中断整轮备份;
且不能用 Join-Path 探测——目标盘符不存在时它会直接抛异常。
- 计划任务脚本外的 Caller 需要 -DryRun 才能验,已实跑确认。
|
2026-09-21 20:55:19 +08:00 |
|
Shuery
|
dbc0c00554
|
重构为可核对、可恢复的备份工具(P0-P3)
修复(P0)
- 退出码:改用 .NET Process 继承控制台启动外部命令。Start-Process -PassThru 的
ExitCode 在 PowerShell 7.7.0-preview.4 上恒为 $null,会把成功的压缩判成失败,
并让 "exit 2 -> 删档重试" 的自愈分支永远不可达。
- BackupList.txt 解析:先按第一个 :: 切开再处理引号,修正整行被引号包住时
排除表被吞进路径的问题(该条目此前被静默跳过,其 2.8 GB 归档成了孤儿)。
- 排除分隔符同时接受 , 与 ;:此前解析器只认 ; 而清单里写的是 ,,
等于所有排除规则都没生效。
- 7z 排除参数不再嵌引号,含空格的模式自动转成 ?:旧写法 -x!"路径" 会让引号
成为模式的一部分,导致排除对所有条目都失效。
加固(P1)
- 先写临时归档 -> 7z t 校验 -> 原子替换,中断不再污染正式归档。
- 放弃 7z 的更新模式 u:固实压缩下收益极小,却让排除规则改动与已删文件
永远进不了归档。
- 新增 Backups/manifest.json 与 logs/*.log,跳过/失败有据可查。
- 结尾按失败数 exit;恢复支持 -WhatIf / -DryRun / -VerifyOnly / -Only。
- 恢复优先用 manifest 定位归档,并精确比较 BaseName(不再用 -Filter 通配)。
- 修正 tar 分支用 $LASTEXITCODE 判断成功与否的缺陷。
- 有警告(文件被占用)时拒绝用不完整的归档覆盖完整归档,需显式 -AcceptWarnings。
策略与安全(P2)
- Edge 条目加排除规则:解压后 4.22 GB 中 3.79 GB 是可再生的缓存/遥测/扩展本体,
保留书签、密码、偏好、历史与站点数据。
- 可选 7z 加密(@encrypt 标记或全局开关),取不到口令时明确失败,绝不写明文。
- 磁盘空间守卫:放不下就跳过该条目,低于阈值告警。
工程化(P3)
- 新增 BackupConfig.psd1、README.md、.gitignore。
- tests/Run-Tests.ps1(32 项)与 tests/Run-E2E.ps1(16 项端到端验收)。
- tools/Register-BackupTask.ps1 注册每日计划任务。
- 归档命名算法保持不变,已有归档不会失联。
|
2026-09-21 20:10:18 +08:00 |
|