Files
Warp/server/internal/storage/pocketbase/pocketbase.go
T
tajniak81andClaude Opus 5.5 92836f8dcd
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
Phase 6: media
Implements PLAN.md Phase 6: the media repository. Clients upload files,
reserve an mxc:// URI and upload to it later, download and ask for
thumbnails, all through the authenticated media endpoints of Matrix 1.11;
other servers download this server's media through the federation media
endpoints, and media uploaded elsewhere is fetched over federation on
first use and kept.

internal/mediaapi is the component. Content goes into a BlobStore named
by its SHA-256, so the same file uploaded twice is stored once and the
hash doubles as the ETag; the metadata that maps an mxc:// URI onto a
blob is in media_repository and media_thumbnails. The local-filesystem
store writes to a temporary file and renames it into place once hashed,
so nothing ever refers to half a file. A storage driver can bring its own
store through storage.BlobProvider, which is where PocketBase's
filesystem abstraction (local or S3) plugs in with Phase 10.

Reserved uploads belong to the user who made them, take one upload,
lapse after a day, and are capped per user; a download of one waits for
its content, woken in memory the moment it arrives, for as long as
timeout_ms allows.

Thumbnails come from a fixed set of configured sizes, so a thousand
requested sizes cannot mean a thousand resizes. They are made on first
request and kept, never enlarge, honour a JPEG's EXIF orientation, and
point at the original when it already fits. The pixel count is checked
from the header before anything is decoded, at most four resizes run at
once, and concurrent requests for the same thumbnail share one.

Remote media is fetched through the signed client: the multipart/mixed
answer of Matrix 1.11, with a fallback to the legacy download endpoint
for servers that answer M_UNRECOGNIZED, and Location redirects followed
by a dialer that refuses loopback, private and link-local addresses.
Concurrent requests share one fetch, detached from the request that
started it. With federation off nothing is fetched, and what was cached
before is still served.

Downloads carry a sandboxing CSP, nosniff, inline Content-Disposition
only for the types the spec lists as safe, and private immutable caching.
The unauthenticated legacy download endpoints are off by default behind
media.legacy_unauthenticated_downloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:26:49 +02:00

479 lines
17 KiB
Go

