// 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)) } var ver int ver, err = strconv.Atoi(v) if err != nil { return fmt.Errorf("invalid schema_version %q: %w", v, err) } 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 }