后端基础层:模块定义、配置解析、SQLite 存储层、Argon2id 认证与 Markdown 渲染
- internal/config:serve/init/backup/gc 子命令参数解析(--dev 强制 loopback 守卫) - internal/store:user_version 版本化迁移(仅追加式 + 越界拒启 + --allow-newer)、 笔记/图片/引用/会话/设置 DAO、并集可见性查询、VACUUM INTO 在线备份、 回收站 30 天 + 孤儿图 7 天宽限 gc - internal/auth:Argon2id PHC 串(m=19456,t=2,p=1)、256bit token 与 SHA-256 摘要 - internal/markdown:goldmark(默认转义)+ bluemonday 双保险,摘要纯文本提取 - internal/middleware:安全头(CSP/HSTS/nosniff 等)、错误日志、 有界令牌桶限流(per-IP 桶上限 + TTL 逐出)、Origin/Referer 同源校验
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// Package store SQLite 打开、版本化迁移与全部数据访问。
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// bgCtx DAO 内部使用的后台 context(个人规模查询均为快速查询)。
|
||||
var bgCtx = context.Background()
|
||||
|
||||
// ErrSchemaNewer 数据库 schema 版本超出代码支持范围(§10.4 启动守卫)。
|
||||
var ErrSchemaNewer = errors.New("数据库 schema 版本高于本程序支持的最高版本,拒绝启动(可用 --allow-newer 显式放行)")
|
||||
|
||||
// migrations 仅追加式:禁止删列/重命名/改类型(§6.2)。
|
||||
var migrations = []string{
|
||||
// v1: 初始 schema(§6.1 DDL)
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'private'
|
||||
CHECK (status IN ('public','private')),
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
deleted_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_public
|
||||
ON notes (status, pinned, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_deleted ON notes (deleted_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sha256 TEXT NOT NULL UNIQUE,
|
||||
mime TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_refs (
|
||||
image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (image_id, note_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_image_refs_note ON image_refs (note_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
csrf_token TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`,
|
||||
}
|
||||
|
||||
// MaxSchemaVersion 代码支持的最高 schema 版本。
|
||||
var MaxSchemaVersion = len(migrations)
|
||||
|
||||
// Store 封装 *sql.DB;单连接串行写(§6.2 并发策略)。
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Open 打开数据库并执行迁移。dbPath 为文件绝对/相对路径。
|
||||
func Open(dbPath string, allowNewer bool) (*Store, error) {
|
||||
// SQLite URI 不接受相对路径
|
||||
abs, err := filepath.Abs(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsn := url.URL{
|
||||
Scheme: "file",
|
||||
Path: abs,
|
||||
RawQuery: "_pragma=journal_mode(WAL)" +
|
||||
"&_pragma=busy_timeout(5000)" +
|
||||
"&_pragma=foreign_keys(1)" +
|
||||
"&_pragma=synchronous(NORMAL)",
|
||||
}
|
||||
db, err := sql.Open("sqlite", dsn.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 单写者串行化(§6.2)
|
||||
db.SetMaxOpenConns(1)
|
||||
s := &Store{db: db}
|
||||
if err := s.migrate(allowNewer); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Store) migrate(allowNewer bool) error {
|
||||
var v int
|
||||
if err := s.db.QueryRow("PRAGMA user_version").Scan(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
if v > MaxSchemaVersion && !allowNewer {
|
||||
return fmt.Errorf("%w: 库版本 %d > 支持上限 %d", ErrSchemaNewer, v, MaxSchemaVersion)
|
||||
}
|
||||
for i := v; i < MaxSchemaVersion; i++ {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(migrations[i]); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("执行迁移 v%d 失败: %w", i+1, err)
|
||||
}
|
||||
// PRAGMA 不能参数化,i 为内部 int 常量,无注入面。
|
||||
if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", i+1)); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchemaVersion 返回当前库版本。
|
||||
func (s *Store) SchemaVersion() (int, error) {
|
||||
var v int
|
||||
err := s.db.QueryRow("PRAGMA user_version").Scan(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Close 关闭连接。
|
||||
func (s *Store) Close() error { return s.db.Close() }
|
||||
|
||||
// DB 暴露底层连接(仅备份等维护路径使用)。
|
||||
func (s *Store) DB() *sql.DB { return s.db }
|
||||
|
||||
func nowUnix() int64 { return time.Now().Unix() }
|
||||
Reference in New Issue
Block a user