From d5cae6efa97a600edcbb78bc5bef465f462bd00e Mon Sep 17 00:00:00 2001 From: didier amyot Date: Thu, 4 Jun 2026 09:23:59 -0400 Subject: [PATCH] Add sqlite write permission check and preStart chown - newSqliteStore now checks directory writability before opening - Immediate SELECT 1 ping catches readonly DB early - Systemd preStart chown ensures db file ownership is correct - Fixes: SQLITE_READONLY_CANTINIT (1032) --- flake.nix | 6 ++++++ sqlite.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/flake.nix b/flake.nix index 79be0fe..ef4c06a 100644 --- a/flake.nix +++ b/flake.nix @@ -122,6 +122,12 @@ wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; + preStart = '' + if [ -f "${builtins.toString cfg.sqlitePath}" ]; then + chown st-pierre:st-pierre "${builtins.toString cfg.sqlitePath}" + fi + ''; + serviceConfig = { User = "st-pierre"; Group = "st-pierre"; diff --git a/sqlite.go b/sqlite.go index 18c160f..0b061b7 100644 --- a/sqlite.go +++ b/sqlite.go @@ -6,6 +6,7 @@ import ( "embed" "encoding/json" "fmt" + "os" "strings" "github.com/pressly/goose/v3" @@ -20,6 +21,16 @@ type sqliteStore struct { } func newSqliteStore(path string) (*sqliteStore, error) { + dir := path + if idx := strings.LastIndexByte(path, '/'); idx > 0 { + dir = path[:idx] + if fi, err := os.Stat(dir); err != nil { + return nil, fmt.Errorf("sqlite dir %s: %w", dir, err) + } else if fi.Mode().Perm()&0200 == 0 { + return nil, fmt.Errorf("sqlite dir %s is not writable (mode %o)", dir, fi.Mode().Perm()) + } + } + db, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) @@ -27,6 +38,11 @@ func newSqliteStore(path string) (*sqliteStore, error) { db.SetMaxOpenConns(1) + if _, err := db.Exec("SELECT 1"); err != nil { + db.Close() + return nil, fmt.Errorf("sqlite ping: %w", err) + } + goose.SetBaseFS(embedMigrations) if err := goose.SetDialect("sqlite3"); err != nil {