Help us shape the future of Go! Take the 2026 Go Developer Survey

pluginsdk

package
v0.47.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package pluginsdk is the Go half of writing a filex storage plugin.

A plugin is a program. filex starts it (or connects to it), asks what it can do, and then speaks a small HTTP/JSON protocol to it — so a plugin can be written in any language. This package exists so that a plugin written in Go is a handful of methods rather than a web server: implement Backend, call Serve, and the handshake, routing, streaming, error codes and instance bookkeeping are done for you.

The whole of a working plugin:

package main

import (
	"context"
	"io"

	"github.com/brf-tech/filex/backend/pkg/pluginsdk"
)

type myFS struct{ root string }

func (m *myFS) List(ctx context.Context, path string) ([]pluginsdk.Object, error) { … }
func (m *myFS) Stat(ctx context.Context, path string) (pluginsdk.Object, error)   { … }
func (m *myFS) Read(ctx context.Context, path string) (io.ReadCloser, error)      { … }

func main() {
	pluginsdk.Serve(pluginsdk.Plugin[*myFS]{
		Name:  "myfs",
		Label: "My storage",
		Fields: []pluginsdk.Field{
			{Key: "root", Type: pluginsdk.FieldString, Label: "Root", Required: true, Root: true},
		},
		Open: func(ctx context.Context, cfg map[string]any) (*myFS, error) {
			return &myFS{root: pluginsdk.String(cfg, "root")}, nil
		},
	})
}

Write support is added by ALSO implementing Writer and Deleter on the same value; the SDK reports the capability only when the interface is there, so the capability and the code cannot disagree. The same applies to Mover, Copier, Mkdirer, RangeReader, Toucher and Watcher.

⚠ Run it under filex, not by hand: the process reads FILEX_PLUGIN_TOKEN and FILEX_PLUGIN_SOCKET_DIR from its environment and prints its handshake line on stdout. `FILEX_PLUGIN_LISTEN=127.0.0.1:PORT` runs it standalone for development (then register it in filex as a REMOTE plugin with that token).

Index

Constants

View Source
const (
	KindFile = "file"
	KindDir  = "dir"
	KindLink = "symlink"
)

ObjectKind values.

View Source
const Protocol = 1

Protocol is the version this SDK speaks.

Variables

View Source
var (
	// ErrNotFound — the path does not exist.
	ErrNotFound = errors.New("not found")
	// ErrReadOnly — this backend, or this path, refuses writes.
	ErrReadOnly = errors.New("read only")
	// ErrUnsupported — the operation is not available here.
	ErrUnsupported = errors.New("unsupported")
)

