Files
NixMsg/internal/store/db.go

144 lines
3.4 KiB
Go
Raw Permalink 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
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
const dbFileName = "nixmsg.db"
// DB 持有读写连接与写入队列。
type DB struct {
Write *sql.DB
Read *sql.DB
Queue *Queue
}
// Open 打开数据目录下的库:写连接、读连接池、迁移,并启动简单写入队列。
// 若 nixmsg.db 尚不存在则跳过迁移前备份。
func Open(dataDir, synchronous string) (*DB, error) {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return nil, fmt.Errorf("mkdir data_dir: %w", err)
}
dbPath := filepath.Join(dataDir, dbFileName)
existed, err := fileExists(dbPath)
if err != nil {
return nil, err
}
write, err := OpenWriter(dataDir, synchronous)
if err != nil {
return nil, err
}
if migErr := Migrate(write, dataDir, existed); migErr != nil {
_ = write.Close()
return nil, migErr
}
read, err := OpenReader(dataDir, synchronous)
if err != nil {
_ = write.Close()
return nil, err
}
q := NewQueue(write)
return &DB{Write: write, Read: read, Queue: q}, nil
}
// Close 停止写入队列并关闭连接。
func (d *DB) Close() error {
var first error
if d.Queue != nil {
if err := d.Queue.Close(); err != nil {
first = err
}
}
if d.Read != nil {
if err := d.Read.Close(); err != nil && first == nil {
first = err
}
}
if d.Write != nil {
if err := d.Write.Close(); err != nil && first == nil {
first = err
}
}
return first
}
// OpenWriter 按 DEVELOPMENT 7.7 打开写连接(带 _txlock=immediate,MaxOpenConns=1)。
func OpenWriter(dataDir, synchronous string) (*sql.DB, error) {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return nil, fmt.Errorf("mkdir data_dir: %w", err)
}
dsn, err := buildDSN(dataDir, synchronous, true)
if err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open sqlite writer: %w", err)
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("ping sqlite writer: %w", err)
}
return db, nil
}
// OpenReader 打开读连接池(DSN 不含 _txlock=immediate)。
func OpenReader(dataDir, synchronous string) (*sql.DB, error) {
dsn, err := buildDSN(dataDir, synchronous, false)
if err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open sqlite reader: %w", err)
}
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("ping sqlite reader: %w", err)
}
return db, nil
}
func buildDSN(dataDir, synchronous string, writer bool) (string, error) {
sync := strings.ToUpper(strings.TrimSpace(synchronous))
if sync == "" {
sync = "FULL"
}
if sync != "FULL" && sync != "NORMAL" {
return "", fmt.Errorf("invalid sqlite_synchronous: %s", synchronous)
}
dbPath := filepath.ToSlash(filepath.Join(dataDir, dbFileName))
dsn := fmt.Sprintf(
"file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=synchronous(%s)&_pragma=foreign_keys(ON)&_pragma=secure_delete(ON)",
dbPath,
sync,
)
if writer {
dsn += "&_txlock=immediate"
}
return dsn, nil
}
func fileExists(path string) (bool, error) {
st, err := os.Stat(path)
if err == nil {
if st.IsDir() {
return false, fmt.Errorf("db path is a directory: %s", path)
}
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}