CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Storage tests on PostgreSQL (push) Waiting to run
CI / Complement (federation off) (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / Smoke test the binary (push) Waiting to run
CI / Admin panel (push) Waiting to run
CI / Web client (push) Waiting to run
CI / Desktop client (ubuntu-24.04) (push) Waiting to run
CI / Desktop client (windows-latest) (push) Waiting to run
The pocketbase driver no longer embeds PocketBase: Warp keeps its data as PocketBase collections on a server of its own and reaches it only through the REST API, the way the postgres driver reaches PostgreSQL. database.connection_string is the server's URL, and Warp signs in as the superuser in WARP_PB_SUPERUSER_EMAIL / WARP_PB_SUPERUSER_PASSWORD (or a WARP_PB_SUPERUSER_FILE of the two). Every SQL table is a superuser-only collection of the same name and columns, created by warp migrate. Reads go through PocketBase's SQL endpoint with the SQL drivers' queries, via a read-only database/sql driver that inlines arguments, moves long ones into warp_params and pages past 1000 rows. Writes are record operations in batch requests under one write lock in the Warp process. Media is a protected file field in warp_media, local or in the S3 bucket Warp writes into PocketBase's settings. Search matches word prefixes with LIKE. The storage tests run against an in-process PocketBase and pass as on SQLite. Docker: the PocketBase stacks get a pocketbase service running tajniak81/pocketbase-docker-root (WARP_PB_IMAGE), with a pb-data volume and its admin UI on loopback; the AIO images run that PocketBase binary as a process beside the homeserver, making up a superuser when none is set. The .env examples, READMEs, docs and warp.yaml.example follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
5.7 KiB
Go
192 lines
5.7 KiB
Go
// Package storage defines the interfaces every Warp storage driver
|
|
// implements, plus the registry that turns a config into a Database.
|
|
//
|
|
// Drivers live in sub-packages (sqlite, postgres, pocketbase) and register
|
|
// themselves from an init function, the database/sql way. Nothing in this
|
|
// package imports a driver, so components can depend on the interfaces
|
|
// without pulling in every database engine.
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Driver names. These are the values accepted by database.driver in the
|
|
// config file.
|
|
const (
|
|
DriverSQLite = "sqlite"
|
|
DriverPostgres = "postgres"
|
|
DriverPocketBase = "pocketbase"
|
|
)
|
|
|
|
// ErrNotImplemented is returned by a driver that is compiled in but not
|
|
// finished.
|
|
var ErrNotImplemented = errors.New("not implemented by this storage driver")
|
|
|
|
// ErrNoSuchSetting is returned by SettingsStore.Get for an unknown key.
|
|
var ErrNoSuchSetting = errors.New("no such setting")
|
|
|
|
// Config is the subset of the file configuration a driver needs.
|
|
type Config struct {
|
|
Driver string
|
|
ConnectionString string
|
|
MaxOpenConns int
|
|
MaxIdleConns int
|
|
ConnMaxLifetime time.Duration
|
|
|
|
// SuperuserEmail and SuperuserPassword are the account the pocketbase
|
|
// driver signs in to its server with. Only the pocketbase driver reads
|
|
// them.
|
|
SuperuserEmail string
|
|
SuperuserPassword string
|
|
|
|
// S3 is the bucket a driver with its own file storage puts media in.
|
|
// Only the pocketbase driver reads it.
|
|
S3 S3Config
|
|
}
|
|
|
|
// S3Config is an S3-compatible bucket.
|
|
type S3Config struct {
|
|
Enabled bool
|
|
Bucket string
|
|
Region string
|
|
Endpoint string
|
|
AccessKey string
|
|
Secret string
|
|
ForcePathStyle bool
|
|
}
|
|
|
|
// Database is the handle every component gets. Component-specific stores
|
|
// (userapi, roomserver, syncapi...) hang off it as they are implemented.
|
|
type Database interface {
|
|
// Driver reports which driver this is, for logs and admin output.
|
|
Driver() string
|
|
|
|
// Ping checks the connection is alive; used by /healthz.
|
|
Ping(ctx context.Context) error
|
|
|
|
// Migrate applies pending migrations.
|
|
Migrate(ctx context.Context) error
|
|
|
|
// PendingMigrations lists migrations that Migrate would apply.
|
|
PendingMigrations(ctx context.Context) ([]string, error)
|
|
|
|
// Settings is the server_settings key/value store backing the runtime
|
|
// settings service (federation and registration switches).
|
|
Settings() SettingsStore
|
|
|
|
// Users is the userapi store: accounts, devices, tokens, account data.
|
|
Users() UserStore
|
|
|
|
// Rooms is the roomserver store: rooms, events, room state, aliases.
|
|
Rooms() RoomStore
|
|
|
|
// Sync is the syncapi store: receipts, to-device messages, presence and
|
|
// uploaded filters.
|
|
Sync() SyncStore
|
|
|
|
// Federation is the federation store: other servers' keys, the
|
|
// outbound queue and its backoff, and the inbound transaction log.
|
|
Federation() FederationStore
|
|
|
|
// Media is the mediaapi store: the metadata of uploaded and cached
|
|
// remote media, and of their thumbnails.
|
|
Media() MediaStore
|
|
|
|
// Keys is the keyserver store: device keys, one-time and fallback
|
|
// keys, cross-signing keys and their signatures, the device list
|
|
// stream and the server-side key backup.
|
|
Keys() KeyStore
|
|
|
|
// Push is the push store: pushers, and the notifications push rules
|
|
// produce with their per-room unread counts.
|
|
Push() PushStore
|
|
|
|
// Admin is the admin API's own state: registration tokens, server
|
|
// notice rooms and the audit log.
|
|
Admin() AdminStore
|
|
|
|
// AppServices is where each application service is in the event
|
|
// stream.
|
|
AppServices() AppServiceStore
|
|
|
|
// DelayedEvents is where the delayed events clients scheduled wait
|
|
// to be sent (MSC4140).
|
|
DelayedEvents() DelayedEventStore
|
|
|
|
// OpenIDTokens is where the OpenID tokens users hand to third parties
|
|
// are kept.
|
|
OpenIDTokens() OpenIDTokenStore
|
|
|
|
// Close releases the connection pool.
|
|
Close() error
|
|
}
|
|
|
|
// SettingsStore persists the handful of settings that are mutable at
|
|
// runtime through the admin API.
|
|
type SettingsStore interface {
|
|
// Get returns the raw value for key. The boolean reports presence.
|
|
Get(ctx context.Context, key string) (string, bool, error)
|
|
|
|
// GetAll returns every setting.
|
|
GetAll(ctx context.Context) (map[string]string, error)
|
|
|
|
// Set writes key, overwriting any previous value.
|
|
Set(ctx context.Context, key, value string) error
|
|
|
|
// Delete removes key, reverting it to its configured default. Deleting
|
|
// a key that is not there is not an error.
|
|
Delete(ctx context.Context, key string) error
|
|
}
|
|
|
|
// OpenFunc opens a database for a driver.
|
|
type OpenFunc func(ctx context.Context, cfg Config, log *slog.Logger) (Database, error)
|
|
|
|
var (
|
|
registryMu sync.RWMutex
|
|
registry = map[string]OpenFunc{}
|
|
)
|
|
|
|
// Register makes a driver available to Open. It panics on a duplicate
|
|
// registration, which can only be a programming error.
|
|
func Register(name string, open OpenFunc) {
|
|
registryMu.Lock()
|
|
defer registryMu.Unlock()
|
|
if _, dup := registry[name]; dup {
|
|
panic("storage: driver " + name + " registered twice")
|
|
}
|
|
registry[name] = open
|
|
}
|
|
|
|
// Drivers lists the registered driver names, sorted.
|
|
func Drivers() []string {
|
|
registryMu.RLock()
|
|
defer registryMu.RUnlock()
|
|
return slices.Sorted(maps.Keys(registry))
|
|
}
|
|
|
|
// Open connects using the driver named in cfg.
|
|
func Open(ctx context.Context, cfg Config, log *slog.Logger) (Database, error) {
|
|
registryMu.RLock()
|
|
open, ok := registry[cfg.Driver]
|
|
registryMu.RUnlock()
|
|
if !ok {
|
|
return nil, fmt.Errorf("storage: unknown driver %q (compiled in: %v)", cfg.Driver, Drivers())
|
|
}
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
db, err := open(ctx, cfg, log.With("driver", cfg.Driver))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: open %s: %w", cfg.Driver, err)
|
|
}
|
|
return db, nil
|
|
}
|