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			for _, k := range d.cfg.InitialAdminKeys {
 65				pk, _, err := backend.ParseAuthorizedKey(k)
 66				if err != nil {
 67					logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
 68					continue
 69				}
 70
 71				stmt, err := tx.Prepare(`INSERT INTO public_key (user_id, public_key, updated_at)
 72					VALUES (?, ?, CURRENT_TIMESTAMP);`)
 73				if err != nil {
 74					return err
 75				}
 76
 77				defer stmt.Close() // nolint: errcheck
 78				if _, err := stmt.Exec(userID, backend.MarshalAuthorizedKey(pk)); err != nil {
 79					return err
 80				}
 81			}
 82		}
 83
 84		// set init flag
 85		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "init", true); err != nil {
 86			return err
 87		}
 88
 89		return nil
 90	})
 91}
 92
 93func wrapDbErr(err error) error {
 94	if err != nil {
 95		if errors.Is(err, sql.ErrNoRows) {
 96			return ErrNoRecord
 97		}
 98		if liteErr, ok := err.(*sqlite.Error); ok {
 99			code := liteErr.Code()
100			if code == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY ||
101				code == sqlite3.SQLITE_CONSTRAINT_UNIQUE {
102				return ErrDuplicateKey
103			}
104		}
105	}
106	return err
107}
108
109func wrapTx(db *sqlx.DB, ctx context.Context, fn func(tx *sqlx.Tx) error) error {
110	tx, err := db.BeginTxx(ctx, nil)
111	if err != nil {
112		return fmt.Errorf("failed to begin transaction: %w", err)
113	}
114
115	if err := fn(tx); err != nil {
116		return rollback(tx, err)
117	}
118
119	if err := tx.Commit(); err != nil {
120		if errors.Is(err, sql.ErrTxDone) {
121			// this is ok because whoever did finish the tx should have also written the error already.
122			return nil
123		}
124		return fmt.Errorf("failed to commit transaction: %w", err)
125	}
126
127	return nil
128}
129
130func rollback(tx *sqlx.Tx, err error) error {
131	if rerr := tx.Rollback(); rerr != nil {
132		if errors.Is(rerr, sql.ErrTxDone) {
133			return err
134		}
135		return fmt.Errorf("failed to rollback: %s: %w", err.Error(), rerr)
136	}
137
138	return err
139}