diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf199b..e7cd32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,24 @@ -# 变更日志 +# 变更日志 面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。 ## 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` 等)**照旧可用** —— 垫片会转发并原样带出退出码。 ### 修复 diff --git a/CONTEXT.md b/CONTEXT.md index dcc3773..b1d3685 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,4 +1,4 @@ -# BakNRet +# BakNRet 一个 Windows 备份 / 恢复工具:把机器上指定的软件与目录收进归档,并能把它们放回原位。本文件是本项目的术语表——「要处理什么」「东西放在哪」这些概念在本仓库里只有一个叫法,写作与命名都照这里的词来。 @@ -71,3 +71,22 @@ _Avoid_: 无用归档、残留、垃圾文件 **BakNRet**: 本工具的名称,任何场合(文档、文件名、标识符)都一律写作 `BakNRet`。 _Avoid_: BakNRet、baknret、BakRet、BaknRet 备份工具 + + +## 模块与入口 + +`BakNRet/` 是 PowerShell **模块**:既能被 `Import-Module` 直接用,也是四个入口脚本背后的实现层。 + +| 位置 | 是什么 | 规矩 | +| --- | --- | --- | +| `BakNRet/BakNRet.psd1` | 模块清单 | `FunctionsToExport` 是**显式白名单**:没列进去的不会出现在外面 | +| `BakNRet/BakNRet.psm1` | 加载器 | **唯一**声明点源顺序的地方(Public 再 Private);别处不许自己点源模块内部文件 | +| `BakNRet/Public/` | 对外函数 | **一个函数一个文件、文件名 = 函数名**;公共函数只放这里 | +| `BakNRet/Private/` | 内部实现 | 4 个映射/读取辅助函数 + `State.ps1`(模块状态:编码、日志句柄、运行锁) | + +根目录的四个入口脚本 —— `Manage-Backup.ps1`(主入口,TUI + `-Action`)、`Backup-Data.ps1`、 +`Restore-Data.ps1`、`Edit-Config.ps1` —— 负责"解析参数 → 干活 → `exit` 退出码"。 + +**编排逻辑目前仍在 `Backup-Data.ps1` / `Restore-Data.ps1` 里**(`Invoke-BackupItem` 等),把它们下沉进模块是 +已被记录、尚未实施的下一步:做完之后一份编排就能同时服务 TUI 与无头两条路,TUI 也不必再起子进程 +(见 `docs/adr/0012`)。层与层的分工见 `docs/adr/0001`。 \ No newline at end of file diff --git a/README.md b/README.md index f6a6367..b057fa9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# BakNRet +# BakNRet -把 `BackupList.txt` 里列出的软件 / 目录用 **7-Zip** 打包进 `Backups/`,并且能用 `Restore.ps1` 原样恢复的 Windows 备份工具。 +把 `BackupList.txt` 里列出的软件 / 目录用 **7-Zip** 打包进 `Backups/`,并且能用 `Restore-Data.ps1` 原样恢复的 Windows 备份工具。 - 清单里**直接写软件名**即可(如 `Edge`),目录映射维护在 `SoftwareCatalog.psd1` 里。 - 一个软件一个归档:**归档名 = 软件名**(`Edge.7z`),归档内按名录里的 **Slot 分层** @@ -24,26 +24,26 @@ ```powershell # 1. 先试运行:只打印计划,不写任何文件 -.\Backup.ps1 -DryRun +.\Backup-Data.ps1 -DryRun # 2. 正式备份 -.\Backup.ps1 +.\Backup-Data.ps1 # 3. 强制重打(忽略"源未更新"判断) -.\Backup.ps1 -Force +.\Backup-Data.ps1 -Force # 3b. 确认可以接受"有文件被占用而没打进归档"时,允许覆盖完整归档 -.\Backup.ps1 -Force -AcceptWarnings +.\Backup-Data.ps1 -Force -AcceptWarnings # 4. 只备份 / 只恢复某几项(通配符匹配清单条目或归档名) -.\Backup.ps1 -Only 'Edge','OpenSSH' -.\Restore.ps1 -Only 'Edge' -Force +.\Backup-Data.ps1 -Only 'Edge','OpenSSH' +.\Restore-Data.ps1 -Only 'Edge' -Force # 5. 恢复前先看计划(恢复会覆盖真实目录,务必先看一眼) -.\Restore.ps1 -DryRun +.\Restore-Data.ps1 -DryRun # 6. 只校验所有归档完整性,不解压(只读,安全) -.\Restore.ps1 -VerifyOnly +.\Restore-Data.ps1 -VerifyOnly ``` ## 文件说明 @@ -53,7 +53,10 @@ | `SoftwareCatalog.psd1` | **软件名 → Slot 组**的映射:每个 Slot 是一个目录/文件,以及它的排除、追加、加密、说明 | | `BackupList.txt` | 备份 / 恢复共用的清单,唯一的"要处理什么"来源 | | `BackupConfig.psd1` | 目录、空间阈值、校验、加密等配置 | -| `Backup.ps1` / `Restore.ps1` | 备份 / 恢复入口 | +| `Manage-Backup.ps1` | **主入口**:不带参数进 TUI 菜单;带 `-Action Backup\|Restore\|Config` 直接做该动作(配 `-Quiet` 走无头) | +| `Backup-Data.ps1` / `Restore-Data.ps1` | 备份 / 恢复动作本身(无头,可在计划任务里直接调) | +| `Edit-Config.ps1` | 配置管理:清单 / 设置 / 名录三个界面(见下节) | +| `Backup.ps1` / `Restore.ps1` | **旧名字,转发用的垫片**:内部实现已改名为上面两个,这层只留一轮,删它的时机是大家都改用新名字之后 | | `BakNRet/` | 模块:`BakNRet.psd1`(清单,`FunctionsToExport` 是显式白名单)+ `BakNRet.psm1`(薄加载器,点源顺序只在这里出现一次)+ `Public/`(62 个对外函数,一函数一文件)+ `Private/`(内部函数与模块级状态) | | `test.ps1` | **唯一验收入口**:Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍 | | `PSScriptAnalyzerSettings.psd1` | 静态分析配置(三条有意排除与 160 字符行长,理由见 `docs/adr/0008`) | @@ -118,8 +121,8 @@ | `sourceFiles` / `sourceBytes` / `archiveBytes` | 源文件数、源大小、归档大小 | | `lastSuccessAt` / `successCount` / `failCount` / `lastRestoreAt` / `encrypted` | 历史与安全标记 | -`Restore.ps1` **优先用 manifest 定位归档**,查不到才退回"从文件名反推路径"。 -如果 `BackupList.txt` 丢了,`Restore.ps1` 会优先用 manifest 里的 `source` 自动重建。 +`Restore-Data.ps1` **优先用 manifest 定位归档**,查不到才退回"从文件名反推路径"。 +如果 `BackupList.txt` 丢了,`Restore-Data.ps1` 会优先用 manifest 里的 `source` 自动重建。 ## 备份前空间预估 @@ -189,7 +192,7 @@ # 口令来源(二者取其一) $env:BAKNRET_PASSWORD = '...' # 或 -.\Backup.ps1 -KeyFile 'D:\secret\baknret.key' # 文件首行即口令 +.\Backup-Data.ps1 -KeyFile 'D:\secret\baknret.key' # 文件首行即口令 ``` 一个软件一个归档:名录里各 Slot 的 `Encrypt` 不一致时,**整个归档按加密处理**(宁可多加密,不可漏加密), @@ -218,14 +221,14 @@ $env:BAKNRET_PASSWORD = '...' Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.baknret.key'); EncryptHeaders = $true } # C. 单次指定 -.\Backup.ps1 -KeyFile (Join-Path $env:USERPROFILE '.baknret.key') +.\Backup-Data.ps1 -KeyFile (Join-Path $env:USERPROFILE '.baknret.key') ``` 取不到口令时,加密条目**明确失败**,绝不退化成明文归档 —— 这条行为没有放宽。 > 从旧版本迁移:如果你的口令文件已经在仓库根(`baknret.key`),**什么都不用做**。 > 想改用仓库外的位置,把它移走后按上面 B 或 C 指过去,并先用 -> `pwsh -File .\Restore.ps1 -VerifyOnly -Only "WindowsTerminal" -KeyFile <新路径>` 验一下 +> `pwsh -File .\Restore-Data.ps1 -VerifyOnly -Only "WindowsTerminal" -KeyFile <新路径>` 验一下 > 口令对不对(那份归档是加密的,口令不对会报错)。 ## 计划任务 @@ -235,7 +238,7 @@ Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.b .\tools\Register-BackupTask.ps1 -Remove # 移除 ``` -任务调用 `Backup.ps1`,脚本自身写日志并按失败数返回退出码,所以「上次运行结果」在任务计划程序里可读。 +任务调用 `Backup-Data.ps1`,脚本自身写日志并按失败数返回退出码,所以「上次运行结果」在任务计划程序里可读。 ## 测试 @@ -243,11 +246,11 @@ Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.b | 套件 | 命令 | 需要什么 | 覆盖 | | --- | --- | --- | --- | -| **Pester 套件**(推荐) | `.\tests\Run-Pester.ps1` | Pester 5.0+ 与 7z | 183 项(含安全描述符套件):清单语法(方向 / `::` / `:-` / `:+` / `:encrypt` / `@ Key='值'` / 整行引号与记号边界)、Slot 结构名录、归档命名、排除翻译(`-x!` / `-xr!` / `!re:`)、Slot 前缀分配、暂存、manifest / 配置 / 名录,外加**用子进程真正跑 `Backup.ps1` / `Restore.ps1`** 的端到端与回归 | +| **Pester 套件**(推荐) | `.\tests\Run-Pester.ps1` | Pester 5.0+ 与 7z | 183 项(含安全描述符套件):清单语法(方向 / `::` / `:-` / `:+` / `:encrypt` / `@ Key='值'` / 整行引号与记号边界)、Slot 结构名录、归档命名、排除翻译(`-x!` / `-xr!` / `!re:`)、Slot 前缀分配、暂存、manifest / 配置 / 名录,外加**用子进程真正跑 `Backup-Data.ps1` / `Restore-Data.ps1`** 的端到端与回归 | | 零依赖套件 | `.\tests\Run-Tests.ps1` | 只要 PowerShell + 7z | 111 项:同样的单元面,适合没装 Pester 的机器 | | 端到端验收 | `.\tests\Run-E2E.ps1` | 只要 PowerShell + 7z | 36 项:备份 → 确认排除生效 → 删源 → 恢复 → 逐字节对拍,含 `\` 布局、文件 Slot、方向标记与旧布局回退 | | **真实归档恢复演练** | `.\tests\Restore-Drill.ps1` | 只要 PowerShell + 7z | 12 个真实归档:解到临时目录再和活源逐字节对拍(全程不碰真实目录) | -| **安全描述符套件** | `.\tests\Run-Pester.ps1`(内含 `BakNRet.Security.Tests.ps1`) | Pester 5 + 7z | 含在上面 183 项里:排除判定与 7z `-x!/-xr!` 语义对齐、SID 映射边界(前缀 SID 不被误伤)、采集与 sidecar 往返、回放(`CREATOR OWNER` + 孤儿 SID + `protected` 逐字节一致)、以及真的用子进程跑 `Backup.ps1`/`Restore.ps1` 做端到端 | +| **安全描述符套件** | `.\tests\Run-Pester.ps1`(内含 `BakNRet.Security.Tests.ps1`) | Pester 5 + 7z | 含在上面 183 项里:排除判定与 7z `-x!/-xr!` 语义对齐、SID 映射边界(前缀 SID 不被误伤)、采集与 sidecar 往返、回放(`CREATOR OWNER` + 孤儿 SID + `protected` 逐字节一致)、以及真的用子进程跑 `Backup-Data.ps1`/`Restore-Data.ps1` 做端到端 | 演练会把"源在备份之后变过"和"归档/解压有问题"分开:内容不一致时看活源文件的修改时间, 晚于归档时间就算"源变了"(只提示),不晚于归档时间却内容不同才算失败。真实机器上的归档 @@ -265,7 +268,7 @@ Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.0 `Run-Pester.ps1` 优先使用 `.tools/` 里的本地副本,其次是机器上已装的 5.x; `.tools/` 已进 `.gitignore`。 -Pester 套件里的端到端用例是**用子进程**跑 `Backup.ps1` / `Restore.ps1` 的,原因有二: +Pester 套件里的端到端用例是**用子进程**跑 `Backup-Data.ps1` / `Restore-Data.ps1` 的,原因有二: 两个脚本结尾都会 `exit`,同进程 `&` 调用会把 Pester 宿主一起带走;而且子进程给出的是 真正的进程退出码,正好独立验证"退出码取法"这条修复。 @@ -320,7 +323,7 @@ Hyper-V 虚拟机(`gsudo pwsh -File .\tools\lab\Lab.ps1 ...`),在那里跑 | 步骤 | 它验证什么 | 最近一次结果 | | --- | --- | --- | | `Lab.ps1 test -Suite all` | 三套仓库测试在干净系统上能不能跑 | Pester 183 / 零依赖 111 / 端到端 36,全部通过 | -| `Lab.ps1 backup` | 在 VM 内真跑 `Backup.ps1`(沙盒清单 + 配置) | 退出码 0 | +| `Lab.ps1 backup` | 在 VM 内真跑 `Backup-Data.ps1`(沙盒清单 + 配置) | 退出码 0 | | `Lab.ps1 restore` | 用真实归档做恢复演练,**逐字节对拍** | 通过 6 / 失败 0(含连接点场景 4/4) | | `Lab.ps1 acl-test` | 安全描述符:属主 / `CREATOR OWNER` / 安全指纹 | 全部通过 19 项(含负对照) | @@ -376,3 +379,31 @@ Hyper-V 虚拟机(`gsudo pwsh -File .\tools\lab\Lab.ps1 ...`),在那里跑 - **跨机恢复要配 `Security.SidMap`**:本机不存在的 SID 写进 DACL 是安全的(那条 ACE 只是 永不匹配),但写进**属主**会让谁都没有合理所有权 —— 换域 / 换机时请给映射,或接受 "属主未恢复"的告警。服务账户(`NT SERVICE\X`)的 SID 是按名字算出来的,跨机一致。 + +## 交互界面(TUI) + +不带参数运行主入口就会进菜单: + +```powershell +.\Manage-Backup.ps1 # 主菜单:备份 / 恢复 / 配置 +.\Edit-Config.ps1 # 配置菜单:清单 / 设置 / 名录 +.\Edit-Config.ps1 -Target List # 直接进某个编辑器 +``` + +三个编辑器改配置的方式是**外科式改写**:只动你改的那一行(注释、对齐、`$( )` 表达式、跨行拼接一字节不动), +先校验再落盘,落盘前把原文件按时间戳复制到 `logs\config-backups\`。所以改坏了随时能翻回去。 + +| 界面 | 改什么 | 说明 | +| --- | --- | --- | +| 清单 | `BackupList.txt` | 改方向(`both` / `backup` / `restore`),保留你原来的写法风格(贴着写与留空格都原样) | +| 设置 | `BackupConfig.psd1` | 单行标量设置;值跨行或本身是集合的(如 `SidMap`、`DefaultExcludes`)**不列出**,因为"改一行"对它们没有明确含义 | +| 名录 | `SoftwareCatalog.psd1` | 只有单行的 `Encrypt` 与 `Description` 可改;`Path` 常带动态表达式、`Exclude` 可能是跨行拼接,一律只读 | + +### 无头与自动化 + +- `-Quiet`:不画界面,把被调动作的输出捕获后原样转发出来(计划任务与管道用这个)。 +- `-InputScript`:用按键序列驱动界面,门禁就是靠它把"打开菜单 → 选一项 → 改一个值 → 保存"整条流程跑通的。 + 序列用尽而界面还在等输入时会**报错退出**,绝不退回去读真终端 —— 挂起比失败糟得多。 +- 只有真终端才画界面:检测到输出被重定向且没给按键序列时,明确报错并给退出码 2,不会卡住。 + +设计取舍(为什么零依赖、为什么不做全屏、为什么异常不改退出码)见 `docs/adr/0010` ~ `0013`。 \ No newline at end of file