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
- Variables
- func Bool(cfg map[string]any, key string, def bool) bool
- func Int(cfg map[string]any, key string, def int) int
- func Run[T Backend](ctx context.Context, p Plugin[T]) error
- func Serve[T Backend](p Plugin[T])
- func String(cfg map[string]any, key string) string
- type Backend
- type Closer
- type Copier
- type Deleter
- type Event
- type Field
- type FieldType
- type Mkdirer
- type Mover
- type Multipart
- type Object
- type Part
- type Plugin
- type Presigned
- type Presigner
- type RangeReader
- type SelectOption
- type Toucher
- type Watcher
- type Writer
Constants ¶
const ( KindFile = "file" KindDir = "dir" KindLink = "symlink" )
ObjectKind values.
const Protocol = 1
Protocol is the version this SDK speaks.
Variables ¶
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 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 ¶
Run is Serve with a context you own — for tests and for embedding a plugin in a larger program.
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 Deleter ¶
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 Mkdirer ¶
Mkdirer creates a directory. Optional: object stores have none, and filex treats a missing Mkdir as a no-op rather than an error.
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 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 ¶
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 ¶
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.