package main import ( "context" "crypto/rand" "database/sql" "errors" "fmt" "math/big" "os" "path/filepath" "regexp" "time" "unicode/utf8" _ "github.com/mattn/go-sqlite3" ) const ( maximumCodeBytes = 200 * 1024 maximumTitleRunes = 80 ) var playgroundIDPattern = regexp.MustCompile(`^[a-z0-9]{6,24}$`) type playground struct { ID string `json:"id"` Revision int `json:"revision"` Title string `json:"title"` Code string `json:"code"` CreatedAt time.Time `json:"createdAt"` } type playgroundStore struct { *sql.DB } func openPlaygroundStore(path string) (*playgroundStore, error) { if path != ":memory:" { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err } } database, err := sql.Open("sqlite3", path+"?_foreign_keys=on&_busy_timeout=5000&_journal_mode=WAL") if err != nil { return nil, err } database.SetMaxOpenConns(1) store := &playgroundStore{DB: database} if err := store.initialize(); err != nil { database.Close() return nil, err } return store, nil } func (store *playgroundStore) initialize() error { _, err := store.Exec(` CREATE TABLE IF NOT EXISTS playgrounds ( id TEXT NOT NULL, revision INTEGER NOT NULL CHECK (revision > 0), title TEXT NOT NULL, code TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (id, revision) ); CREATE INDEX IF NOT EXISTS playgrounds_created_at ON playgrounds (created_at DESC); `) return err } func (store *playgroundStore) save(ctx context.Context, id, title, code string) (playground, error) { if id != "" && !playgroundIDPattern.MatchString(id) { return playground{}, errors.New("invalid playground id") } if title == "" || utf8.RuneCountInString(title) > maximumTitleRunes { return playground{}, fmt.Errorf("title must contain 1 to %d characters", maximumTitleRunes) } if code == "" || len(code) > maximumCodeBytes || !utf8.ValidString(code) { return playground{}, fmt.Errorf("code must contain 1 to %d bytes of UTF-8", maximumCodeBytes) } if id == "" { var err error id, err = randomPlaygroundID() if err != nil { return playground{}, err } } transaction, err := store.BeginTx(ctx, nil) if err != nil { return playground{}, err } defer transaction.Rollback() var revision int if err := transaction.QueryRowContext( ctx, `SELECT COALESCE(MAX(revision), 0) + 1 FROM playgrounds WHERE id = ?`, id, ).Scan(&revision); err != nil { return playground{}, err } createdAt := time.Now().UTC().Truncate(time.Millisecond) if _, err := transaction.ExecContext( ctx, `INSERT INTO playgrounds (id, revision, title, code, created_at) VALUES (?, ?, ?, ?, ?)`, id, revision, title, code, createdAt.Format(time.RFC3339Nano), ); err != nil { return playground{}, err } if err := transaction.Commit(); err != nil { return playground{}, err } return playground{ID: id, Revision: revision, Title: title, Code: code, CreatedAt: createdAt}, nil } func (store *playgroundStore) get(ctx context.Context, id string, revision int) (playground, error) { if !playgroundIDPattern.MatchString(id) || revision < 1 { return playground{}, sql.ErrNoRows } var result playground var createdAt string err := store.QueryRowContext( ctx, `SELECT id, revision, title, code, created_at FROM playgrounds WHERE id = ? AND revision = ?`, id, revision, ).Scan(&result.ID, &result.Revision, &result.Title, &result.Code, &createdAt) if err != nil { return playground{}, err } result.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt) return result, err } func randomPlaygroundID() (string, error) { const alphabet = "abcdefghijkmnopqrstuvwxyz23456789" result := make([]byte, 10) for index := range result { value, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet)))) if err != nil { return "", err } result[index] = alphabet[value.Int64()] } return string(result), nil }