// 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 }