1package sqlite
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8
9 "github.com/charmbracelet/soft-serve/server/backend"
10 "github.com/jmoiron/sqlx"
11 "modernc.org/sqlite"
12 sqlite3 "modernc.org/sqlite/lib"
13)
14
15// Close closes the database.
16func (d *SqliteBackend) Close() error {
17 return d.db.Close()
18}
19
20// init creates the database.
21func (d *SqliteBackend) init() error {
22 return wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
23 if _, err := tx.Exec(sqlCreateSettingsTable); err != nil {
24 return err
25 }
26 if _, err := tx.Exec(sqlCreateUserTable); err != nil {
27 return err
28 }
29 if _, err := tx.Exec(sqlCreatePublicKeyTable); err != nil {
30 return err
31 }
32 if _, err := tx.Exec(sqlCreateRepoTable); err != nil {
33 return err
34 }
35 if _, err := tx.Exec(sqlCreateCollabTable); err != nil {
36 return err
37 }
38
39 // Set default settings.
40 if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "allow_keyless", true); err != nil {
41 return err
42 }
43 if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "anon_access", backend.ReadOnlyAccess.String()); err != nil {
44 return err
45 }
46
47 var init bool
48 if err := tx.Get(&init, "SELECT value FROM settings WHERE key = 'init'"); err != nil && !errors.Is(err, sql.ErrNoRows) {
49 return err
50 }
51
52 // Create default user.
53 if !init {
54 r, err := tx.Exec("INSERT OR IGNORE INTO user (username, admin, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);", "admin", true)
55 if err != nil {
56 return err
57 }
58 userID, err := r.LastInsertId()
59 if err != nil {
60 return err
61 }
62
63 // Add initial keys
64 // Don't use cfg.AdminKeys since it also includes the internal key
65 // used for internal api access.
66 for _, k := range d.cfg.InitialAdminKeys {
67 pk, _, err := backend.ParseAuthorizedKey(k)
68 if err != nil {
69 d.logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
70 continue
71 }
72
73 stmt, err := tx.Prepare(`INSERT INTO public_key (user_id, public_key, updated_at)
74 VALUES (?, ?, CURRENT_TIMESTAMP);`)
75 if err != nil {
76 return err
77 }
78
79 defer stmt.Close() // nolint: errcheck
80 if _, err := stmt.Exec(userID, backend.MarshalAuthorizedKey(pk)); err != nil {
81 return err
82 }
83 }
84 }
85
86 // set init flag
87 if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "init", true); err != nil {
88 return err
89 }
90
91 return nil
92 })
93}
94
95func wrapDbErr(err error) error {
96 if err != nil {
97 if errors.Is(err, sql.ErrNoRows) {
98 return ErrNoRecord
99 }
100 if liteErr, ok := err.(*sqlite.Error); ok {
101 code := liteErr.Code()
102 if code == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY ||
103 code == sqlite3.SQLITE_CONSTRAINT_UNIQUE {
104 return ErrDuplicateKey
105 }
106 }
107 }
108 return err
109}
110
111func wrapTx(db *sqlx.DB, ctx context.Context, fn func(tx *sqlx.Tx) error) error {
112 tx, err := db.BeginTxx(ctx, nil)
113 if err != nil {
114 return fmt.Errorf("failed to begin transaction: %w", err)
115 }
116
117 if err := fn(tx); err != nil {
118 return rollback(tx, err)
119 }
120
121 if err := tx.Commit(); err != nil {
122 if errors.Is(err, sql.ErrTxDone) {
123 // this is ok because whoever did finish the tx should have also written the error already.
124 return nil
125 }
126 return fmt.Errorf("failed to commit transaction: %w", err)
127 }
128
129 return nil
130}
131
132func rollback(tx *sqlx.Tx, err error) error {
133 if rerr := tx.Rollback(); rerr != nil {
134 if errors.Is(rerr, sql.ErrTxDone) {
135 return err
136 }
137 return fmt.Errorf("failed to rollback: %s: %w", err.Error(), rerr)
138 }
139
140 return err
141}