二进制及自命令命名统一简化
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ internal/webui/dist/*
|
|||||||
!internal/webui/dist/.gitkeep
|
!internal/webui/dist/.gitkeep
|
||||||
|
|
||||||
# 本地构建的二进制
|
# 本地构建的二进制
|
||||||
/pure-note
|
/pn
|
||||||
/pure-note-linux-*
|
/pn-linux-*
|
||||||
|
|
||||||
# 系统文件
|
# 系统文件
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# 常用目标:
|
# 常用目标:
|
||||||
# make web 构建前端(npm ci + vite build → web/dist)
|
# make web 构建前端(npm ci + vite build → web/dist)
|
||||||
# make sync-assets 拷贝 web/dist → internal/webui/dist(go:embed 约束,§8.3-1)
|
# make sync-assets 拷贝 web/dist → internal/webui/dist(go:embed 约束,§8.3-1)
|
||||||
# make build 全量构建(前端 + 单二进制)
|
# make build 全量构建(前端 + 单二进制 pn)
|
||||||
# make linux 交叉编译 linux/amd64
|
# make linux 交叉编译 linux/amd64
|
||||||
# make test Go 测试 + 前端测试
|
# make test Go 测试 + 前端测试
|
||||||
# make smoke 真实构建冒烟测试(§8.3-6)
|
# make smoke 真实构建冒烟测试(§8.3-6)
|
||||||
@@ -27,11 +27,11 @@ sync-assets:
|
|||||||
touch internal/webui/dist/.gitkeep
|
touch internal/webui/dist/.gitkeep
|
||||||
|
|
||||||
build: web sync-assets
|
build: web sync-assets
|
||||||
CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o pure-note ./cmd/pure-note
|
CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o pn ./cmd/pn
|
||||||
|
|
||||||
linux: web sync-assets
|
linux: web sync-assets
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="$(LDFLAGS)" \
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="$(LDFLAGS)" \
|
||||||
-o pure-note-linux-amd64 ./cmd/pure-note
|
-o pn-linux-amd64 ./cmd/pn
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
@@ -50,4 +50,4 @@ vulncheck:
|
|||||||
govulncheck ./...
|
govulncheck ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f pure-note pure-note-linux-amd64
|
rm -f pn pn-linux-amd64
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- **博客**:首页(置顶优先 + 分页 + 标签云)、详情(上一篇/下一篇)、标签页、RSS、sitemap、SEO meta 注入(仅公开内容)
|
- **博客**:首页(置顶优先 + 分页 + 标签云)、详情(上一篇/下一篇)、标签页、RSS、sitemap、SEO meta 注入(仅公开内容)
|
||||||
- **管理**:单管理员口令登录(Argon2id)、会话 7 天滑动续期(CSRF 轮换保持不变)、防爆破限流(per-IP + per-账号)、改密
|
- **管理**:单管理员口令登录(Argon2id)、会话 7 天滑动续期(CSRF 轮换保持不变)、防爆破限流(per-IP + per-账号)、改密
|
||||||
- **回收站**:删除 = 软删除,30 天内可恢复,`gc` 到期物理清除
|
- **回收站**:删除 = 软删除,30 天内可恢复,`gc` 到期物理清除
|
||||||
- **运维**:`init` / `reset-password`(重设口令并吊销全部会话)/ `serve` / `backup`(在线一致快照)/ `gc`(默认 dry-run)/ `version`
|
- **运维**:`init` / `passwd`(重设口令并吊销全部会话)/ `start` / `backup`(在线一致快照)/ `gc`(默认 dry-run)/ `version`(亦可 `-v`)
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
make build
|
make build
|
||||||
|
|
||||||
# 2. 初始化(设置管理员口令与站点标题)
|
# 2. 初始化(设置管理员口令与站点标题)
|
||||||
./pure-note init
|
./pn init
|
||||||
# 非交互:PN_ADMIN_PASSWORD=xxx PN_SITE_TITLE=yyy ./pure-note init
|
# 非交互:PN_ADMIN_PASSWORD=xxx PN_SITE_TITLE=yyy ./pn init
|
||||||
|
|
||||||
# 3. 启动
|
# 3. 启动
|
||||||
./pure-note serve --addr 127.0.0.1:8080
|
./pn start --addr 127.0.0.1:8080
|
||||||
|
|
||||||
# 4. 访问
|
# 4. 访问
|
||||||
# 博客: http://127.0.0.1:8080/
|
# 博客: http://127.0.0.1:8080/
|
||||||
@@ -34,7 +34,7 @@ make build
|
|||||||
开发模式(本机调试,Cookie 允许非 Secure,仅限 loopback 监听):
|
开发模式(本机调试,Cookie 允许非 Secure,仅限 loopback 监听):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./pure-note serve --dev --addr 127.0.0.1:8080
|
./pn start --dev --addr 127.0.0.1:8080
|
||||||
# 前端热重载(另开终端):
|
# 前端热重载(另开终端):
|
||||||
cd web && npm install && npm run dev # /api 代理到 127.0.0.1:8080
|
cd web && npm install && npm run dev # /api 代理到 127.0.0.1:8080
|
||||||
```
|
```
|
||||||
@@ -42,8 +42,8 @@ cd web && npm install && npm run dev # /api 代理到 127.0.0.1:8080
|
|||||||
忘记口令(CLI 可达即具备服务器权限,可直接重设;重设后全部会话被吊销):
|
忘记口令(CLI 可达即具备服务器权限,可直接重设;重设后全部会话被吊销):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./pure-note reset-password
|
./pn passwd
|
||||||
# 非交互:PN_ADMIN_PASSWORD=xxx ./pure-note reset-password
|
# 非交互:PN_ADMIN_PASSWORD=xxx ./pn passwd
|
||||||
```
|
```
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
@@ -60,25 +60,25 @@ slug 冲突策略、设置白名单(永不泄露口令哈希)、meta 注入
|
|||||||
## 部署(生产)
|
## 部署(生产)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make linux # 产出 pure-note-linux-amd64
|
make linux # 产出 pn-linux-amd64
|
||||||
sudo cp pure-note-linux-amd64 /opt/pure-note/pure-note
|
sudo cp pn-linux-amd64 /opt/pure-note/pn
|
||||||
sudo -u purenote ./pure-note init --data-dir /opt/pure-note/data
|
sudo -u purenote ./pn init --data-dir /opt/pure-note/data
|
||||||
sudo cp deploy/pure-note.service deploy/pure-note-maint.{service,timer} /etc/systemd/system/
|
sudo cp deploy/pn.service deploy/pn-maint.{service,timer} /etc/systemd/system/
|
||||||
sudo systemctl enable --now pure-note pure-note-maint.timer
|
sudo systemctl enable --now pn pn-maint.timer
|
||||||
```
|
```
|
||||||
|
|
||||||
反向代理用 Caddy(自动 TLS),示例见 [deploy/Caddyfile](deploy/Caddyfile);
|
反向代理用 Caddy(自动 TLS),示例见 [deploy/Caddyfile](deploy/Caddyfile);
|
||||||
启动需带 `--behind-proxy`(取 X-Forwarded-For 最右条目)。
|
启动需带 `--behind-proxy`(取 X-Forwarded-For 最右条目)。
|
||||||
|
|
||||||
**升级 SOP**:`pure-note backup` → 停服 → 换二进制 → 起服(迁移自动执行;
|
**升级 SOP**:`pn backup` → 停服 → 换二进制 → 起服(迁移自动执行;
|
||||||
库版本高于代码支持范围时拒绝启动,`--allow-newer` 显式放行)。
|
库版本高于代码支持范围时拒绝启动,`--allow-newer` 显式放行)。
|
||||||
|
|
||||||
**恢复 SOP**:停服 → 删除 `data/pure-note.db-wal` 与 `-shm` → 以备份文件替换 `pure-note.db` → 起服。
|
**恢复 SOP**:停服 → 删除 `data/pn.db-wal` 与 `-shm` → 以备份文件替换 `pn.db` → 起服。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
cmd/pure-note/ CLI(serve/init/reset-password/backup/gc/version)
|
cmd/pn/ CLI(start/init/passwd/backup/gc/version)
|
||||||
internal/config/ 命令行解析(--dev loopback 守卫)
|
internal/config/ 命令行解析(--dev loopback 守卫)
|
||||||
internal/store/ SQLite(modernc 纯 Go 驱动):迁移(user_version) + DAO + 备份 + gc
|
internal/store/ SQLite(modernc 纯 Go 驱动):迁移(user_version) + DAO + 备份 + gc
|
||||||
internal/auth/ Argon2id(PHC) + 随机 token
|
internal/auth/ Argon2id(PHC) + 随机 token
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// pure-note 单二进制入口:serve / init / reset-password / backup / gc / version(§7.6)。
|
// pn 单二进制入口:start / init / passwd / backup / gc / version(§7.6)。
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -38,18 +38,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "serve":
|
case "start":
|
||||||
err = runServe(os.Args[2:])
|
err = runStart(os.Args[2:])
|
||||||
case "init":
|
case "init":
|
||||||
err = runInit(os.Args[2:])
|
err = runInit(os.Args[2:])
|
||||||
case "reset-password":
|
case "passwd":
|
||||||
err = runResetPassword(os.Args[2:])
|
err = runResetPassword(os.Args[2:])
|
||||||
case "backup":
|
case "backup":
|
||||||
err = runBackup(os.Args[2:])
|
err = runBackup(os.Args[2:])
|
||||||
case "gc":
|
case "gc":
|
||||||
err = runGC(os.Args[2:])
|
err = runGC(os.Args[2:])
|
||||||
case "version":
|
case "version", "-v", "--version":
|
||||||
fmt.Printf("pure-note %s (commit=%s, built=%s)\n", version, commit, buildDate)
|
fmt.Printf("pn %s (commit=%s, built=%s)\n", version, commit, buildDate)
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
usage()
|
usage()
|
||||||
default:
|
default:
|
||||||
@@ -64,15 +64,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
fmt.Print(`pure-note — 极简高安全私人笔记 + 博客
|
fmt.Print(`pn — 极简高安全私人笔记 + 博客
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
pure-note serve [--addr :8080] [--behind-proxy] [--dev] [--allow-newer]
|
pn start [--addr :8080] [--behind-proxy] [--dev] [--allow-newer]
|
||||||
pure-note init
|
pn init
|
||||||
pure-note reset-password # 重设管理员口令并吊销全部会话
|
pn passwd # 重设管理员口令并吊销全部会话
|
||||||
pure-note backup [输出路径]
|
pn backup [输出路径]
|
||||||
pure-note gc [--commit] # 默认 dry-run
|
pn gc [--commit] # 默认 dry-run
|
||||||
pure-note version
|
pn version # 亦可 -v / --version
|
||||||
|
|
||||||
# --data-dir 可指定数据目录,默认 ./data
|
# --data-dir 可指定数据目录,默认 ./data
|
||||||
`)
|
`)
|
||||||
@@ -98,9 +98,9 @@ func newLogger(cfg *config.Config) *slog.Logger {
|
|||||||
return slog.New(h)
|
return slog.New(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runServe 启动 HTTP 服务(§7.2 服务器参数 + §10.5 优雅停机)。
|
// runStart 启动 HTTP 服务(§7.2 服务器参数 + §10.5 优雅停机)。
|
||||||
func runServe(args []string) error {
|
func runStart(args []string) error {
|
||||||
cfg, err := config.ParseServe(args)
|
cfg, err := config.ParseStart(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func runServe(args []string) error {
|
|||||||
if _, ok, err := st.GetSetting("admin_password_hash"); err != nil {
|
if _, ok, err := st.GetSetting("admin_password_hash"); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
return errors.New("尚未初始化管理员口令:请先执行 `pure-note init --data-dir " + cfg.DataDir + "`")
|
return errors.New("尚未初始化管理员口令:请先执行 `pn init --data-dir " + cfg.DataDir + "`")
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := httpapi.New(st, cfg, log)
|
srv := httpapi.New(st, cfg, log)
|
||||||
@@ -157,7 +157,7 @@ func runServe(args []string) error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
log.Info("pure-note 已启动", "addr", cfg.Addr, "data_dir", cfg.DataDir, "dev", cfg.Dev)
|
log.Info("pn 已启动", "addr", cfg.Addr, "data_dir", cfg.DataDir, "dev", cfg.Dev)
|
||||||
errCh <- httpServer.ListenAndServe()
|
errCh <- httpServer.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ func runInit(args []string) error {
|
|||||||
if err := st.SetSetting(store.KeySiteTitle, title); err != nil {
|
if err := st.SetSetting(store.KeySiteTitle, title); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("初始化完成。现在可以启动:pure-note serve --data-dir", cfg.DataDir)
|
fmt.Println("初始化完成。现在可以启动:pn start --data-dir", cfg.DataDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ func runInit(args []string) error {
|
|||||||
// 口令来源与 init 一致:环境变量 PN_ADMIN_PASSWORD 或交互输入(避免出现在 ps 参数里)。
|
// 口令来源与 init 一致:环境变量 PN_ADMIN_PASSWORD 或交互输入(避免出现在 ps 参数里)。
|
||||||
// 重设成功后吊销全部会话,强制所有端重新登录。
|
// 重设成功后吊销全部会话,强制所有端重新登录。
|
||||||
func runResetPassword(args []string) error {
|
func runResetPassword(args []string) error {
|
||||||
cfg, _, err := config.ParseMaint("reset-password", args)
|
cfg, _, err := config.ParseMaint("passwd", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ func runBackup(args []string) error {
|
|||||||
dest = rest[0]
|
dest = rest[0]
|
||||||
}
|
}
|
||||||
if dest == "" {
|
if dest == "" {
|
||||||
dest = filepath.Join(cfg.DataDir, fmt.Sprintf("pure-note-backup-%s.db", time.Now().Format("20060102-150405")))
|
dest = filepath.Join(cfg.DataDir, fmt.Sprintf("pn-backup-%s.db", time.Now().Format("20060102-150405")))
|
||||||
}
|
}
|
||||||
st, err := store.Open(cfg.DBPath(), false)
|
st, err := store.Open(cfg.DBPath(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# /etc/systemd/system/pn-maint.service
|
||||||
|
# 每日维护:先 gc(--commit)后备份(§10.3)
|
||||||
|
[Unit]
|
||||||
|
Description=Pure Note daily maintenance (gc + backup)
|
||||||
|
After=pn.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=purenote
|
||||||
|
ExecStart=/opt/pure-note/pn gc --data-dir /opt/pure-note/data --commit
|
||||||
|
ExecStart=/opt/pure-note/pn backup --data-dir /opt/pure-note/data /backup/pn-%%F.db
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# /etc/systemd/system/pure-note-maint.timer
|
# /etc/systemd/system/pn-maint.timer
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Daily Pure Note maintenance (gc + backup)
|
Description=Daily Pure Note maintenance (gc + backup)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# /etc/systemd/system/pure-note.service
|
# /etc/systemd/system/pn.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pure Note - minimalist high-security private notes + blog
|
Description=Pure Note - minimalist high-security private notes + blog
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -6,7 +6,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=purenote
|
User=purenote
|
||||||
WorkingDirectory=/opt/pure-note
|
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
|
--data-dir /opt/pure-note/data --behind-proxy
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# /etc/systemd/system/pure-note-maint.service
|
|
||||||
# 每日维护:先 gc(--commit)后备份(§10.3)
|
|
||||||
[Unit]
|
|
||||||
Description=Pure Note daily maintenance (gc + backup)
|
|
||||||
After=pure-note.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=purenote
|
|
||||||
ExecStart=/opt/pure-note/pure-note gc --data-dir /opt/pure-note/data --commit
|
|
||||||
ExecStart=/opt/pure-note/pure-note backup --data-dir /opt/pure-note/data /backup/pure-note-%%F.db
|
|
||||||
+4
-4
@@ -27,7 +27,7 @@
|
|||||||
| 回收站恢复用例;gc dry-run→commit;7 天内孤儿图片不删 | ✅ | `TestTrashLifecycleAndGC` + CLI 实测(dry-run 报告、commit 执行、宽限期保留) |
|
| 回收站恢复用例;gc dry-run→commit;7 天内孤儿图片不删 | ✅ | `TestTrashLifecycleAndGC` + CLI 实测(dry-run 报告、commit 执行、宽限期保留) |
|
||||||
| user_version 越界启动被拒;--allow-newer 放行 | ✅ | `TestMigrationNewerRejected` + CLI 实测(拒启 exit=1;--allow-newer 后 /api/health 200) |
|
| 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 注入、安全头、私有不可见) |
|
| 构建冒烟测试通过 | ✅ | `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 测试策略落地
|
## 三、§13 测试策略落地
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@
|
|||||||
## 六、交付物清单
|
## 六、交付物清单
|
||||||
|
|
||||||
```
|
```
|
||||||
pure-note(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
pn(单二进制,CGO_ENABLED=0,go:embed 前端产物)
|
||||||
├── cmd/pure-note/ serve / init / reset-password / backup / gc / version
|
├── cmd/pn/ start / init / passwd / backup / gc / version
|
||||||
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
|
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
|
||||||
├── web/ React 19 + Vite 8 + Tailwind 4(vitest 6 用例)
|
├── 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)
|
├── scripts/smoke.sh 构建冒烟(make smoke)
|
||||||
├── Makefile / README.md / .gitignore
|
├── Makefile / README.md / .gitignore
|
||||||
└── docs/{design.md, review-round1.md, decisions.md, acceptance.md}
|
└── 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)全部满足,无升级必要 |
|
| 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 解析到的版本为准」——解析结果与设计核实值完全一致 |
|
| 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 生态) |
|
| 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` 实测 |
|
| 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)
|
│ HTTPS (Caddy 自动 TLS, 反代, 追加密名 XFF)
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────┐
|
┌──────────────────────────┐
|
||||||
│ pure-note 单一二进制 (Go) │
|
│ pn 单一二进制 (Go) │
|
||||||
│ │
|
│ │
|
||||||
│ net/http + ServeMux │
|
│ net/http + ServeMux │
|
||||||
│ ├─ 公共 API /api/* │──┐
|
│ ├─ 公共 API /api/* │──┐
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
│ database/sql (SetMaxOpenConns=1)
|
│ database/sql (SetMaxOpenConns=1)
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────┐
|
┌──────────────────────────┐
|
||||||
│ data/pure-note.db (SQLite)│
|
│ data/pn.db (SQLite) │
|
||||||
│ notes(含 deleted_at) / │
|
│ notes(含 deleted_at) / │
|
||||||
│ images(BLOB) / image_refs /│
|
│ images(BLOB) / image_refs /│
|
||||||
│ sessions / settings │
|
│ sessions / settings │
|
||||||
@@ -176,8 +176,8 @@ pure-note/
|
|||||||
│ ├── design.md # 本文档(v1.1)
|
│ ├── design.md # 本文档(v1.1)
|
||||||
│ └── review-round1.md # 第 1 轮审评报告(修订依据,留档)
|
│ └── review-round1.md # 第 1 轮审评报告(修订依据,留档)
|
||||||
├── cmd/
|
├── cmd/
|
||||||
│ └── pure-note/
|
│ └── pn/
|
||||||
│ └── main.go # 入口 + 命令行(serve/init/reset-password/backup/gc/version)
|
│ └── main.go # 入口 + 命令行(start/init/passwd/backup/gc/version)
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── config/ # 环境变量/flag 解析
|
│ ├── config/ # 环境变量/flag 解析
|
||||||
│ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO
|
│ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO
|
||||||
@@ -200,8 +200,8 @@ pure-note/
|
|||||||
│ ├── features/ # notes / admin 业务 hooks
|
│ ├── features/ # notes / admin 业务 hooks
|
||||||
│ └── pages/ # Home / Note / Tags / AdminLogin / AdminList / AdminTrash / AdminEdit
|
│ └── pages/ # Home / Note / Tags / AdminLogin / AdminList / AdminTrash / AdminEdit
|
||||||
└── deploy/
|
└── deploy/
|
||||||
├── pure-note.service # systemd 服务单元
|
├── pn.service # systemd 服务单元
|
||||||
├── pure-note-maint.timer # 每日 gc + 备份 timer
|
├── pn-maint.timer # 每日 gc + 备份 timer
|
||||||
└── Caddyfile # 反代示例
|
└── Caddyfile # 反代示例
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ pure-note/
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 连接串(DSN)统一注入以下 pragma:
|
-- 连接串(DSN)统一注入以下 pragma:
|
||||||
-- file:data/pure-note.db?_pragma=journal_mode(WAL)
|
-- file:data/pn.db?_pragma=journal_mode(WAL)
|
||||||
-- &_pragma=busy_timeout(5000)
|
-- &_pragma=busy_timeout(5000)
|
||||||
-- &_pragma=foreign_keys(1)
|
-- &_pragma=foreign_keys(1)
|
||||||
-- &_pragma=synchronous(NORMAL)
|
-- &_pragma=synchronous(NORMAL)
|
||||||
@@ -281,7 +281,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
|||||||
| 迁移 | `PRAGMA user_version` 版本化,顺序执行内嵌迁移脚本;**仅追加式**(禁止删列/重命名/改类型);启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(`--allow-newer` 显式放行,§10.4) |
|
| 迁移 | `PRAGMA user_version` 版本化,顺序执行内嵌迁移脚本;**仅追加式**(禁止删列/重命名/改类型);启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(`--allow-newer` 显式放行,§10.4) |
|
||||||
| 并发 | `db.SetMaxOpenConns(1)` — 单写者串行化,WAL 下个人规模足够;规模上来后按「多读者连接池」扩展 |
|
| 并发 | `db.SetMaxOpenConns(1)` — 单写者串行化,WAL 下个人规模足够;规模上来后按「多读者连接池」扩展 |
|
||||||
|
|
||||||
### 6.3 数据清理(`pure-note gc`)
|
### 6.3 数据清理(`pn gc`)
|
||||||
|
|
||||||
默认 `--dry-run` 只输出清理计划,`--commit` 才执行;systemd timer 每日调用一次(§10.3):
|
默认 `--dry-run` 只输出清理计划,`--commit` 才执行;systemd timer 每日调用一次(§10.3):
|
||||||
|
|
||||||
@@ -348,14 +348,14 @@ SecurityHeaders(含 HSTS,§9.2)
|
|||||||
|
|
||||||
### 7.3 认证与会话
|
### 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「用户名或密码错误」不泄露差异;
|
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 + 根路径约束);
|
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 匹配;
|
4. **CSRF**:会话行内存随机 `csrf_token`,登录响应下发、前端仅存内存并附到所有 `/api/admin/*` 变更请求的 `X-CSRF-Token` 头;**刷新页面后经 `GET /api/me` 重取**;明确禁止任何 localStorage 持久化;服务端同时校验 Origin/Referer 与 Host 匹配;
|
||||||
5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感);
|
5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感);
|
||||||
6. **登出**:删除会话行;
|
6. **登出**:删除会话行;
|
||||||
7. **改密**:`POST /api/admin/password` 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制);
|
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 时允许启动,防止误部署到生产**。
|
9. **开发模式**:`--dev` 允许非 Secure Cookie(否则 `__Host-` 在 `http://局域网IP` 下无法登录);**强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产**。
|
||||||
|
|
||||||
### 7.4 图片上传与访问
|
### 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` |
|
| `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` |
|
||||||
| `pure-note init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
| `pn init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 |
|
||||||
| `pure-note reset-password` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 |
|
| `pn passwd` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 |
|
||||||
| `pure-note backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
| `pn backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 |
|
||||||
| `pure-note gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
| `pn gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) |
|
||||||
| `pure-note version` | 版本号(build 时注入 commit/时间) |
|
| `pn version` | 版本号(build 时注入 commit/时间);亦支持 `-v` / `--version` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -522,20 +522,20 @@ sync-assets:
|
|||||||
|
|
||||||
build: web sync-assets
|
build: web sync-assets
|
||||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
||||||
-o pure-note ./cmd/pure-note
|
-o pn ./cmd/pn
|
||||||
|
|
||||||
linux: web sync-assets
|
linux: web sync-assets
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
|
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
|
air & cd web && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 10.2 运行
|
### 10.2 运行
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# deploy/pure-note.service
|
# deploy/pn.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pure Note
|
Description=Pure Note
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -543,7 +543,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=purenote
|
User=purenote
|
||||||
WorkingDirectory=/opt/pure-note
|
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
|
--data-dir /opt/pure-note/data --behind-proxy
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
@@ -571,7 +571,7 @@ example.com {
|
|||||||
**调度(systemd timer,替代 root cron)**
|
**调度(systemd timer,替代 root cron)**
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# deploy/pure-note-maint.timer
|
# deploy/pn-maint.timer
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Daily Pure Note maintenance (gc + backup)
|
Description=Daily Pure Note maintenance (gc + backup)
|
||||||
|
|
||||||
@@ -583,7 +583,7 @@ Persistent=true
|
|||||||
WantedBy=timers.target
|
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**(内含全部私有笔记、图片、口令哈希、会话哈希的明文副本);
|
- `/backup` 目录属主 `purenote`、权限 0700;备份文件**chmod 0600**(内含全部私有笔记、图片、口令哈希、会话哈希的明文副本);
|
||||||
- 保留 30 天;**语义声明**:备份保留期内被删除的数据物理上仍存在于备份中;
|
- 保留 30 天;**语义声明**:备份保留期内被删除的数据物理上仍存在于备份中;
|
||||||
- 异地同步(可选):`rclone` 远端启用 **crypt**(或先 `age` 加密再上传),禁止明文上云;
|
- 异地同步(可选):`rclone` 远端启用 **crypt**(或先 `age` 加密再上传),禁止明文上云;
|
||||||
- **恢复 SOP**:停服 → 删除 `data/pure-note.db-wal` 与 `-shm`(残留会污染还原)→ 以备份文件替换 `pure-note.db` → 起服。
|
- **恢复 SOP**:停服 → 删除 `data/pn.db-wal` 与 `-shm`(残留会污染还原)→ 以备份文件替换 `pn.db` → 起服。
|
||||||
|
|
||||||
### 10.4 升级
|
### 10.4 升级
|
||||||
|
|
||||||
1. **升级前第一步固定为 `pure-note backup`**(写入 SOP);
|
1. **升级前第一步固定为 `pn backup`**(写入 SOP);
|
||||||
2. 停服 → 替换二进制 → 起服触发迁移:迁移仅追加式(不删列/重命名/改类型),`user_version` 顺序递增;
|
2. 停服 → 替换二进制 → 起服触发迁移:迁移仅追加式(不删列/重命名/改类型),`user_version` 顺序递增;
|
||||||
3. **启动守卫**:代码校验 `user_version ≤ 本版本支持的最高版本`,超出**拒绝启动**并提示,`--allow-newer` 显式放行(防止旧二进制静默在陌生 schema 上读写);
|
3. **启动守卫**:代码校验 `user_version ≤ 本版本支持的最高版本`,超出**拒绝启动**并提示,`--allow-newer` 显式放行(防止旧二进制静默在陌生 schema 上读写);
|
||||||
4. 回滚:若迁移尚未执行(版本一致)可直接换回旧二进制;若已执行且不满足追加式约束,回滚 = 从步骤 1 的备份还原数据文件后再换回旧二进制。
|
4. 回滚:若迁移尚未执行(版本一致)可直接换回旧二进制;若已执行且不满足追加式约束,回滚 = 从步骤 1 的备份还原数据文件后再换回旧二进制。
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ T10 点名了 API/RSS/sitemap/图片,唯独漏了 tags;API 表里只写「
|
|||||||
### P1-13 升级回滚不严谨(§10.4)
|
### P1-13 升级回滚不严谨(§10.4)
|
||||||
|
|
||||||
「回滚 = 换回旧二进制(迁移可逆或旧版本可忽略新表)」不成立:`user_version` 是单向整数,旧二进制看到更高版本**不会报错**,会静默在陌生 schema 上读写,把可恢复事故变成不可恢复。「向前兼容」一词也用反了。
|
「回滚 = 换回旧二进制(迁移可逆或旧版本可忽略新表)」不成立:`user_version` 是单向整数,旧二进制看到更高版本**不会报错**,会静默在陌生 schema 上读写,把可恢复事故变成不可恢复。「向前兼容」一词也用反了。
|
||||||
**修复**:启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(留 `--allow-newer` 旗标);迁移策略声明为「仅追加式」(禁止删列/重命名/改类型);升级 SOP 第一步固定为 `pure-note backup`。
|
**修复**:启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(留 `--allow-newer` 旗标);迁移策略声明为「仅追加式」(禁止删列/重命名/改类型);升级 SOP 第一步固定为 `pn backup`。
|
||||||
|
|
||||||
### P1-14 测试策略缺失(§12 / §9.3)
|
### P1-14 测试策略缺失(§12 / §9.3)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ T10 点名了 API/RSS/sitemap/图片,唯独漏了 tags;API 表里只写「
|
|||||||
### P1-15 备份文件敏感性与恢复 SOP(§10.3)
|
### P1-15 备份文件敏感性与恢复 SOP(§10.3)
|
||||||
|
|
||||||
备份 = 全量私有笔记 + 图片 BLOB + 口令哈希 + 会话哈希的**明文副本**。crontab 若以 root 运行、umask 022,备份文件 0644,同机任意本地用户可读全部私密内容;rclone 异地同步也是明文。恢复步骤未要求删除残留 `-wal`/`-shm`(旧 WAL 会污染还原后的库);「备份保留 30 天」与「删除笔记」的数据语义关系未声明。
|
备份 = 全量私有笔记 + 图片 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`」;声明备份保留与删除的关系。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DBPath SQLite 数据库文件路径。
|
// DBPath SQLite 数据库文件路径。
|
||||||
func (c *Config) DBPath() string { return c.DataDir + "/pure-note.db" }
|
func (c *Config) DBPath() string { return c.DataDir + "/pn.db" }
|
||||||
|
|
||||||
func addCommonFlags(fs *flag.FlagSet, c *Config) {
|
func addCommonFlags(fs *flag.FlagSet, c *Config) {
|
||||||
fs.StringVar(&c.DataDir, "data-dir", "./data", "数据目录(SQLite 数据库所在)")
|
fs.StringVar(&c.DataDir, "data-dir", "./data", "数据目录(SQLite 数据库所在)")
|
||||||
@@ -36,10 +36,10 @@ func addServeFlags(fs *flag.FlagSet, c *Config) {
|
|||||||
fs.BoolVar(&c.Dev, "dev", false, "开发模式:允许非 Secure Cookie,仅允许监听 loopback 地址")
|
fs.BoolVar(&c.Dev, "dev", false, "开发模式:允许非 Secure Cookie,仅允许监听 loopback 地址")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseServe 解析 `pure-note serve` 参数。
|
// ParseStart 解析 `pn start` 参数。
|
||||||
func ParseServe(args []string) (*Config, error) {
|
func ParseStart(args []string) (*Config, error) {
|
||||||
c := &Config{}
|
c := &Config{}
|
||||||
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
|
fs := flag.NewFlagSet("start", flag.ContinueOnError)
|
||||||
addServeFlags(fs, c)
|
addServeFlags(fs, c)
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -50,7 +50,7 @@ func ParseServe(args []string) (*Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseInit 解析 `pure-note init` 参数。
|
// ParseInit 解析 `pn init` 参数。
|
||||||
func ParseInit(args []string) (*Config, error) {
|
func ParseInit(args []string) (*Config, error) {
|
||||||
c := &Config{}
|
c := &Config{}
|
||||||
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||||
@@ -61,7 +61,7 @@ func ParseInit(args []string) (*Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseMaint 解析 `pure-note backup` / `pure-note gc` 参数(均只需数据目录)。
|
// ParseMaint 解析 `pn backup` / `pn gc` / `pn passwd` 参数(均只需数据目录)。
|
||||||
// 返回 flag 解析后的剩余位置参数(如 backup 的输出路径)。
|
// 返回 flag 解析后的剩余位置参数(如 backup 的输出路径)。
|
||||||
func ParseMaint(cmd string, args []string) (*Config, []string, error) {
|
func ParseMaint(cmd string, args []string) (*Config, []string, error) {
|
||||||
c := &Config{}
|
c := &Config{}
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import "testing"
|
|||||||
|
|
||||||
func TestDevLoopbackGuard(t *testing.T) {
|
func TestDevLoopbackGuard(t *testing.T) {
|
||||||
// --dev 强制 loopback(§7.3-8)
|
// --dev 强制 loopback(§7.3-8)
|
||||||
if _, err := ParseServe([]string{"--dev", "--addr", ":8080"}); err == nil {
|
if _, err := ParseStart([]string{"--dev", "--addr", ":8080"}); err == nil {
|
||||||
t.Error("--dev + 0.0.0.0 应拒绝启动")
|
t.Error("--dev + 0.0.0.0 应拒绝启动")
|
||||||
}
|
}
|
||||||
if _, err := ParseServe([]string{"--dev", "--addr", "192.168.1.5:8080"}); err == nil {
|
if _, err := ParseStart([]string{"--dev", "--addr", "192.168.1.5:8080"}); err == nil {
|
||||||
t.Error("--dev + 局域网地址应拒绝启动")
|
t.Error("--dev + 局域网地址应拒绝启动")
|
||||||
}
|
}
|
||||||
if _, err := ParseServe([]string{"--dev", "--addr", "127.0.0.1:8080"}); err != nil {
|
if _, err := ParseStart([]string{"--dev", "--addr", "127.0.0.1:8080"}); err != nil {
|
||||||
t.Errorf("--dev + 127.0.0.1 应允许: %v", err)
|
t.Errorf("--dev + 127.0.0.1 应允许: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := ParseServe([]string{"--dev", "--addr", "localhost:8080"}); err != nil {
|
if _, err := ParseStart([]string{"--dev", "--addr", "localhost:8080"}); err != nil {
|
||||||
t.Errorf("--dev + localhost 应允许: %v", err)
|
t.Errorf("--dev + localhost 应允许: %v", err)
|
||||||
}
|
}
|
||||||
// 非 dev 不限制
|
// 非 dev 不限制
|
||||||
if _, err := ParseServe([]string{"--addr", ":8080"}); err != nil {
|
if _, err := ParseStart([]string{"--addr", ":8080"}); err != nil {
|
||||||
t.Errorf("非 dev 任意地址应允许: %v", err)
|
t.Errorf("非 dev 任意地址应允许: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(w, http.StatusInternalServerError, "not_initialized", "尚未初始化管理员口令,请先执行 pure-note init")
|
writeError(w, http.StatusInternalServerError, "not_initialized", "尚未初始化管理员口令,请先执行 pn init")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func newEnvCustom(t *testing.T, global, loginIP, loginAcct *middleware.Limiter)
|
|||||||
loginAcct = middleware.NewLimiter(1e9, 1<<20, 1<<20, time.Minute)
|
loginAcct = middleware.NewLimiter(1e9, 1<<20, 1<<20, time.Minute)
|
||||||
}
|
}
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
st, err := store.Open(dir+"/pure-note.db", false)
|
st, err := store.Open(dir+"/pn.db", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("打开测试库失败: %v", err)
|
t.Fatalf("打开测试库失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// TestMigrationsFromEmpty 空库 → 最新版本;重复打开幂等(§13 迁移组)。
|
// TestMigrationsFromEmpty 空库 → 最新版本;重复打开幂等(§13 迁移组)。
|
||||||
func TestMigrationsFromEmpty(t *testing.T) {
|
func TestMigrationsFromEmpty(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
dbPath := filepath.Join(dir, "pure-note.db")
|
dbPath := filepath.Join(dir, "pn.db")
|
||||||
s1, err := Open(dbPath, false)
|
s1, err := Open(dbPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("首次打开失败: %v", err)
|
t.Fatalf("首次打开失败: %v", err)
|
||||||
@@ -41,7 +41,7 @@ func TestMigrationsFromEmpty(t *testing.T) {
|
|||||||
// TestMigrationNewerRejected user_version 越界拒绝启动;--allow-newer 放行(§10.4)。
|
// TestMigrationNewerRejected user_version 越界拒绝启动;--allow-newer 放行(§10.4)。
|
||||||
func TestMigrationNewerRejected(t *testing.T) {
|
func TestMigrationNewerRejected(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
dbPath := filepath.Join(dir, "pure-note.db")
|
dbPath := filepath.Join(dir, "pn.db")
|
||||||
s1, err := Open(dbPath, false)
|
s1, err := Open(dbPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
BIN="$ROOT/pure-note"
|
BIN="$ROOT/pn"
|
||||||
TMP="$(mktemp -d)"
|
TMP="$(mktemp -d)"
|
||||||
PORT="${SMOKE_PORT:-18099}"
|
PORT="${SMOKE_PORT:-18099}"
|
||||||
BASE="http://127.0.0.1:$PORT"
|
BASE="http://127.0.0.1:$PORT"
|
||||||
@@ -22,7 +22,7 @@ echo "[smoke] 初始化数据目录"
|
|||||||
PN_ADMIN_PASSWORD=smoke-password-123 PN_SITE_TITLE="Smoke 站" "$BIN" init --data-dir "$TMP/data"
|
PN_ADMIN_PASSWORD=smoke-password-123 PN_SITE_TITLE="Smoke 站" "$BIN" init --data-dir "$TMP/data"
|
||||||
|
|
||||||
echo "[smoke] 启动服务 :$PORT"
|
echo "[smoke] 启动服务 :$PORT"
|
||||||
"$BIN" serve --data-dir "$TMP/data" --addr "127.0.0.1:$PORT" --dev >"$TMP/serve.log" 2>&1 &
|
"$BIN" start --data-dir "$TMP/data" --addr "127.0.0.1:$PORT" --dev >"$TMP/serve.log" 2>&1 &
|
||||||
SRV_PID=$!
|
SRV_PID=$!
|
||||||
for _ in $(seq 1 50); do
|
for _ in $(seq 1 50); do
|
||||||
curl -sf "$BASE/api/health" >/dev/null 2>&1 && break
|
curl -sf "$BASE/api/health" >/dev/null 2>&1 && break
|
||||||
|
|||||||
Reference in New Issue
Block a user