Files
pure-note/internal/store/store.go
T
wangairnan 247e88c4fb 后端基础层:模块定义、配置解析、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 同源校验
2026-09-08 08:14:12 +08:00

155 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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() }