Files
at-container-registry/pkg/appview/db/readonly.go

83 lines
2.3 KiB
Go

package db
import (
"context"
"database/sql"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
)
// InitializeDatabase initializes the libSQL database and session store.
// Returns: (read-write DB, read-only DB, session store)
func InitializeDatabase(dbPath string, cfg LibsqlConfig) (*sql.DB, *sql.DB, *SessionStore) {
// Ensure directory exists
dbDir := filepath.Dir(dbPath)
if err := os.MkdirAll(dbDir, 0700); err != nil {
slog.Warn("Failed to create UI database directory", "error", err)
return nil, nil, nil
}
// Initialize read-write database (for writes and auth operations)
database, err := InitDB(dbPath, cfg)
if err != nil {
slog.Warn("Failed to initialize UI database", "error", err)
return nil, nil, nil
}
// Open read-only connection for public queries (search, user pages, etc.)
// Uses ?mode=ro to prevent writes from public-facing handlers
roDSN := dbPath
if !strings.HasPrefix(dbPath, "file:") && !strings.HasPrefix(dbPath, ":memory:") {
roDSN = "file:" + dbPath
}
// Append ?mode=ro for read-only access
if strings.Contains(roDSN, "?") {
roDSN += "&mode=ro"
} else {
roDSN += "?mode=ro"
}
readOnlyDB, err := sql.Open("libsql", roDSN)
if err != nil {
slog.Warn("Failed to open read-only database connection", "error", err)
return nil, nil, nil
}
// busy_timeout is per-connection — without this, reads return SQLITE_BUSY
// immediately when a write is in progress on the read-write connection.
var busyTimeout int
if err := readOnlyDB.QueryRow("PRAGMA busy_timeout = 5000").Scan(&busyTimeout); err != nil {
slog.Warn("Failed to set busy_timeout on read-only connection", "error", err)
}
slog.Info("UI database initialized", "mode", "readonly", "path", dbPath)
// Create session store
sessionStore := NewSessionStore(database)
// Start cleanup goroutines
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
ctx := context.Background()
// Cleanup UI sessions
sessionStore.Cleanup()
// Cleanup OAuth sessions (older than 30 days)
oauthStore := NewOAuthStore(database)
oauthStore.CleanupOldSessions(ctx, 30*24*time.Hour)
oauthStore.CleanupExpiredAuthRequests(ctx)
// Cleanup device pending auths
deviceStore := NewDeviceStore(database)
deviceStore.CleanupExpired()
}
}()
return database, readOnlyDB, sessionStore
}