PocketBase as a standalone database server
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
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>
This commit is contained in:
1 parent
0994156705
commit
a47e15cc1c
69 files changed
+8209
-374
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -101,7 +102,7 @@ type Database struct {
|
||||
Driver string `yaml:"driver" env:"WARP_DB_DRIVER"`
|
||||
|
||||
// ConnectionString is driver specific: a DSN for postgres, a file URI
|
||||
// for sqlite, a directory for pocketbase.
|
||||
// for sqlite, the server's URL for pocketbase.
|
||||
ConnectionString string `yaml:"connection_string" env:"WARP_DB_CONNECTION_STRING"`
|
||||
|
||||
MaxOpenConns int `yaml:"max_open_conns" env:"WARP_DB_MAX_OPEN_CONNS"`
|
||||
@@ -112,14 +113,28 @@ type Database struct {
|
||||
// schema changes behind an explicit `warp migrate`.
|
||||
AutoMigrate bool `yaml:"auto_migrate" env:"WARP_DB_AUTO_MIGRATE"`
|
||||
|
||||
// PocketBase configures what the pocketbase driver adds over sqlite.
|
||||
// PocketBase configures the pocketbase driver: how it signs in to the
|
||||
// server, and where that server keeps media.
|
||||
PocketBase PocketBase `yaml:"pocketbase"`
|
||||
}
|
||||
|
||||
// PocketBase configures the pocketbase storage driver.
|
||||
type PocketBase struct {
|
||||
// SuperuserEmail and SuperuserPassword are the PocketBase superuser
|
||||
// Warp signs in as. Warp's collections are superuser-only, so nothing
|
||||
// less can read or write them.
|
||||
SuperuserEmail string `yaml:"superuser_email" env:"WARP_PB_SUPERUSER_EMAIL"`
|
||||
SuperuserPassword string `yaml:"superuser_password" env:"WARP_PB_SUPERUSER_PASSWORD"`
|
||||
|
||||
// SuperuserFile names a file of WARP_PB_SUPERUSER_EMAIL=... and
|
||||
// WARP_PB_SUPERUSER_PASSWORD=... lines, read for whichever of the two
|
||||
// is not set otherwise. The all-in-one images keep the superuser they
|
||||
// make up there, so that every warp command in the container finds it.
|
||||
SuperuserFile string `yaml:"superuser_file" env:"WARP_PB_SUPERUSER_FILE"`
|
||||
|
||||
// S3 puts media content in an S3-compatible bucket through
|
||||
// PocketBase's filesystem, instead of under the data directory.
|
||||
// PocketBase's filesystem, instead of under its pb_data. Warp writes it
|
||||
// into PocketBase's settings on every start.
|
||||
S3 PocketBaseS3 `yaml:"s3"`
|
||||
}
|
||||
|
||||
@@ -143,6 +158,9 @@ func (d Database) Storage() storage.Config {
|
||||
MaxOpenConns: d.MaxOpenConns,
|
||||
MaxIdleConns: d.MaxIdleConns,
|
||||
ConnMaxLifetime: d.ConnMaxLifetime,
|
||||
|
||||
SuperuserEmail: d.PocketBase.SuperuserEmail,
|
||||
SuperuserPassword: d.PocketBase.SuperuserPassword,
|
||||
S3: storage.S3Config{
|
||||
Enabled: s3.Enabled,
|
||||
Bucket: s3.Bucket,
|
||||
@@ -709,6 +727,14 @@ func (c *Config) Validate() error {
|
||||
bad("tracing.endpoint: must be an http or https URL")
|
||||
}
|
||||
}
|
||||
if c.Database.Driver == "pocketbase" {
|
||||
if u, err := url.Parse(c.Database.ConnectionString); err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
bad("database.connection_string: must be the PocketBase server's http or https URL for the pocketbase driver")
|
||||
}
|
||||
if c.Database.PocketBase.SuperuserEmail == "" || c.Database.PocketBase.SuperuserPassword == "" {
|
||||
bad("database.pocketbase: superuser_email and superuser_password are required")
|
||||
}
|
||||
}
|
||||
if c.Database.Driver == "pocketbase" && c.Database.PocketBase.S3.Enabled {
|
||||
s3 := c.Database.PocketBase.S3
|
||||
if s3.Bucket == "" || s3.Region == "" || s3.Endpoint == "" || s3.AccessKey == "" || s3.Secret == "" {
|
||||
@@ -889,8 +915,45 @@ func Load(path string) (*Config, error) {
|
||||
if err := ApplyEnv(cfg, os.Getenv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.Database.PocketBase.readSuperuserFile(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration:\n%w", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// readSuperuserFile fills in the superuser from SuperuserFile, for whichever
|
||||
// of the email and password is still empty. A file that does not exist yet
|
||||
// is not an error: the all-in-one entrypoint writes it on first start, and
|
||||
// a command run before that finds nothing either way.
|
||||
func (p *PocketBase) readSuperuserFile() error {
|
||||
if p.SuperuserFile == "" || (p.SuperuserEmail != "" && p.SuperuserPassword != "") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(p.SuperuserFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read database.pocketbase.superuser_file: %w", err)
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "WARP_PB_SUPERUSER_EMAIL":
|
||||
if p.SuperuserEmail == "" {
|
||||
p.SuperuserEmail = value
|
||||
}
|
||||
case "WARP_PB_SUPERUSER_PASSWORD":
|
||||
if p.SuperuserPassword == "" {
|
||||
p.SuperuserPassword = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSuperuserFileFillsInWhatIsUnset(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "superuser.env")
|
||||
if err := os.WriteFile(path, []byte("WARP_PB_SUPERUSER_EMAIL=warp@warp.local\nWARP_PB_SUPERUSER_PASSWORD=from-the-file\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := PocketBase{SuperuserFile: path}
|
||||
if err := p.readSuperuserFile(); err != nil {
|
||||
t.Fatalf("readSuperuserFile: %v", err)
|
||||
}
|
||||
if p.SuperuserEmail != "warp@warp.local" || p.SuperuserPassword != "from-the-file" {
|
||||
t.Errorf("read %q / %q", p.SuperuserEmail, p.SuperuserPassword)
|
||||
}
|
||||
|
||||
// What the environment set wins.
|
||||
p = PocketBase{SuperuserFile: path, SuperuserPassword: "from-the-environment"}
|
||||
if err := p.readSuperuserFile(); err != nil {
|
||||
t.Fatalf("readSuperuserFile: %v", err)
|
||||
}
|
||||
if p.SuperuserEmail != "warp@warp.local" || p.SuperuserPassword != "from-the-environment" {
|
||||
t.Errorf("read %q / %q", p.SuperuserEmail, p.SuperuserPassword)
|
||||
}
|
||||
|
||||
// A file not written yet is no error.
|
||||
p = PocketBase{SuperuserFile: filepath.Join(t.TempDir(), "missing.env")}
|
||||
if err := p.readSuperuserFile(); err != nil || p.SuperuserEmail != "" {
|
||||
t.Errorf("missing file: %q, %v", p.SuperuserEmail, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user