pluginsdk

package
v0.45.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 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