// Package pocketbase is the placeholder for the PocketBase storage driver.
//
// The plan (PLAN.md section 4) is an appliance deployment: one binary that
// also brings admin auth, a filesystem abstraction for media (local or S3)
// and realtime for the admin panel. The event DAG is a poor fit for
// PocketBase's record model, so the roomserver and syncapi tables will use
// its underlying dbx connection directly rather than its collection API.
//
// Until that work starts (Phase 10) the driver is compiled in, registered
// and fails loudly, so that `database.driver: pocketbase` is a clear error
// instead of an unknown-driver one, and so the interface stays honest:
// every method the other drivers implement exists here too.
package pocketbase
import (
"context"
"fmt"
"log/slog"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
func init() {
storage.Register(storage.DriverPocketBase, Open)
}
// Database is the not-yet-implemented PocketBase driver.
type Database struct{}
// Open always fails with storage.ErrNotImplemented.
func Open(context.Context, storage.Config, *slog.Logger) (storage.Database, error) {
return nil, fmt.Errorf(
"pocketbase driver is not implemented yet (planned for Phase 10); use %q or %q: %w",
storage.DriverSQLite, storage.DriverPostgres, storage.ErrNotImplemented)
}
// Driver implements storage.Database.
func (*Database) Driver() string { return storage.DriverPocketBase }
// Ping implements storage.Database.
func (*Database) Ping(context.Context) error { return storage.ErrNotImplemented }
// Migrate implements storage.Database.
func (*Database) Migrate(context.Context) error { return storage.ErrNotImplemented }
// PendingMigrations implements storage.Database.
func (*Database) PendingMigrations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
// Settings implements storage.Database.
func (*Database) Settings() storage.SettingsStore { return notImplementedSettings{} }
// Users implements storage.Database.
func (*Database) Users() storage.UserStore { return notImplementedUsers{} }
// Rooms implements storage.Database.
func (*Database) Rooms() storage.RoomStore { return notImplementedRooms{} }
// Sync implements storage.Database.
func (*Database) Sync() storage.SyncStore { return notImplementedSync{} }
// Federation implements storage.Database.
func (*Database) Federation() storage.FederationStore { return notImplementedFederation{} }
// Media implements storage.Database.
func (*Database) Media() storage.MediaStore { return notImplementedMedia{} }
// Close implements storage.Database.
func (*Database) Close() error { return nil }
// notImplementedSettings satisfies storage.SettingsStore by refusing every
// call, so a partially wired-up PocketBase deployment cannot silently lose
// an admin's federation switch.
type notImplementedSettings struct{}
func (notImplementedSettings) Get(context.Context, string) (string, bool, error) {
return "", false, storage.ErrNotImplemented
}
func (notImplementedSettings) GetAll(context.Context) (map[string]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSettings) Set(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedSettings) Delete(context.Context, string) error {
return storage.ErrNotImplemented
}
// notImplementedUsers satisfies storage.UserStore the same way: a
// half-wired PocketBase deployment must not accept a registration it cannot
// store, or hand out an access token it cannot look up again.
type notImplementedUsers struct{}
func (notImplementedUsers) CreateAccount(context.Context, storage.NewAccount) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Account(context.Context, string) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) PasswordHash(context.Context, string) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedUsers) SetPasswordHash(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAdmin(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetDisplayName(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAvatarURL(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) ListAccounts(context.Context, storage.AccountFilter) ([]storage.Account, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedUsers) Stats(context.Context) (storage.Stats, error) {
return storage.Stats{}, storage.ErrNotImplemented
}
func (notImplementedUsers) CreateSession(context.Context, storage.NewSession) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) RefreshSession(context.Context, storage.RefreshedSession) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TokenInfo(context.Context, string) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TouchDevice(context.Context, string, string, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) Device(context.Context, string, string) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Devices(context.Context, string) ([]storage.Device, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetDeviceDisplayName(context.Context, string, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteDevices(context.Context, string, []string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteAllDevices(context.Context, string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) AccountData(context.Context, string, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetAccountData(context.Context, string, string, string, []byte) error {
return storage.ErrNotImplemented
}
// notImplementedRooms satisfies storage.RoomStore the same way. It matters
// more here than anywhere else: a room whose create event was accepted and
// not stored would hand a client a room ID that refers to nothing.
type notImplementedRooms struct{}
func (notImplementedRooms) CreateRoom(context.Context, storage.NewRoom) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Room(context.Context, string) (storage.Room, error) {
return storage.Room{}, storage.ErrNotImplemented
}
func (notImplementedRooms) Write(context.Context, storage.RoomWrite) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Event(context.Context, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) EventsByID(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentState(context.Context, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvents(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvent(context.Context, string, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) StateBefore(context.Context, string, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Extremities(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Members(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipEntries(context.Context, string, []string) ([]storage.MembershipEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Membership(context.Context, string, string) (storage.MembershipEntry, error) {
return storage.MembershipEntry{}, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsForUser(context.Context, string, []string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipsForUser(context.Context, string, []string) ([]storage.SyncMembership, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) UserMembershipEvents(context.Context, string, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MaxStreamOrdering(context.Context) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Timeline(context.Context, storage.TimelineRequest) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsWithEvents(context.Context, []string, int64, int64) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) StateAtStream(context.Context, string, int64) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) TransactionEvent(context.Context, storage.TransactionRef) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedRooms) CreateAlias(context.Context, storage.RoomAlias) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) DeleteAlias(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Alias(context.Context, string) (storage.RoomAlias, error) {
return storage.RoomAlias{}, storage.ErrNotImplemented
}
func (notImplementedRooms) AliasesForRoom(context.Context, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) SetPublished(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) PublishedRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) ListRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomSummary(context.Context, string) (storage.RoomSummary, error) {
return storage.RoomSummary{}, storage.ErrNotImplemented
}
func (notImplementedRooms) PurgeRoom(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) RoomStats(context.Context) (storage.RoomStats, error) {
return storage.RoomStats{}, storage.ErrNotImplemented
}
func (notImplementedRooms) WriteOutliers(context.Context, string, []storage.StoredEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) WriteBackfill(context.Context, string, []storage.BackfillEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) InviteState(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) HasJoinedMemberOn(context.Context, string, string) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedRooms) EventByID(context.Context, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
// notImplementedFederation satisfies storage.FederationStore the same way.
// A queue that accepted events and never stored them would lose them the
// moment federation was switched on.
type notImplementedFederation struct{}
func (notImplementedFederation) StoreServerKeys(context.Context, []storage.ServerKey) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) ServerKeys(context.Context, string, []string) ([]storage.ServerKey, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Enqueue(context.Context, []storage.QueuedItem) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Queued(context.Context, string, string, int) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Dequeue(context.Context, []int64) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) QueuedDestinations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Destination(context.Context, string) (storage.Destination, error) {
return storage.Destination{}, storage.ErrNotImplemented
}
func (notImplementedFederation) SetDestination(context.Context, storage.Destination) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) Destinations(context.Context) ([]storage.DestinationSummary, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) InboundTransaction(context.Context, string, string) ([]byte, bool, error) {
return nil, false, storage.ErrNotImplemented
}
func (notImplementedFederation) RecordInboundTransaction(context.Context, string, string, []byte, int64) error {
return storage.ErrNotImplemented
}
// notImplementedSync satisfies storage.SyncStore the same way. A sync that
// returned an empty success would be worse than an error: a client would
// take it for "nothing has happened" and stop asking.
type notImplementedSync struct{}
func (notImplementedSync) StreamPositions(context.Context) (storage.StreamPositions, error) {
return storage.StreamPositions{}, storage.ErrNotImplemented
}
func (notImplementedSync) SetReceipt(context.Context, storage.Receipt) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Receipts(context.Context, []string, int64, int64) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) ReceiptsForUser(context.Context, string, []string) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SendToDevice(context.Context, []storage.NewToDeviceMessage) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedSync) ToDeviceMessages(context.Context, string, string, int64, int64, int) ([]storage.ToDeviceMessage, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) DeleteToDeviceMessages(context.Context, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedSync) SetPresence(context.Context, storage.PresenceState) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Presence(context.Context, string) (storage.PresenceState, error) {
return storage.PresenceState{}, storage.ErrNotImplemented
}
func (notImplementedSync) PresenceForUsers(context.Context, []string, int64, int64) ([]storage.PresenceState, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataChanges(context.Context, string, int64, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataAt(context.Context, string, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SaveFilter(context.Context, string, []byte, int64) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedSync) Filter(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
// notImplementedMedia satisfies storage.MediaStore the same way. An upload
// whose metadata was not stored would hand the client an mxc:// URI that
// refers to nothing.
type notImplementedMedia struct{}
func (notImplementedMedia) CreateMedia(context.Context, storage.Media) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) Media(context.Context, string, string) (storage.Media, error) {
return storage.Media{}, storage.ErrNotImplemented
}
func (notImplementedMedia) CompleteUpload(context.Context, string, string, storage.MediaContent) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) PendingUploads(context.Context, string, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) DeleteExpiredUploads(context.Context, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) Thumbnail(context.Context, string, string, int, int, string) (storage.Thumbnail, error) {
return storage.Thumbnail{}, storage.ErrNotImplemented
}
func (notImplementedMedia) StoreThumbnail(context.Context, storage.Thumbnail) error {
return storage.ErrNotImplemented
}
// Compile-time proof that the stub keeps up with the interfaces.
var (
_ storage.Database = (*Database)(nil)
_ storage.UserStore = notImplementedUsers{}
_ storage.RoomStore = notImplementedRooms{}
_ storage.SyncStore = notImplementedSync{}
_ storage.FederationStore = notImplementedFederation{}
_ storage.MediaStore = notImplementedMedia{}
)