Errors a Backend returns. Anything else becomes a plain failure with its message forwarded to filex (and into the admin's error line).

Functions

func Bool

func Bool(cfg map[string]any, key string, def bool) bool

Bool reads a boolean config value, accepting "true"/"1" as well.

func Int

func Int(cfg map[string]any, key string, def int) int

Int reads an integer config value. JSON numbers arrive as float64, and a form may send "8080" as a string — both are accepted, so a plugin does not have to care which surface filled the form.

func Run

func Run[T Backend](ctx context.Context, p Plugin[T]) error

Run is Serve with a context you own — for tests and for embedding a plugin in a larger program.

func Serve

func Serve[T Backend](p Plugin[T])

Serve runs the plugin until the process is asked to stop. It never returns normally; a fatal error exits non-zero with a message on stderr, which filex relays into its own log.

func String

func String(cfg map[string]any, key string) string

String reads a string config value (empty when absent or another type).

Types

type Backend

type Backend interface {
	List(ctx context.Context, path string) ([]Object, error)
	Stat(ctx context.Context, path string) (Object, error)
	Read(ctx context.Context, path string) (io.ReadCloser, error)
}

Backend is one opened storage: what Open returned for one config. The three methods here are the minimum; everything else is an optional interface.

type Closer

type Closer interface {
	Close() error
}

Closer is called when filex releases an instance (storage edited, plugin stopping). Optional.

type Copier

type Copier interface {
	Copy(ctx context.Context, src, dst string) error
}

Copier copies server-side. Optional: filex falls back to read+write.

type Deleter

type Deleter interface {
	Delete(ctx context.Context, path string) error
}

Deleter removes an object. Deleting something that is not there is not an error. ⚠ filex requires Writer and Deleter TOGETHER: it will not offer a backend that can create files it cannot remove (trash and versioning both need to).

type Event

type Event struct {
	Op   string `json:"op"` // create | modify | delete | move
	Path string `json:"path"`
	From string `json:"from,omitempty"`
}

Event is one change notification (Watcher).

type Field

type Field struct {
	Key         string         `json:"key"`
	Type        FieldType      `json:"type"`
	Label       string         `json:"label"`
	Help        string         `json:"help,omitempty"`
	I18nKey     string         `json:"i18n_key,omitempty"`
	HelpI18nKey string         `json:"help_i18n_key,omitempty"`
	Required    bool           `json:"required"`
	Secret      bool           `json:"secret"`
	Default     any            `json:"default,omitempty"`
	Placeholder string         `json:"placeholder,omitempty"`
	Options     []SelectOption `json:"options,omitempty"`
	Min         *int           `json:"min,omitempty"`
	Max         *int           `json:"max,omitempty"`
	Monospace   bool           `json:"monospace,omitempty"`
	Multiline   bool           `json:"multiline,omitempty"`
	Advanced    bool           `json:"advanced,omitempty"`
	// Root marks the field that scopes this storage inside the backend (a
	// path, a prefix, a bucket). filex refuses to mount a backend's root, so
	// a plugin with no Root field can never be scoped — declare one.
	Root bool `json:"root,omitempty"`
}

Field describes one key of the config form filex shows when somebody adds a storage on this plugin. Same shape filex's built-in drivers use.

type FieldType

type FieldType string

FieldType picks the widget filex renders for a config field.

const (
	FieldString   FieldType = "string"
	FieldInt      FieldType = "int"
	FieldBool     FieldType = "bool"
	FieldPassword FieldType = "password"
	FieldSelect   FieldType = "select"
)

Field types.

type Mkdirer

type Mkdirer interface {
	Mkdir(ctx context.Context, path string) error
}

Mkdirer creates a directory. Optional: object stores have none, and filex treats a missing Mkdir as a no-op rather than an error.

type Mover

type Mover interface {
	Move(ctx context.Context, src, dst string) error
}

Mover renames server-side. Optional: filex falls back to copy+delete.

type Multipart

type Multipart interface {
	InitMultipart(ctx context.Context, path string, totalSize int64, partCount int) (uploadID string, partURLs []string, err error)
	UploadPart(ctx context.Context, path, uploadID string, partNumber int, r io.Reader, size int64) (etag string, err error)
	CompleteMultipart(ctx context.Context, path, uploadID string, parts []Part) error
	AbortMultipart(ctx context.Context, path, uploadID string) error
}

Multipart is a resumable upload in parts. Implementing it requires Writer and Deleter too — filex refuses the combination otherwise, because a resumable upload is still an upload.

PartURLs may be empty: filex then pushes each part itself through UploadPart, which is what the staged-upload path does (it holds the bytes, so it cannot hand a browser a URL).

type Object

type Object struct {
	Path     string            `json:"path"`
	Name     string            `json:"name"`
	Size     int64             `json:"size"`
	Kind     string            `json:"kind"`
	Mime     string            `json:"mime,omitempty"`
	Etag     string            `json:"etag,omitempty"`
	Mtime    time.Time         `json:"mtime,omitempty"`
	Metadata map[string]string `json:"metadata,omitempty"`
}

Object is one entry of a listing or a stat.

type Part

type Part struct {
	PartNumber int    `json:"part_number"`
	Etag       string `json:"etag"`
}

Part is one finished piece of a multipart upload.

type Plugin

type Plugin[T Backend] struct {
	Name    string // [a-z0-9][a-z0-9_-]{0,31}; filex registers it as plugin:<name>
	Version string
	Label   string // human name in the driver picker
	Fields  []Field
	Open    func(ctx context.Context, config map[string]any) (T, error)
	// SelfTest opens a THROWAWAY backend for filex's conformance probes.
	//
	// ⚠⚠ Provide it. filex probes every capability a plugin declares before
	// it will let anybody build a storage on it — because a plugin that claims
	// write and cannot write produces failures the user reads as filex being
	// broken. Without a self-test area the plugin is registered UNVERIFIED and
	// the probes run against somebody's first real storage instead.
	//
	// Return a backend over scratch space you are happy to have written to and
	// deleted (a temp directory, a throwaway prefix, an in-memory area). filex
	// releases it with DELETE /v1/instances/{id} when it is done.
	SelfTest func(ctx context.Context) (T, error)
}

Plugin is what Serve needs: identity, the config form, and how to open a backend for one config.

⚠ The type parameter is not ceremony — it is how the capabilities are derived. The SDK reports what T's METHOD SET implements (Writer, Toucher, Watcher …), computed from the type itself, so a plugin can neither claim a capability it did not write nor hide one it did. Nothing is opened to find out, which also means describing a plugin never touches the backend.

type Presigned

type Presigned struct {
	URL       string            `json:"url"`
	Method    string            `json:"method,omitempty"`
	Headers   map[string]string `json:"headers,omitempty"`
	ExpiresAt time.Time         `json:"expires_at"`
}

Presigned is one signed URL.

type Presigner

type Presigner interface {
	PresignUpload(ctx context.Context, path string, size int64) (Presigned, error)
	PresignDownload(ctx context.Context, path string, ttl time.Duration) (Presigned, error)
}

Presigner hands out URLs the CLIENT uses directly, so bytes never pass through filex.

⚠ Only implement this when the URL is reachable from where the client is — a browser, not the filex host. A plugin that returns a loopback URL passes conformance (which only checks that a URL comes back and parses) and then fails in every browser.

type RangeReader

type RangeReader interface {
	ReadRange(ctx context.Context, path string, off, length int64) (io.ReadCloser, error)
}

RangeReader lets filex fetch a byte window without pulling the whole object. length < 0 means "to the end". An offset at or past EOF is NOT an error — return a reader that is immediately at EOF.

⚠ Implement this only if the backend can genuinely start a transfer at an offset. filex emulates ranges for plugins that do not, which is slower but always correct; a driver that returned the object from byte 0 while claiming a range would hand corrupt data to a video player.

type SelectOption

type SelectOption struct {
	Value   string `json:"value"`
	Label   string `json:"label"`
	I18nKey string `json:"i18n_key,omitempty"`
}

SelectOption is one choice of a FieldSelect field.

type Toucher

type Toucher interface {
	SetMtime(ctx context.Context, path string, mtime time.Time) error
}

Toucher sets an object's modification time — what carries a file's own timestamp through a sync instead of stamping it "now".

⚠ Implement it only if the backend really stores it. filex can tell "not supported" from "supported and applied"; it cannot tell "applied" from "pretended", and a plugin that accepts an mtime and drops it makes every sync run copy everything again.

type Watcher

type Watcher interface {
	Watch(ctx context.Context) (<-chan Event, error)
}

Watcher streams change events until ctx is cancelled. Close the channel when the stream ends.

⚠⚠ Nothing in filex subscribes to this yet. The protocol carries the stream and this SDK serves it, but the only event-driven sync mode today is fsnotify, which works on the local driver alone; no built-in driver implements storage.Watcher either. Implementing it is cheap and forward compatible — just do not expect it to change how often filex notices your changes until a watch-driven sync mode exists.

type Writer

type Writer interface {
	Write(ctx context.Context, path string, r io.Reader, size int64) error
}

Writer accepts uploads. size is -1 when the length is not known up front.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL