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
Implements PLAN.md Phase 9. internal/appservice reads registration files (appservice.config_files), matches namespaces the way Synapse does (anchored at the start), and delivers events to each service as transactions read from the roomserver's event stream at a cursor kept in appservice_state. A transaction in flight is recorded, so after a restart it is resent under the same ID; failures back off from 2s to 5m, and a successful ping cuts the backoff short. In the Client-Server API an as_token acts as the service's sender or, with ?user_id=, as any existing user in its namespace. Services register and log in their users with m.login.application_service; exclusive user and alias namespaces are refused to everybody else with M_EXCLUSIVE. Unknown aliases and users in a namespace are asked of the service first. The ping endpoint reports M_URL_NOT_SET, M_BAD_STATUS, M_CONNECTION_FAILED and M_CONNECTION_TIMEOUT. Services' own requests, and their users' when rate_limited is false, skip the rate limiter. Users can change their password (POST /account/password) and deactivate their account (POST /account/deactivate), both behind the password. Deactivation revokes tokens, deletes devices, optionally erases the profile and leaves every room through a new userapi.AccountObserver. The admin API is complete: user detail, the admin flag, deactivation and reactivation, password resets, devices, media quotas (media.max_user_storage_bytes plus per-user overrides), registration tokens with use limits and expiry (registration.require_token), server notices (internal/servernotices, server_notices.*), an editor for the federation allow/deny lists persisted in server_settings, a delivery view of the application services, and an audit log of every change. Migration 0011 adds the registration token, server notice room, media quota, audit log and appservice cursor tables. Policy lists are now returned sorted. The Complement entrypoint loads the registrations Complement mounts in /complement/appservice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
5.4 KiB
Go
170 lines
5.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
)
|
|
|
|
// Errors of the media store.
|
|
var (
|
|
// ErrNoSuchMedia means no media, or no thumbnail, is stored under the
|
|
// given name.
|
|
ErrNoSuchMedia = errors.New("no such media")
|
|
|
|
// ErrMediaExists is CreateMedia's answer for a media ID that is taken.
|
|
ErrMediaExists = errors.New("media already exists")
|
|
|
|
// ErrMediaUploaded is CompleteUpload's answer for media whose content
|
|
// has already been uploaded: content, once stored, never changes.
|
|
ErrMediaUploaded = errors.New("media content already uploaded")
|
|
)
|
|
|
|
// Media is the metadata of one piece of content: uploaded here, or fetched
|
|
// from the server it was uploaded to and kept.
|
|
type Media struct {
|
|
// Origin is the server name in the mxc:// URI: this server for an
|
|
// upload, another for cached remote media.
|
|
Origin string
|
|
MediaID string
|
|
|
|
// ContentType is the Content-Type the content was uploaded with,
|
|
// parameters included.
|
|
ContentType string
|
|
|
|
// UploadName is the file name the uploader gave, or "".
|
|
UploadName string
|
|
|
|
// Size is the content's length in bytes.
|
|
Size int64
|
|
|
|
// SHA256 names the content in the blob store: its SHA-256, in
|
|
// lower-case hex. Empty while the media is pending.
|
|
SHA256 string
|
|
|
|
// UserID is the uploader; "" for remote media.
|
|
UserID string
|
|
|
|
CreatedAtMS int64
|
|
|
|
// Pending is set for an ID handed out by /create whose content has
|
|
// not been uploaded yet. Pending media expires at ExpiresAtMS.
|
|
Pending bool
|
|
ExpiresAtMS int64
|
|
}
|
|
|
|
// MediaContent is what CompleteUpload fills in on pending media.
|
|
type MediaContent struct {
|
|
ContentType string
|
|
UploadName string
|
|
Size int64
|
|
SHA256 string
|
|
}
|
|
|
|
// Thumbnail is one generated (or, for remote media, fetched) thumbnail.
|
|
type Thumbnail struct {
|
|
Origin string
|
|
MediaID string
|
|
|
|
// Width, Height and Method are the size the thumbnail was made for —
|
|
// one of the configured sizes — not the size of the image itself,
|
|
// which for "scale" keeps the original's aspect ratio.
|
|
Width int
|
|
Height int
|
|
Method string
|
|
|
|
ContentType string
|
|
Size int64
|
|
SHA256 string
|
|
CreatedAtMS int64
|
|
}
|
|
|
|
// MediaStore is the mediaapi store: the metadata of local and cached
|
|
// remote media and of their thumbnails. The content itself is in a
|
|
// BlobStore.
|
|
type MediaStore interface {
|
|
// CreateMedia records media: an upload, a pending upload or a remote
|
|
// fetch. A taken (origin, media ID) is ErrMediaExists.
|
|
CreateMedia(ctx context.Context, m Media) error
|
|
|
|
// Media reads one media record, pending or not.
|
|
Media(ctx context.Context, origin, mediaID string) (Media, error)
|
|
|
|
// CompleteUpload stores the content of pending media and clears its
|
|
// pending flag. It is ErrNoSuchMedia for an unknown ID and
|
|
// ErrMediaUploaded for media that is not pending.
|
|
CompleteUpload(ctx context.Context, origin, mediaID string, content MediaContent) error
|
|
|
|
// PendingUploads counts a user's pending media that has not expired
|
|
// by nowMS.
|
|
PendingUploads(ctx context.Context, userID string, nowMS int64) (int, error)
|
|
|
|
// DeleteExpiredUploads removes pending media that expired before
|
|
// nowMS, reporting how many.
|
|
DeleteExpiredUploads(ctx context.Context, nowMS int64) (int, error)
|
|
|
|
// Thumbnail reads the thumbnail made for one size.
|
|
Thumbnail(ctx context.Context, origin, mediaID string, width, height int, method string) (Thumbnail, error)
|
|
|
|
// StoreThumbnail records a thumbnail, replacing one stored for the
|
|
// same size.
|
|
StoreThumbnail(ctx context.Context, t Thumbnail) error
|
|
|
|
// MediaUsage totals what a user has uploaded to this server. Pending
|
|
// reservations count for nothing until their content arrives.
|
|
MediaUsage(ctx context.Context, origin, userID string) (MediaUsage, error)
|
|
|
|
// MediaQuota returns the quota the admin API set for a user, and
|
|
// whether one is set.
|
|
MediaQuota(ctx context.Context, userID string) (int64, bool, error)
|
|
|
|
// SetMediaQuota sets a user's quota; 0 is no limit.
|
|
SetMediaQuota(ctx context.Context, userID string, maxBytes int64) error
|
|
|
|
// DeleteMediaQuota returns a user to the configured default.
|
|
DeleteMediaQuota(ctx context.Context, userID string) error
|
|
}
|
|
|
|
// MediaUsage is what one user's uploads add up to.
|
|
type MediaUsage struct {
|
|
Count int
|
|
Bytes int64
|
|
}
|
|
|
|
// ErrNoSuchBlob means the blob store holds nothing under a hash.
|
|
var ErrNoSuchBlob = errors.New("no such blob")
|
|
|
|
// ErrBlobTooLarge means content passed the limit it was stored with.
|
|
var ErrBlobTooLarge = errors.New("content is larger than allowed")
|
|
|
|
// BlobInfo describes stored content.
|
|
type BlobInfo struct {
|
|
// SHA256 is the lower-case hex SHA-256 of the content, and its name in
|
|
// the store.
|
|
SHA256 string
|
|
Size int64
|
|
}
|
|
|
|
// BlobStore keeps media content, addressed by its SHA-256. The same bytes
|
|
// uploaded twice are stored once.
|
|
type BlobStore interface {
|
|
// Put stores what r yields, up to limit bytes; more is
|
|
// ErrBlobTooLarge and stores nothing.
|
|
Put(ctx context.Context, r io.Reader, limit int64) (BlobInfo, error)
|
|
|
|
// Open reads stored content.
|
|
Open(ctx context.Context, sha256 string) (io.ReadSeekCloser, error)
|
|
|
|
// Delete removes stored content. Deleting what is not there is not an
|
|
// error.
|
|
Delete(ctx context.Context, sha256 string) error
|
|
}
|
|
|
|
// BlobProvider is implemented by a storage driver that brings its own
|
|
// BlobStore. PocketBase will (PLAN.md section 4: its filesystem
|
|
// abstraction puts media on local disk or S3); a driver that does not is
|
|
// given the local filesystem store.
|
|
type BlobProvider interface {
|
|
Blobs() BlobStore
|
|
}
|