DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
// Pure-Go SQLite driver, registered as "sqlite"
|
|
"modernc.org/sqlite"
|
|
)
|
|
|
|
// schemaSQL holds the authoritative, idempotent table definitions
|
|
//
|
|
//go:embed schema.sql
|
|
var schemaSQL string
|
|
|
|
// dsnParams are applied by the driver to every pooled connection
|
|
// foreign_keys: enforce the operations -> tokens reference
|
|
// busy_timeout: wait instead of failing when the server and the CLI write concurrently
|
|
// journal_mode WAL: readers never block the writer
|
|
// _txlock=immediate: transactions take the write lock at BEGIN, so read-then-write never fails with SQLITE_BUSY
|
|
const dsnParams = "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_txlock=immediate"
|
|
|
|
// maxOpenConns bounds the SQLite connection pool
|
|
const maxOpenConns = 4
|
|
|
|
// sqliteDSN builds the driver DSN for a filesystem path
|
|
// The path is made absolute and percent-encoded so that spaces, '?', '#', '%' and other bytes are taken literally
|
|
func sqliteDSN(path string) (string, error) {
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve database path: %w", err)
|
|
}
|
|
return "file://" + escapeURIPath(abs) + "?" + dsnParams, nil
|
|
}
|
|
|
|
// escapeURIPath percent-encodes every byte except RFC 3986 unreserved characters and '/'
|
|
func escapeURIPath(p string) string {
|
|
const hexDigits = "0123456789ABCDEF"
|
|
out := make([]byte, 0, len(p))
|
|
for i := 0; i < len(p); i++ {
|
|
c := p[i]
|
|
switch {
|
|
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9',
|
|
c == '-', c == '.', c == '_', c == '~', c == '/':
|
|
out = append(out, c)
|
|
default:
|
|
out = append(out, '%', hexDigits[c>>4], hexDigits[c&0x0f])
|
|
}
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// OpenDatabase opens (creating if needed) the SQLite database at path and initializes the schema
|
|
func OpenDatabase(ctx context.Context, path string, log *slog.Logger) (*sql.DB, error) {
|
|
defer traceFunc(ctx, log, "OpenDatabase")()
|
|
|
|
// Build the DSN
|
|
dsn, err := sqliteDSN(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Open the connection pool (lazy: the file is opened on first use)
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
db.SetMaxOpenConns(maxOpenConns)
|
|
db.SetMaxIdleConns(maxOpenConns)
|
|
|
|
// Create the tables if needed
|
|
if err := InitializeSchema(ctx, db, log); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
log.DebugContext(ctx, "database opened", "path", path)
|
|
return db, nil
|
|
}
|
|
|
|
// InitializeSchema executes the embedded schema; every statement is idempotent
|
|
func InitializeSchema(ctx context.Context, db *sql.DB, log *slog.Logger) error {
|
|
defer traceFunc(ctx, log, "InitializeSchema")()
|
|
|
|
if _, err := db.ExecContext(ctx, schemaSQL); err != nil {
|
|
return fmt.Errorf("initialize database schema: %w", err)
|
|
}
|
|
log.DebugContext(ctx, "database schema initialized")
|
|
return nil
|
|
}
|
|
|
|
// isSQLiteCode reports whether err is a SQLite error with the given (extended) result code
|
|
func isSQLiteCode(err error, code int) bool {
|
|
var se *sqlite.Error
|
|
return errors.As(err, &se) && se.Code() == code
|
|
}
|
|
|
|
// formatTime renders a timestamp as stored in the database: UTC RFC3339Nano
|
|
func formatTime(t time.Time) string {
|
|
return t.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
|
|
// parseTime parses a timestamp stored by formatTime
|
|
func parseTime(s string) (time.Time, error) {
|
|
t, err := time.Parse(time.RFC3339Nano, s)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("parse stored timestamp: %w", err)
|
|
}
|
|
return t.UTC(), nil
|
|
}
|