feat: bootstrap durable server store

This commit is contained in:
2026-08-31 09:11:58 +00:00
parent 392129c253
commit 8a88a72e62
13 changed files with 822 additions and 12 deletions
+149
View File
@@ -0,0 +1,149 @@
// Package store owns RVBox server persistence and migrations.
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"sync"
"time"
_ "modernc.org/sqlite"
)
var (
ErrUnsafeDataDirectory = errors.New("unsafe server data directory")
ErrAlreadyOpen = errors.New("server data directory is already locked")
)
type Options struct {
DataDir string
BusyTimeout time.Duration
}
type Store struct {
db *sql.DB
unlock func() error
mu sync.Mutex
}
func Open(ctx context.Context, options Options) (*Store, error) {
if !filepath.IsAbs(options.DataDir) || filepath.Clean(options.DataDir) == string(filepath.Separator) {
return nil, ErrUnsafeDataDirectory
}
if options.BusyTimeout <= 0 {
return nil, fmt.Errorf("busy timeout must be positive")
}
if err := ensurePrivateDirectory(options.DataDir); err != nil {
return nil, err
}
for _, child := range []string{"segments", "audit"} {
if err := ensurePrivateDirectory(filepath.Join(options.DataDir, child)); err != nil {
return nil, err
}
}
unlock, err := acquireInstanceLock(filepath.Join(options.DataDir, "server.lock"))
if err != nil {
return nil, err
}
databasePath := filepath.Join(options.DataDir, "rvbox.db")
if err := ensurePrivateFile(databasePath); err != nil {
_ = unlock()
return nil, err
}
query := url.Values{}
query.Add("_defensive", "1")
query.Add("_pragma", "journal_mode(WAL)")
query.Add("_pragma", "foreign_keys(ON)")
query.Add("_pragma", "synchronous(FULL)")
query.Add("_pragma", "busy_timeout("+strconv.FormatInt(options.BusyTimeout.Milliseconds(), 10)+")")
databaseURL := &url.URL{Scheme: "file", Path: filepath.ToSlash(databasePath)}
databaseURL.RawQuery = query.Encode()
dsn := databaseURL.String()
db, err := sql.Open("sqlite", dsn)
if err != nil {
_ = unlock()
return nil, err
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
store := &Store{db: db, unlock: unlock}
if err := db.PingContext(ctx); err != nil {
_ = store.Close()
return nil, fmt.Errorf("open SQLite: %w", err)
}
if err := applyMigrations(ctx, db); err != nil {
_ = store.Close()
return nil, err
}
return store, nil
}
func (store *Store) DB() *sql.DB { return store.db }
func (store *Store) Close() error {
if store == nil {
return nil
}
store.mu.Lock()
defer store.mu.Unlock()
var result error
if store.db != nil {
result = store.db.Close()
store.db = nil
}
if store.unlock != nil {
if err := store.unlock(); result == nil {
result = err
}
store.unlock = nil
}
return result
}
func ensurePrivateFile(path string) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
if err == nil {
return file.Close()
}
if !errors.Is(err, os.ErrExist) {
return err
}
info, err := os.Lstat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%w: %s is not a regular file", ErrUnsafeDataDirectory, path)
}
if info.Mode().Perm()&0o077 != 0 {
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
}
return nil
}
func ensurePrivateDirectory(path string) error {
info, err := os.Lstat(path)
if os.IsNotExist(err) {
if err := os.MkdirAll(path, 0o700); err != nil {
return err
}
info, err = os.Lstat(path)
}
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("%w: %s is not a real directory", ErrUnsafeDataDirectory, path)
}
if info.Mode().Perm()&0o077 != 0 {
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
}
return nil
}