二进制及自命令命名统一简化
This commit is contained in:
+4
-4
@@ -27,7 +27,7 @@
|
||||
| 回收站恢复用例;gc dry-run→commit;7 天内孤儿图片不删 | ✅ | `TestTrashLifecycleAndGC` + CLI 实测(dry-run 报告、commit 执行、宽限期保留) |
|
||||
| user_version 越界启动被拒;--allow-newer 放行 | ✅ | `TestMigrationNewerRejected` + CLI 实测(拒启 exit=1;--allow-newer 后 /api/health 200) |
|
||||
| 构建冒烟测试通过 | ✅ | `make smoke`(npm ci → vite build → sync-assets → CGO_ENABLED=0 交叉构建 → 起服务 19 项断言:SPA 资源 200+MIME、meta 注入、安全头、私有不可见) |
|
||||
| Caddy 仅开放 80/443、程序监听 127.0.0.1 | ✅(配置交付) | deploy/Caddyfile + pure-note.service(`--addr 127.0.0.1:8080 --behind-proxy`)——本机无生产部署环境,按配置文件验收 |
|
||||
| Caddy 仅开放 80/443、程序监听 127.0.0.1 | ✅(配置交付) | deploy/Caddyfile + pn.service(`--addr 127.0.0.1:8080 --behind-proxy`)——本机无生产部署环境,按配置文件验收 |
|
||||
|
||||
## 三、§13 测试策略落地
|
||||
|
||||
@@ -70,11 +70,11 @@
|
||||
## 六、交付物清单
|
||||
|
||||
```
|
||||
pure-note(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
||||
├── cmd/pure-note/ serve / init / reset-password / backup / gc / version
|
||||
pn(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
||||
├── cmd/pn/ start / init / passwd / backup / gc / version
|
||||
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
|
||||
├── web/ React 19 + Vite 8 + Tailwind 4(vitest 6 用例)
|
||||
├── deploy/ pure-note.service、pure-note-maint.{service,timer}、Caddyfile
|
||||
├── deploy/ pn.service、pn-maint.{service,timer}、Caddyfile
|
||||
├── scripts/smoke.sh 构建冒烟(make smoke)
|
||||
├── Makefile / README.md / .gitignore
|
||||
└── docs/{design.md, review-round1.md, decisions.md, acceptance.md}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| --- | --- | --- |
|
||||
| D1 | Go 工具链使用 **1.26.6**(设计写 1.27.1) | 本机已装版本。所用特性(1.22+ ServeMux 方法/通配符路由、slog、go:embed)全部满足,无升级必要 |
|
||||
| D2 | 依赖版本按设计锁定值解析:modernc.org/sqlite v1.58.0、goldmark v1.8.6、bluemonday v1.0.27、x/crypto v0.56.0、x/time v0.15.0 | 设计附录 A「实际开发时以 go mod tidy 解析到的版本为准」——解析结果与设计核实值完全一致 |
|
||||
| D3 | 新增第 6 个直接依赖 **golang.org/x/term v0.45.0** | `pure-note init` 交互式口令输入需要关闭回显。设计 §2.2 预算「直接依赖 ≤6(Go 侧 5 个)」,6 个在预算内;`PN_ADMIN_PASSWORD` 环境变量仍是非交互主通道 |
|
||||
| D3 | 新增第 6 个直接依赖 **golang.org/x/term v0.45.0** | `pn init` 交互式口令输入需要关闭回显。设计 §2.2 预算「直接依赖 ≤6(Go 侧 5 个)」,6 个在预算内;`PN_ADMIN_PASSWORD` 环境变量仍是非交互主通道 |
|
||||
| D4 | 前端 TypeScript 用 **5.9**(设计允许 7.x 或回退 5.9) | 直接采用设计声明的保守回退路径,规避第三方类型兼容风险(CodeMirror/react-markdown 生态) |
|
||||
| D5 | 前端其余依赖与设计一致:react 19.2.8、vite 8.2.2、tailwind 4.3.3、react-router 8.3.1、@tanstack/react-query 5.102.8、react-markdown 10.1.0 等,npm 解析 0 漏洞 | `npm install` 实测 |
|
||||
|
||||
|
||||
+25
-25
@@ -129,7 +129,7 @@
|
||||
│ HTTPS (Caddy 自动 TLS, 反代, 追加密名 XFF)
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ pure-note 单一二进制 (Go) │
|
||||
│ pn 单一二进制 (Go) │
|
||||
│ │
|
||||
│ net/http + ServeMux │
|
||||
│ ├─ 公共 API /api/* │──┐
|
||||
@@ -145,7 +145,7 @@
|
||||
│ database/sql (SetMaxOpenConns=1)
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ data/pure-note.db (SQLite)│
|
||||
│ data/pn.db (SQLite) │
|
||||
│ notes(含 deleted_at) / │
|
||||
│ images(BLOB) / image_refs /│
|
||||
│ sessions / settings │
|
||||
@@ -176,8 +176,8 @@ pure-note/
|
||||
│ ├── design.md # 本文档(v1.1)
|
||||
│ └── review-round1.md # 第 1 轮审评报告(修订依据,留档)
|
||||
├── cmd/
|
||||
│ └── pure-note/
|
||||
│ └── main.go # 入口 + 命令行(serve/init/reset-password/backup/gc/version)
|
||||
│ └── pn/
|
||||
│ └── main.go # 入口 + 命令行(start/init/passwd/backup/gc/version)
|
||||
├── internal/
|
||||
│ ├── config/ # 环境变量/flag 解析
|
||||
│ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO
|
||||
@@ -200,8 +200,8 @@ pure-note/
|
||||
│ ├── features/ # notes / admin 业务 hooks
|
||||
│ └── pages/ # Home / Note / Tags / AdminLogin / AdminList / AdminTrash / AdminEdit
|
||||
└── deploy/
|
||||
├── pure-note.service # systemd 服务单元
|
||||
├── pure-note-maint.timer # 每日 gc + 备份 timer
|
||||
├── pn.service # systemd 服务单元
|
||||
├── pn-maint.timer # 每日 gc + 备份 timer
|
||||
└── Caddyfile # 反代示例
|
||||
```
|
||||
|
||||
@@ -213,7 +213,7 @@ pure-note/
|
||||
|
||||
```sql
|
||||
-- 连接串(DSN)统一注入以下 pragma:
|
||||
-- file:data/pure-note.db?_pragma=journal_mode(WAL)
|
||||
-- file:data/pn.db?_pragma=journal_mode(WAL)
|
||||
-- &_pragma=busy_timeout(5000)
|
||||
-- &_pragma=foreign_keys(1)
|
||||
-- &_pragma=synchronous(NORMAL)
|
||||
@@ -281,7 +281,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
| 迁移 | `PRAGMA user_version` 版本化,顺序执行内嵌迁移脚本;**仅追加式**(禁止删列/重命名/改类型);启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(`--allow-newer` 显式放行,§10.4) |
|
||||
| 并发 | `db.SetMaxOpenConns(1)` — 单写者串行化,WAL 下个人规模足够;规模上来后按「多读者连接池」扩展 |
|
||||
|
||||
### 6.3 数据清理(`pure-note gc`)
|
||||
### 6.3 数据清理(`pn gc`)
|
||||
|
||||
默认 `--dry-run` 只输出清理计划,`--commit` 才执行;systemd timer 每日调用一次(§10.3):
|
||||
|
||||
@@ -348,14 +348,14 @@ SecurityHeaders(含 HSTS,§9.2)
|
||||
|
||||
### 7.3 认证与会话
|
||||
|
||||
1. **初始化**:`pure-note init`(交互式设密码,或环境变量 `PN_ADMIN_PASSWORD` 非交互)→ Argon2id(m=19456, t=2, p=1, salt=16B 随机, keyLen=32B) **以 PHC 串 `$argon2id$v=19$m=19456,t=2,p=1$<b64salt>$<b64hash>` 存库**——参数随哈希走,未来调参可继续校验旧口令;
|
||||
1. **初始化**:`pn init`(交互式设密码,或环境变量 `PN_ADMIN_PASSWORD` 非交互)→ Argon2id(m=19456, t=2, p=1, salt=16B 随机, keyLen=32B) **以 PHC 串 `$argon2id$v=19$m=19456,t=2,p=1$<b64salt>$<b64hash>` 存库**——参数随哈希走,未来调参可继续校验旧口令;
|
||||
2. **登录**:Origin/Referer→Host 校验通过后,per-IP 10 次/5 分钟 + 账号维度 5 次/10 分钟令牌桶(只计失败尝试);校验用 `subtle.ConstantTimeCompare`;失败统一返回 401「用户名或密码错误」不泄露差异;
|
||||
3. **会话 Cookie**:`__Host-pn_session` = 256bit 随机 token;属性 `Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=7d`(`__Host-` 前缀自带 Secure + 根路径约束);
|
||||
4. **CSRF**:会话行内存随机 `csrf_token`,登录响应下发、前端仅存内存并附到所有 `/api/admin/*` 变更请求的 `X-CSRF-Token` 头;**刷新页面后经 `GET /api/me` 重取**;明确禁止任何 localStorage 持久化;服务端同时校验 Origin/Referer 与 Host 匹配;
|
||||
5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感);
|
||||
6. **登出**:删除会话行;
|
||||
7. **改密**:`POST /api/admin/password` 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制);
|
||||
8. **口令重置(CLI)**:`pure-note reset-password` 免旧口令直接覆盖哈希并**吊销全部会话**——前提是具备服务器访问权限(读写数据目录即可改库,CLI 提供规范入口优于手工改库);口令来源与 `init` 一致,避免出现在 argv;
|
||||
8. **口令重置(CLI)**:`pn passwd` 免旧口令直接覆盖哈希并**吊销全部会话**——前提是具备服务器访问权限(读写数据目录即可改库,CLI 提供规范入口优于手工改库);口令来源与 `init` 一致,避免出现在 argv;
|
||||
9. **开发模式**:`--dev` 允许非 Secure Cookie(否则 `__Host-` 在 `http://局域网IP` 下无法登录);**强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产**。
|
||||
|
||||
### 7.4 图片上传与访问
|
||||
@@ -376,12 +376,12 @@ SecurityHeaders(含 HSTS,§9.2)
|
||||
|
||||
| 子命令 / 开关 | 说明 |
|
||||
| --- | --- |
|
||||
| `pure-note serve` | 启动;`--addr :8080`、`--data-dir`(默认 `./data`)、`--log-level`、`--log-format text|json`、`--behind-proxy`(声明位于可信反代之后,启用 XFF 处理)、`--dev`(loopback-only,§7.3-8)、`--allow-newer`(跳过 schema 版本上界校验,§10.4);环境变量:`PN_ADMIN_PASSWORD` |
|
||||
| `pure-note init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
||||
| `pure-note reset-password` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 |
|
||||
| `pure-note backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
||||
| `pure-note gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
||||
| `pure-note version` | 版本号(build 时注入 commit/时间) |
|
||||
| `pn start` | 启动;`--addr :8080`、`--data-dir`(默认 `./data`)、`--log-level`、`--log-format text|json`、`--behind-proxy`(声明位于可信反代之后,启用 XFF 处理)、`--dev`(loopback-only,§7.3-8)、`--allow-newer`(跳过 schema 版本上界校验,§10.4);环境变量:`PN_ADMIN_PASSWORD` |
|
||||
| `pn init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
||||
| `pn passwd` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 |
|
||||
| `pn backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
||||
| `pn gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
||||
| `pn version` | 版本号(build 时注入 commit/时间);亦支持 `-v` / `--version` |
|
||||
|
||||
---
|
||||
|
||||
@@ -522,20 +522,20 @@ sync-assets:
|
||||
|
||||
build: web sync-assets
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
||||
-o pure-note ./cmd/pure-note
|
||||
-o pn ./cmd/pn
|
||||
|
||||
linux: web sync-assets
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
|
||||
-ldflags="-s -w" -o pure-note-linux-amd64 ./cmd/pure-note
|
||||
-ldflags="-s -w" -o pn-linux-amd64 ./cmd/pn
|
||||
|
||||
dev: # 双端热重载:air(Go)+ vite dev(proxy /api → 127.0.0.1:8080)+ serve --dev
|
||||
dev: # 双端热重载:air(Go)+ vite dev(proxy /api → 127.0.0.1:8080)+ start --dev
|
||||
air & cd web && npm run dev
|
||||
```
|
||||
|
||||
### 10.2 运行
|
||||
|
||||
```ini
|
||||
# deploy/pure-note.service
|
||||
# deploy/pn.service
|
||||
[Unit]
|
||||
Description=Pure Note
|
||||
After=network.target
|
||||
@@ -543,7 +543,7 @@ After=network.target
|
||||
[Service]
|
||||
User=purenote
|
||||
WorkingDirectory=/opt/pure-note
|
||||
ExecStart=/opt/pure-note/pure-note serve --addr 127.0.0.1:8080 \
|
||||
ExecStart=/opt/pure-note/pn start --addr 127.0.0.1:8080 \
|
||||
--data-dir /opt/pure-note/data --behind-proxy
|
||||
Restart=on-failure
|
||||
NoNewPrivileges=true
|
||||
@@ -571,7 +571,7 @@ example.com {
|
||||
**调度(systemd timer,替代 root cron)**
|
||||
|
||||
```ini
|
||||
# deploy/pure-note-maint.timer
|
||||
# deploy/pn-maint.timer
|
||||
[Unit]
|
||||
Description=Daily Pure Note maintenance (gc + backup)
|
||||
|
||||
@@ -583,7 +583,7 @@ Persistent=true
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
timer 触发 `pure-note-maint.service`:先 `pure-note gc --commit`,后 `pure-note backup /backup/pure-note-$(date +\%F).db`。
|
||||
timer 触发 `pn-maint.service`:先 `pn gc --commit`,后 `pn backup /backup/pn-$(date +\%F).db`。
|
||||
|
||||
**要点**
|
||||
|
||||
@@ -591,11 +591,11 @@ timer 触发 `pure-note-maint.service`:先 `pure-note gc --commit`,后 `pure
|
||||
- `/backup` 目录属主 `purenote`、权限 0700;备份文件**chmod 0600**(内含全部私有笔记、图片、口令哈希、会话哈希的明文副本);
|
||||
- 保留 30 天;**语义声明**:备份保留期内被删除的数据物理上仍存在于备份中;
|
||||
- 异地同步(可选):`rclone` 远端启用 **crypt**(或先 `age` 加密再上传),禁止明文上云;
|
||||
- **恢复 SOP**:停服 → 删除 `data/pure-note.db-wal` 与 `-shm`(残留会污染还原)→ 以备份文件替换 `pure-note.db` → 起服。
|
||||
- **恢复 SOP**:停服 → 删除 `data/pn.db-wal` 与 `-shm`(残留会污染还原)→ 以备份文件替换 `pn.db` → 起服。
|
||||
|
||||
### 10.4 升级
|
||||
|
||||
1. **升级前第一步固定为 `pure-note backup`**(写入 SOP);
|
||||
1. **升级前第一步固定为 `pn backup`**(写入 SOP);
|
||||
2. 停服 → 替换二进制 → 起服触发迁移:迁移仅追加式(不删列/重命名/改类型),`user_version` 顺序递增;
|
||||
3. **启动守卫**:代码校验 `user_version ≤ 本版本支持的最高版本`,超出**拒绝启动**并提示,`--allow-newer` 显式放行(防止旧二进制静默在陌生 schema 上读写);
|
||||
4. 回滚:若迁移尚未执行(版本一致)可直接换回旧二进制;若已执行且不满足追加式约束,回滚 = 从步骤 1 的备份还原数据文件后再换回旧二进制。
|
||||
|
||||
@@ -139,7 +139,7 @@ T10 点名了 API/RSS/sitemap/图片,唯独漏了 tags;API 表里只写「
|
||||
### P1-13 升级回滚不严谨(§10.4)
|
||||
|
||||
「回滚 = 换回旧二进制(迁移可逆或旧版本可忽略新表)」不成立:`user_version` 是单向整数,旧二进制看到更高版本**不会报错**,会静默在陌生 schema 上读写,把可恢复事故变成不可恢复。「向前兼容」一词也用反了。
|
||||
**修复**:启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(留 `--allow-newer` 旗标);迁移策略声明为「仅追加式」(禁止删列/重命名/改类型);升级 SOP 第一步固定为 `pure-note backup`。
|
||||
**修复**:启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(留 `--allow-newer` 旗标);迁移策略声明为「仅追加式」(禁止删列/重命名/改类型);升级 SOP 第一步固定为 `pn backup`。
|
||||
|
||||
### P1-14 测试策略缺失(§12 / §9.3)
|
||||
|
||||
@@ -149,7 +149,7 @@ T10 点名了 API/RSS/sitemap/图片,唯独漏了 tags;API 表里只写「
|
||||
### P1-15 备份文件敏感性与恢复 SOP(§10.3)
|
||||
|
||||
备份 = 全量私有笔记 + 图片 BLOB + 口令哈希 + 会话哈希的**明文副本**。crontab 若以 root 运行、umask 022,备份文件 0644,同机任意本地用户可读全部私密内容;rclone 异地同步也是明文。恢复步骤未要求删除残留 `-wal`/`-shm`(旧 WAL 会污染还原后的库);「备份保留 30 天」与「删除笔记」的数据语义关系未声明。
|
||||
**修复**:备份文件 chmod 0600 / 专用低权用户;异地前强制加密(age / rclone crypt);恢复 SOP 补「删除 `pure-note.db-wal` 与 `-shm`」;声明备份保留与删除的关系。
|
||||
**修复**:备份文件 chmod 0600 / 专用低权用户;异地前强制加密(age / rclone crypt);恢复 SOP 补「删除 `pn.db-wal` 与 `-shm`」;声明备份保留与删除的关系。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user