Files
emcli/internal/store/store.go
T
2026-06-23 20:16:15 +01:00

92 lines
2.5 KiB
Go

// Package store owns the encrypted SQLite config and read state.
package store
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
_ "modernc.org/sqlite"
)
// Store wraps the database and the field-encryption key.
type Store struct {
db *sql.DB
key []byte
}
// Open opens (creating if needed) the DB at path and applies the schema.
// The store opens LOCKED: call InitKeys (first run) or Unlock before any
// secret read/write.
func Open(path string) (*Store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return nil, fmt.Errorf("create db dir: %w", err)
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
// Pin pool to a single connection so PRAGMA foreign_keys = ON sticks.
// SQLite PRAGMAs are connection-scoped; pool would otherwise create
// new connections without the pragma set.
db.SetMaxOpenConns(1)
if _, err := db.Exec("PRAGMA foreign_keys = ON;"); err != nil {
db.Close()
return nil, err
}
if _, err := db.Exec(schemaSQL); err != nil {
db.Close()
return nil, fmt.Errorf("apply schema: %w", err)
}
s := &Store{db: db}
if err := s.migrate(); err != nil {
db.Close()
return nil, err
}
return s, nil
}
// migrate brings an existing database up to the current schemaVersion. A brand-
// new database (no schema_version yet) already has every column from schemaSQL,
// so it is simply stamped at the current version. Each older version runs its
// forward step. The version gate makes every step idempotent across reopens.
func (s *Store) migrate() error {
v, err := s.GetSetting("schema_version")
if err != nil {
// Fresh database: schemaSQL created all columns already.
return s.SetSetting("schema_version", strconv.Itoa(schemaVersion))
}
ver, _ := strconv.Atoi(v)
if ver < 2 {
if _, err := s.db.Exec(`ALTER TABLE accounts ADD COLUMN from_address TEXT`); err != nil {
return fmt.Errorf("migrate to v2: %w", err)
}
if err := s.SetSetting("schema_version", "2"); err != nil {
return err
}
}
return nil
}
func (s *Store) Close() error { return s.db.Close() }
// DefaultDBPath resolves EMCLI_DB or the per-OS default location.
func DefaultDBPath() (string, error) {
if p := os.Getenv("EMCLI_DB"); p != "" {
return p, nil
}
if runtime.GOOS == "windows" {
if dir := os.Getenv("AppData"); dir != "" {
return filepath.Join(dir, "emcli", "emcli.db"), nil
}
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "emcli", "emcli.db"), nil
}