storage

package
v0.56.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package storage provides the abstract Storage Driver interface used to front any byte-stream backend (local FS, S3, SFTP, WebDAV, …).

The base Driver interface is intentionally tiny — additional capabilities are advertised through optional sub-interfaces (Writer, Mover, Copier, Deleter, Mkdirer, Presigner, Watcher), AList-style. The Capabilities helper computes the runtime feature set by checking which sub-interfaces the concrete driver satisfies.

Index

Constants

View Source
const (
	// LazyFillKey chooses the behaviour: LazyFillBackground (A, the default)
	// or LazyFillOnOpen (B).
	LazyFillKey = "lazy_fill"
	// LazyMaxWatchesKey caps the fsnotify watches on visited folders.
	LazyMaxWatchesKey = "lazy_max_watches"
	// LazyWatchTTLKey is how many minutes a folder nobody opens keeps its watch.
	LazyWatchTTLKey = "lazy_watch_ttl"

	LazyFillBackground = "background"
	LazyFillOnOpen     = "on_open"

	LazyMaxWatchesDefault = 1024
	LazyWatchTTLDefault   = 60 // minutes
)

Lazy catalogue settings (sync_mode `lazy`, issue #45 — docs/LAZY-CATALOGUE.md). They live in the storage's config map beside ScanExcludeKey, and like it they are the scan's, not the driver's: Init never reads them.

View Source
const (
	MetaLinkState = "link_state"
	// MetaLinkTarget carries the driver's own resolved identity for a FOLLOWED
	// directory link — the thing CycleGuard keys on. Only directory links need
	// it, because only a directory link can send a walk round in a circle, and
	// resolving one is the expensive call (measured 1.1 ms on Windows), so it
	// is not paid for file links or for links that were not followed.
	MetaLinkTarget = "link_target"

	// LinkFollowed — the link resolved inside the root and the Object
	// describes its TARGET. Carried so a surface can still badge the row as a
	// link; the Kind, size and mtime are the target's.
	LinkFollowed = "followed"
	// LinkOutsideRoot — the target is outside the storage root and the
	// storage's follow_symlinks option is off.
	LinkOutsideRoot = "outside_root"
	// LinkBroken — the link resolves to nothing at all.
	LinkBroken = "broken"
	// LinkUnresolved — the driver knows this is a link and deliberately did
	// not follow it, because it has no way to tell an in-root target from an
	// out-of-root one. Remote backends say this: filex's boundary there is the
	// account's own permissions, so following a link would leave the
	// configured root with nothing to stop the walk.
	LinkUnresolved = "unresolved"
)

Metadata key and values a driver uses to say WHY an entry came back as KindSymlink instead of as the thing it points at.

A driver that can follow a link reports the TARGET's kind, so an entry left as KindSymlink is always one the caller cannot open. Without a reason attached, the UI has a row it must refuse to act on and nothing to tell anyone — which is exactly how issue #34 was experienced: a 0-byte file that would not open and gave no explanation.

View Source
const MaxWalkDepth = 64

MaxWalkDepth bounds every recursive walk in filex.

It is the backstop, not the guard: it catches a cyclic tree no driver reported a link for — a plugin backend, or a future driver that resolves links without saying so — and it catches it at a depth no real storage reaches. Deepest directory measured on the storages filex runs against is well under 20; a symlink cycle reached depth 81 on Linux and 127 on Windows before the OS's own link limit stopped it, so 64 sits clear of the first and well inside the second.

View Source
const MinBackendPartSize = 5 * 1024 * 1024

MinBackendPartSize is the smallest non-final multipart part S3 accepts. Anything smaller is rejected with EntityTooSmall at CompleteMultipartUpload time — that is, after every byte has already been sent.

View Source
const MutationCeiling = 2 * time.Hour

MutationCeiling bounds a change that runs on after its caller has gone (see DetachMutation). Generous on purpose: every request a driver sends has a timeout of its own, so this only has to stop a walk that never ends.

View Source
const ScanExcludeKey = "scan_exclude"

ScanExcludeKey is the storage config key holding a storage's scan exclusions: glob patterns, one per line, for paths the scan must not walk. internal/scanrule reads it; the key lives here so the descriptor that offers it and the code that reads it name the same thing.

View Source
const SecretMask = "***"

SecretMask is what an admin surface shows in place of a stored credential in a driver configuration - a storage's or a replication target's. A save that sends it back means "keep the one I was shown" (KeepSecrets).

Variables

View Source
var ErrChangedMeanwhile = errors.New("storage: the item changed while the change waited for its turn")

ErrChangedMeanwhile is a source that is not the item the change was checked against any more by the time the change held its storage's row gate: gone, replaced, or rewritten. A request answers it 412 Precondition Failed.

View Source
var ErrKindConflict = errors.New("storage: path exists with a different kind")

ErrKindConflict is returned when a write target already exists as the OTHER kind: a file written onto an existing directory, or a directory created onto an existing file.

On a real filesystem the OS refuses this for us — os.Create on a directory fails, and the error surfaces. An object store has no such rule: keys are flat, so `X` (an object) and `X/y` (an object under a prefix) can both exist and the write succeeds silently. Hetzner Object Storage accepts it; MinIO, which is directory-backed, cannot represent it.

What that cost us (2026-08-06, a DR mirror): the mirror could never settle the colliding prefix, so `mc mirror` re-copied it every run — 2760 syncs in 24h, 1016 versions of a single PNG, a 43 MiB folder occupying 45 GB, and the disk at 96%. Worse and quieter: the colliding object made everything under that prefix unlistable, so 314 objects had no DR backup at all and nothing reported it.

View Source
var ErrNotFound = errors.New("storage: not found")

ErrNotFound is returned by drivers when a path does not exist.

View Source
var ErrReadOnly = errors.New("storage: read-only")

ErrReadOnly is returned when a write op hits a read-only mount.

View Source
var ErrRootPathForbidden = errors.New("ROOT_PATH_FORBIDDEN: storage prefix/path cannot be empty or root '/'; use a sub-folder like 'fileman' or 'data/files'")

ErrRootPathForbidden is returned when a storage is configured with an empty or root path/prefix. Operators must always scope a Storage to a sub-folder (e.g. "fileman", "data/files") so that filemanager does not shadow or inadvertently take ownership of pre-existing files at the bucket / FS root.

View Source
var ErrSecretAddressChanged = errors.New("storage: a saved credential is only sent where it was saved")

ErrSecretAddressChanged is what KeepSecrets answers for a credential that came back masked while the address it would be sent to changed (or the driver did). Wrapped in a *SecretAddressError naming the field.

View Source
var ErrTakenMeanwhile = fmt.Errorf("%w: the destination was taken while the change waited for its turn", os.ErrExist)

ErrTakenMeanwhile is a destination that was free when the change was checked and holds an item by the time the change held its storage's row gate. It is an os.ErrExist, so every surface answers it as the conflict it already answers for a taken name (409, WebDAV's refusal, SSH_FX_FAILURE).

View Source
var ErrUnavailable = errors.New("storage: unavailable")

ErrUnavailable is wrapped around a failure that means the backend could not be reached or would not answer (connection refused, no answer in time, a name that does not resolve, a 5xx after the retries): the storage is down, the request was fine. Answer it with 503 and "try again later", never with "check your input". A refusal the backend answered (403, a missing bucket) is not this.

View Source
var ErrUnsupported = errors.New("storage: unsupported")

ErrUnsupported is returned when an op is not supported by this driver.

Functions

func CloseDriver added in v0.50.0

func CloseDriver(d Driver)

CloseDriver releases what a driver holds once nothing will use it again - a connection (sftp, ftp, smb), or the instance a storage plugin keeps with the storage's configuration and credentials in it. Driver does not require Close, so this is best effort by contract: a driver without one holds nothing worth releasing.

func ConfigInt

func ConfigInt(v any) (int, bool)

ConfigInt reads an integer from a driver config value that may arrive as an int (YAML / direct), a float64 (JSON numbers unmarshal to float64), or a numeric string (form input). This is why e.g. an sftp "port" set via a JSON config still resolves correctly.

func ConfigLookup

func ConfigLookup(cfg map[string]any, key string, aliases ...string) (any, bool)

ConfigLookup returns the first non-empty value for key or any alias. Empty strings count as absent — a blank text input is not a value.

func ConfigString

func ConfigString(cfg map[string]any, key string, aliases ...string) string

ConfigString is ConfigLookup narrowed to strings.

func DetachMutation added in v0.47.0

func DetachMutation(ctx context.Context) (context.Context, context.CancelFunc)

DetachMutation is the context a change to the storage runs under once every check has passed: the values of ctx — the caller, the tenant, the audit record — and none of its cancellation. Every surface that renames, moves, trashes, restores or purges a folder asks it here: the HTTP handlers (the explorer, the trash, the agent surface) and WebDAV alike.

⚠⚠ net/http cancels r.Context() the moment the client's connection closes: a tab closed, a proxy that stopped waiting (nginx after 60 s by default, Cloudflare after 100 s, the admin SPA after 30 s), an agent that gave up on a slow tool call, a WebDAV client that timed out a DELETE or a MOVE. On an object store a folder is changed one object at a time, and the SDK refuses every request after that point. The folder was left in two places, with the catalogue still describing the old one, and a retry was refused because the half that had arrived already held the name.

Finishing is the only way the storage and the catalogue end up agreeing. Nobody may be waiting for the response any more, but the next listing is.

func EmptyReadCloser

func EmptyReadCloser() io.ReadCloser

EmptyReadCloser returns a ReadCloser that is immediately at EOF.

func EnsureDirTarget

func EnsureDirTarget(ctx context.Context, d Driver, p string) error

EnsureDirTarget refuses to create a DIRECTORY at p when p already exists as a file — the same collision approached from the other side.

func EnsureFileTarget

func EnsureFileTarget(ctx context.Context, d Driver, p string) error

EnsureFileTarget refuses to let a FILE be written at p when p already exists as a directory. Call it before every user-facing write.

This check only became expressible in v0.9.0: before it, the S3 driver's Stat answered ErrNotFound for a bare prefix, so "is this a folder?" had no answer on an object store. Stat now resolves a prefix to KindDirectory.

func Exists

func Exists(ctx context.Context, d Driver, p string) bool

Exists reports whether anything occupies p on d. Only a definite ErrNotFound reads as free: any other Stat failure (a timeout, a 403, a 503) counts as TAKEN, because every caller asks in order to avoid putting bytes on top of something, and "I could not check" must not read as "nothing is there".

func FenceAt added in v0.55.0

func FenceAt(ctx context.Context, d Driver, paths ...string) []string

FenceAt is FenceFor asking d whether paths[0], the change's source, is a folder: one Stat, and only on an object store. A Stat that fails answers nil (the change holds the gate, as before).

func FenceFor added in v0.55.0

func FenceFor(d Driver, dir bool, paths ...string) []string

FenceFor answers the prefixes a change of a folder must fence on d instead of holding the storage's row gate (rowgate.HoldCtx): paths - the folder's path and, for a move, its destination's - when the change is a folder (dir) moved or trashed object by object (ObjectByObject); nil for every other change, which holds the gate as before. Empty paths are left out (a trash's destination is the storage's trash, which no scan walks).

func InternalPath added in v0.53.0

func InternalPath(p string) bool

InternalPath reports whether p is in one of filex's own folders - the trash, version history, legacy thumbnails, the desktop's open-with working copies, the drafts - at any depth (syspath.InDir). Replication leaves them out, before every rule and whatever a rule says (the maintainers, 2026-10-06): they are filex's machinery, tied to rows in its database, not anybody's files, and a backup of them restores nothing a person could open. A file that moves INTO one of them (to the trash) leaves the backup like a delete; one that comes OUT (a restore) reaches it like a new file (ReplicatedDriver.Move and Copy).

func IsSecretField added in v0.53.0

func IsSecretField(driver, key string) bool

IsSecretField reports whether key holds a credential in driver's configuration: a field (or an alias of one) the driver's descriptor marks Secret, or a key that is a credential by name (secretWords).

func KeepSecrets added in v0.53.0

func KeepSecrets(driver string, next json.RawMessage, prevDriver string, prev json.RawMessage) (json.RawMessage, error)

KeepSecrets merges a configuration an admin surface sent back with the one stored (prev, on prevDriver): a credential that comes back as SecretMask is the stored one - the surface never had it to send.

  • With nothing stored (a create; prev nil) a mask is dropped rather than saved as three asterisks.
  • ⚠ A mask is only kept while the credential would go where it was saved: the same driver and the same address (host, endpoint, url, …). Otherwise the answer is a *SecretAddressError naming the field to type again. Without this, anybody who may edit a storage but not read its password - an admin API key, an MCP agent - could point it at a server of their own and have filex send the password there.

func Landed

func Landed(ctx context.Context, d Driver, p string, size int64) (int64, string, time.Time)

Landed reads back what a write just left at p — its size, etag and modification time — from the driver's own Stat, for the caller to record on the node row.

⚠ The alternative every overwrite path used to take was to keep the row's etag, i.e. the etag of the file that had just been REPLACED. On a backend that reports etags (S3, WebDAV) that value is load-bearing: content search re-extracts a file's text only when its fingerprint moves, and the fingerprint is the etag (search.ContentFingerprint); clients tell a changed file from an unchanged one by the listing's etag; and a scan that finds the etag it recorded believes nothing happened.

⚠⚠ The mtime is the DRIVER's too, not time.Now(). The row is what every listing reports, and the storage scan later compares it with the driver: a handler-clock mtime that straddles a second boundary reads as drift and is rewritten, so the file's last_modified CHANGES with no byte of it changing (measured on a local storage: a desktop upload listed as ...692000 became ...692587 after one scan). A sync client sees that as a new server version and downloads its own upload back, and its `expect` precondition — the listing signature <size>:<mtime-ms> — no longer matches the file it wrote.

When Stat fails — or answers with something that is not a file — the answer is the caller's size, an EMPTY etag and now. An empty etag never matches the backend's on the next scan, so the row is corrected there, and the search fingerprint falls back to size+mtime at once. It is never the old etag.

func LimitReadCloser

func LimitReadCloser(rc io.ReadCloser, n int64) io.ReadCloser

LimitReadCloser caps rc at n bytes while still closing the underlying stream — the shape every RangeReader needs when the backend can start at an offset but not stop at one (local seek, SFTP seek, FTP REST). n < 0 returns rc untouched ("to the end of the object").

func MaskSecrets added in v0.53.0

func MaskSecrets(driver string, raw json.RawMessage) json.RawMessage

MaskSecrets returns a driver configuration with every credential that is set replaced by SecretMask, for an admin surface to show. An empty value stays empty (it says "none set"); a configuration that is not a JSON object is returned as it is.

⚠ Every read of a configuration an administrator's browser, an API key or an MCP client receives goes through this - a storage's (/api/admin/storages) and a replication target's: an S3 secret key, an SMB or SFTP password, a private key. They are written, never read back.

func Names

func Names() []string

Names returns the registered driver names sorted alphabetically.

func ObjectByObject added in v0.55.0

func ObjectByObject(d Driver) bool

ObjectByObject reports whether d moves, copies and trashes a folder one object at a time: an object store (it walks a tree in one pass, TreeWalker), directly or as a replicated storage's primary. Moving or trashing a folder there takes as long as the folder is large - minutes, hours - where a storage with real folders renames it in one call; such a change fences the folder's prefixes rather than holding the storage's row gate (internal/rowgate FenceCtx, FenceFor).

func ReadAhead added in v0.50.0

func ReadAhead(fr io.ReadCloser, buffer int, length int64) io.ReadCloser

ReadAhead gives a file reader its read-ahead: buffer bytes, or less for a short ranged read (length > 0), never under 32 KiB. Close closes fr.

func RefineOfficeMime

func RefineOfficeMime(detected, filename string) string

RefineOfficeMime upgrades the generic "application/zip" detected by http.DetectContentType to the registered OOXML/ODF MIME when the file has an office extension. Returns the input unchanged otherwise.

Drivers and serving handlers call this BEFORE returning a stored or freshly-sniffed MIME to clients. The motivation: OnlyOffice Document Server refuses pptx/docx/odt sources when the fetch Content-Type is "application/zip" even though the JWT-signed config says fileType=pptx. xlsx happens to be lenient; the rest aren't.

func Register

func Register(name string, f Factory)

Register adds a driver factory, called from drivers' init().

func RegisterDescriptor

func RegisterDescriptor(d Descriptor)

RegisterDescriptor records a driver's config contract. Called from the driver package's init(), next to storage.Register.

func SeekOpened added in v0.50.0

func SeekOpened(f interface {
	io.Seeker
	io.Closer
}, off int64) error

SeekOpened moves a file just opened for a ranged read to off (nothing to do at 0) and closes it when that fails, so a failed open leaves no handle behind. The shape the SFTP and SMB drivers share.

func StillAsSeen added in v0.55.0

func StillAsSeen(ctx context.Context, drv Driver, rel string, seen Object) error

StillAsSeen asks whether rel is still the object seen (a Stat taken when the change was checked): there, of the same kind, and - for a file - of the same size, ETag and modification time wherever both answers carry one. A folder's own time changes with its contents, so a folder is compared by kind alone. Answers nil, ErrChangedMeanwhile, or the Stat's own error.

func StillFree added in v0.55.0

func StillFree(ctx context.Context, drv Driver, dst, src string) error

StillFree asks whether dst is still free on drv. src is the change's own source: on a storage that folds case, a change of case alone finds the source itself at dst, which does not make dst taken. Answers nil, ErrTakenMeanwhile, or the Stat's own error.

func Unregister

func Unregister(name string)

Unregister removes a driver factory. Built-in drivers never call this; it exists for storage PLUGINS (internal/plugin), which come and go while the process runs — an operator disables one, replaces its binary, or the process behind it is gone for good. Unknown names are a no-op.

⚠ Storages already opened on the driver keep their live instance; the server's resolver caches it. Unregistering only refuses NEW instances.

func UnregisterDescriptor

func UnregisterDescriptor(name string)

UnregisterDescriptor forgets a driver's config contract — the plugin counterpart of Unregister. Unknown names are a no-op.

func ValidateLazyConfig added in v0.44.2

func ValidateLazyConfig(cfg map[string]any) error

ValidateLazyConfig checks the lazy catalogue's settings in a storage's config map against LazyFields(): the behaviour must be one of its options and the two numbers within their bounds. A key that is absent (or empty) is fine — the default applies. The admin API refuses a storage that fails.

func ValidateNonRootPath

func ValidateNonRootPath(driver string, cfg map[string]any) error

ValidateNonRootPath returns ErrRootPathForbidden if the configured driver path/prefix/root is empty or evaluates to the filesystem/bucket root after trimming whitespace and slashes.

The field to look at comes from the driver's Descriptor — the one field flagged Root, plus its legacy aliases. That is deliberate: this function used to carry its own per-driver key list, which is exactly how it drifted away from the admin form (the form collected "base_path", the validator read "root", every submit 400'd). One declaration, both sides.

Drivers without a descriptor keep the historical behaviour:

s3              → "prefix"
local           → "path" (preferred), then "root"
ftp/sftp/webdav → "root" (preferred), then "remote_path"

Unknown drivers fall back to a sweep of the common keys so future drivers are protected by default. Callers should invoke this from API handlers (Storage create / update) BEFORE persisting the row.

func WithTally added in v0.47.0

func WithTally(ctx context.Context, t *Tally) context.Context

WithTally returns a context whose driver calls count into t.

Types

type Capabilities

type Capabilities struct {
	Read bool `json:"read"`
	// Range reports ranged reads (storage.RangeReader). Callers that serve
	// HTTP bodies use it to decide between http.ServeContent (seekable,
	// 206/Content-Range) and a whole-object io.Copy.
	Range   bool `json:"range"`
	Write   bool `json:"write"`
	Move    bool `json:"move"`
	Copy    bool `json:"copy"`
	Delete  bool `json:"delete"`
	Mkdir   bool `json:"mkdir"`
	Presign bool `json:"presign"`
	Watch   bool `json:"watch"`
}

Capabilities advertises the Storage Driver's operation set.

func ComputeCapabilities

func ComputeCapabilities(d Driver) Capabilities

ComputeCapabilities introspects a Driver to figure out which optional interfaces it implements. Drivers that lie in their static Capabilities() are still trumped by reality here.

type Collision

type Collision struct {
	Path string `json:"path"`
	// Dir is the listing the pair was found in ("" = storage root).
	Dir string `json:"dir"`
}

Collision is one path that exists as BOTH a file and a folder.

func ScanKindCollisions

func ScanKindCollisions(ctx context.Context, d Driver, root string) ([]Collision, error)

ScanKindCollisions walks root and reports every name that a listing returns twice — once as a file, once as a folder.

This finds damage that predates the write guards (EnsureFileTarget / EnsureDirTarget): an object store accepted `X` and `X/…` side by side, and nothing surfaced it. The consequences were only visible downstream — a DR mirror that re-copied the prefix on every run and, worse, a prefix whose contents no longer listed at all, so its objects were silently unbacked.

Detection is cheap because the collision is visible in a single listing: S3 returns `X` in Contents and `X/` in CommonPrefixes, so the driver yields two entries with the same name and different kinds. No full key enumeration is needed.

A directory that fails to list is skipped rather than aborting the scan — a colliding prefix is exactly the thing that may refuse to list, and giving up there would hide the rest of the storage.

type Copier

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

Copier copies objects (server-side if possible).

type CycleGuard

type CycleGuard struct {
	// contains filtered or unexported fields
}

CycleGuard stops a recursive walk from following a symlinked directory back into a tree it is already inside.

⚠⚠ Why this had to exist BEFORE symlinked directories became walkable. Until v0.43.0 the catalogue walk descended only on Kind == KindDirectory and the local driver called every link KindSymlink, so a linked directory was catalogued as a childless leaf. That accident — not any guard — was the only thing keeping the walk finite. Measured with a wrapper that relabelled links as directories, on a root containing `real/cycle -> root`:

Linux    depth 81,  123 visits
Windows  depth 127, 192 visits

before the operating system's symlink limit ended it. Every visit re-catalogued the same subtree under a different path, so each pass minted a fresh set of rows with distinct path hashes: the walk does not merely spin, it fills the catalogue with duplicates of real files.

⚠ A `seen` set keyed on the LOGICAL path does not help and looks like it does. The paths in a cycle are all different — /real, /real/cycle/real, /real/cycle/real/cycle/real — which is exactly why kindscan's existing seen[dir] map never stopped one.

The key is the driver's own resolved identity for the link (Object.Metadata under MetaLinkTarget), so two different paths naming one directory collapse to one entry. A driver that cannot resolve targets reports nothing, and such a walk is bounded by MaxWalkDepth alone.

⚠ A link is recorded when the walk DESCENDS into it, not when the root is opened, so a cycle is traversed once before it is recognised. That is deliberate and it is the cheap half of the trade: a link genuinely is a second view of that data, one extra pass is bounded work, and seeding the set would need a real identity for the root that only some drivers can give.

func NewCycleGuard

func NewCycleGuard() *CycleGuard

NewCycleGuard returns a guard for one walk. It is not safe for concurrent use; each walk owns its own.

func (*CycleGuard) Enter

func (g *CycleGuard) Enter(o Object, depth int) bool

Enter reports whether a walk at the given depth may descend into o, and records o as visited when it may.

depth is the recursion depth about to be entered (the storage root is 0).

type Deleter

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

Deleter removes objects (idempotent — no error on missing).

type Descriptor

type Descriptor struct {
	Driver string `json:"driver"`
	// Label / I18nKey name the driver in a picker.
	Label   string  `json:"label"`
	I18nKey string  `json:"i18n_key"`
	Fields  []Field `json:"fields"`

	// Capabilities is the driver's runtime feature set (ComputeCapabilities
	// on a fresh instance). Surfaces want it in the same payload: a picker
	// can say "no presigned URLs" before anything is saved.
	Capabilities Capabilities `json:"capabilities"`

	// ScanFields are the settings a STORAGE on this driver has for the scan
	// that catalogues it (ScanFields()), as opposed to Fields, which the
	// driver's Init reads. They live in the same config map and are drawn by
	// the same form component, but only where a storage is edited: a
	// replication target is never scanned, so its dialog leaves them out.
	// Filled by Descriptors(); the registry does not hold them, which keeps
	// TestDescriptorMatchesInit about what Init reads.
	ScanFields []Field `json:"scan_fields,omitempty"`

	// LazyFields are the lazy catalogue's settings (LazyFields()), offered
	// only for a driver a storage may be catalogued lazily on
	// (model.LazyDrivers). Same map, same renderer as ScanFields; filled by
	// the admin descriptor endpoint, which knows which drivers those are.
	LazyFields []Field `json:"lazy_fields,omitempty"`
}

Descriptor is a driver's complete, machine-readable config contract.

func DescriptorFor

func DescriptorFor(name string) (Descriptor, bool)

DescriptorFor returns the descriptor for a driver name.

func Descriptors

func Descriptors() []Descriptor

Descriptors returns every registered descriptor, driver-name sorted, with Capabilities computed from a fresh driver instance. Descriptors without a registered factory (test doubles) come back with the zero capability set rather than being dropped.

func (Descriptor) Field

func (d Descriptor) Field(key string) (Field, bool)

Field returns the field with the given key.

func (Descriptor) Keys

func (d Descriptor) Keys() []string

Keys returns every declared key plus its aliases — the complete set of config keys this driver understands.

func (Descriptor) MissingRequired

func (d Descriptor) MissingRequired(cfg map[string]any) []string

MissingRequired lists required fields absent (or blank) from cfg, honouring aliases. Fields carrying a Default are never "missing" — the driver fills them in.

func (Descriptor) RootField

func (d Descriptor) RootField() (Field, bool)

RootField returns the field flagged Root, if the driver has one.

type Driver

type Driver interface {
	Init(ctx context.Context, cfg map[string]any) error
	Name() string
	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)
	Capabilities() Capabilities
}

Driver is the minimum surface a backend must implement. All paths use POSIX-style forward slashes and are relative to the storage root.

func Get

func Get(name string) (Driver, error)

Get returns a freshly-constructed driver instance, error if unknown.

func UnavailableDriver added in v0.53.0

func UnavailableDriver(name string, err error) Driver

UnavailableDriver stands in for a replica that could not be opened (its configuration is refused by the driver, its driver is not registered - a plugin that is not running). The storage keeps working; every change that would reach the backup is recorded as a REPLICA_UNAVAILABLE failure with err, so the problem is on the Replication page and in the bell instead of in a log line nobody reads, and Fix all replays the backlog once the target is repaired.

type Event

type Event struct {
	Op   string // "create", "modify", "delete", "move"
	Path string
	From string // populated for move events only
}

Event is emitted by Watcher-capable drivers.

type EventNotifier

type EventNotifier interface {
	NotifyReplicaFail(ctx context.Context, path, op string, err error, attempt int)
	NotifyPrimaryReadFail(ctx context.Context, path string, err error)
}

EventNotifier emits replica events to the notify subsystem. We keep this as a tiny interface so the storage package doesn't import notify (and notify can't import storage either — there's already a dep in the other direction via handlers).

type Factory

type Factory func() Driver

Factory builds a fresh Driver instance — invoked once per Storage row.

type FailureRecorder

type FailureRecorder interface {
	Record(ctx context.Context, path, op, errCode, errMsg string) error
	Resolve(ctx context.Context, path, op string) error
}

FailureRecorder is the persistence sink for replica errors.

The implementation lives in the replica/recorder.go subpackage and wraps db.Store; storage uses only this thin interface so it can avoid pulling the DB driver in tests. One recorder belongs to one storage: the rows it writes carry that storage's id.

type Field

type Field struct {
	Key  string    `json:"key"`
	Type FieldType `json:"type"`

	// Label / Help are the English fallbacks. Surfaces resolve I18nKey /
	// HelpI18nKey first and only fall back to these — never render a
	// hardcoded English string when a translation exists.
	Label       string `json:"label"`
	Help        string `json:"help,omitempty"`
	I18nKey     string `json:"i18n_key"`
	HelpI18nKey string `json:"help_i18n_key,omitempty"`

	Required bool `json:"required"`
	// Secret marks credential material: surfaces must not echo it in
	// logs/URLs and should render a masked input.
	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 hints that the value is a path / URL / key blob.
	Monospace bool `json:"monospace,omitempty"`
	// Multiline hints at a textarea (PEM blobs).
	Multiline bool `json:"multiline,omitempty"`
	// Advanced fields are collapsed by default — rarely needed knobs.
	Advanced bool `json:"advanced,omitempty"`

	// Root marks THE field that scopes the storage inside the backend
	// (s3 prefix, local path, sftp/ftp/webdav root). ValidateNonRootPath
	// reads this field instead of its own hardcoded per-driver list.
	Root bool `json:"root,omitempty"`

	// Aliases are legacy keys Init still reads for this field.
	Aliases []string `json:"aliases,omitempty"`
}

Field describes exactly one key of a driver's config map.

Key is the wire name — what lands in storages.config_json and what the driver's Init reads. Aliases are historical spellings that Init still honours so rows written before this descriptor existed keep working.

func LazyFields added in v0.44.2

func LazyFields() []Field

LazyFields returns the lazy catalogue's settings for a storage on a driver that supports it (model.LazyDrivers), in the shape every other storage field has, so the storage form draws them with the same renderer. A fresh slice: callers cannot edit the set.

func ScanFields

func ScanFields() []Field

ScanFields returns the scan settings every storage has, whatever its driver. A fresh slice: callers cannot edit the set.

⚠ The help text says what the setting is NOT, on purpose: an excluded path is not walked, catalogued, indexed, thumbnailed or virus-scanned, but it is still on the storage and still served to whoever asks for it by path — the file protocols and the AI tools read the storage directly. It saves work; it is not an access control.

type FieldType

type FieldType string

FieldType tells a surface which widget to render for a config field.

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

Field types. Keep this list small — a surface that meets an unknown type should fall back to a plain text input rather than hide the field.

type Mkdirer

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

Mkdirer creates directories — drivers without a directory concept may no-op or persist a placeholder.

type Mover

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

Mover renames/moves objects.

type MultipartUploader

type MultipartUploader interface {
	InitMultipart(ctx context.Context, path string, totalSize int64, partCount int) (uploadID string, partURLs []string, err error)
	CompleteMultipart(ctx context.Context, path string, uploadID string, parts []PartCompletion) error
	AbortMultipart(ctx context.Context, path string, uploadID string) error
}

MultipartUploader is implemented by drivers (S3, GCS, Azure) that support multipart uploads. partURLs is historical: since 0.54 no caller reads it (the presigned browser upload was removed) and a driver may return nil.

type Object

type Object struct {
	Path     string            `json:"path"` // logical path within storage (POSIX-style)
	Name     string            `json:"name"` // basename
	Size     int64             `json:"size"`
	Kind     ObjectKind        `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 a backend-agnostic representation of a single FS entry.

type ObjectKind

type ObjectKind string

ObjectKind enumerates Object types.

const (
	KindFile      ObjectKind = "file"
	KindDirectory ObjectKind = "dir"
	KindSymlink   ObjectKind = "symlink"
)

type PartCompletion

type PartCompletion struct {
	PartNumber int
	Etag       string
}

PartCompletion is a finished multipart segment (stored in DB during upload).

type PartUploader

type PartUploader interface {
	MultipartUploader
	UploadPart(ctx context.Context, path, uploadID string, partNumber int, r io.Reader, size int64) (etag string, err error)
}

PartUploader is MultipartUploader plus a SERVER-SIDE part upload.

MultipartUploader on its own only starts, completes and aborts an upload. The staged upload path (internal/staging, docs/UPLOADS.md) holds the bytes itself and must push them, so it needs a way to send one part from an io.Reader and get its ETag back.

⚠ The part boundaries here are the DRIVER's, not the client's. A client that staged 1 MiB chunks must not break an S3 backend, where every non-final part has to be at least MinBackendPartSize — so the commit step re-chunks.

type PresignedUpload

type PresignedUpload struct {
	URL       string            `json:"url"`
	Method    string            `json:"method"`
	Headers   map[string]string `json:"headers,omitempty"`
	UploadID  string            `json:"upload_id,omitempty"` // for multipart
	PartSize  int64             `json:"part_size,omitempty"`
	PartCount int               `json:"part_count,omitempty"`
	PartURLs  []string          `json:"part_urls,omitempty"`
	ExpiresAt time.Time         `json:"expires_at"`
}

PresignedUpload holds the URL/method/headers a browser uses for direct uploads.

type Presigner

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

Presigner returns URLs for browser-direct upload/download.

type RangeReader

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

RangeReader adds ranged (partial) reads, so a caller can fetch a byte window without pulling the whole object. Implemented only by backends that can genuinely start a transfer at an offset — a driver that silently returns the object from byte 0 would hand corrupt data to http.ServeContent, so leaving this unimplemented is always correct and callers must be able to fall back to Read.

Contract:

  • off is an absolute offset from the start of the object and must be >= 0; a negative off is an error (suffix ranges are resolved by the caller, which knows the size).
  • length < 0 means "to the end of the object". length == 0 yields a reader that is immediately at EOF, without touching the backend.
  • off at or past EOF is NOT an error: the returned ReadCloser reports io.EOF on the first Read. (S3 answers 416 for that case and the driver maps it back; local/SFTP get it for free.) The caller cannot always know the current size — it may have shrunk since Stat — so a short read is the honest answer, not a failure.
  • length beyond EOF is clamped by the backend: the reader ends early rather than erroring.
  • A missing path returns ErrNotFound, exactly like Read.

The returned ReadCloser must be closed by the caller.

type Reloader

type Reloader interface {
	Reload() error
}

Reloader is the optional Reload method exposed by rulesEngine.

type ReplicaMode

type ReplicaMode string

ReplicaMode is the return type of RuleEngine.Match.

const (
	ModeMirror     ReplicaMode = "mirror"
	ModeAppendOnly ReplicaMode = "append_only"
	ModeSkip       ReplicaMode = "skip"
)

Mode values, mirroring model.ReplicaMode* but typed for the rules engine to avoid cross-package magic strings.

type ReplicatedDriver

type ReplicatedDriver struct {
	// contains filtered or unexported fields
}

ReplicatedDriver is the wrapper Driver. Construct via NewReplicated.

func AsReplicated added in v0.53.0

func AsReplicated(d Driver) (*ReplicatedDriver, bool)

AsReplicated returns the replication wrapper d is, or carries (Shaped), and false for a bare driver.

func NewReplicated

func NewReplicated(primary, replica Driver, rules RuleEngine, failures FailureRecorder, notifier EventNotifier) *ReplicatedDriver

NewReplicated wires a wrapper. replica may be nil — the wrapper then degenerates to a pure passthrough on writes (no fan-out).

rules MUST be non-nil; pass DefaultRules() if you don't have a configured engine yet (it returns mirror for everything).

func (*ReplicatedDriver) Capabilities

func (r *ReplicatedDriver) Capabilities() Capabilities

Capabilities mirrors the primary's capabilities — replica fan-out is invisible to clients above the wrapper.

func (*ReplicatedDriver) Close added in v0.53.0

func (r *ReplicatedDriver) Close() error

Close retires the wrapper at once and, in the background, waits for the fan-outs in flight before closing the primary and the replica (connections, a plugin's instance). It is what the resolver's cache calls when it drops the wrapper: the caller - an admin saving a storage - does not wait for a large file still on its way to the backup. Done says when it is over.

func (*ReplicatedDriver) Copy

func (r *ReplicatedDriver) Copy(ctx context.Context, src, dst string) error

Copy clones on primary, then async clones on replica.

func (*ReplicatedDriver) CopyNow added in v0.53.0

func (r *ReplicatedDriver) CopyNow(ctx context.Context, path string) (code string, err error)

CopyNow copies one file to the replica synchronously - the initial copy's step. On success any failure recorded for (path, write) is resolved; on failure the code and error are returned and nothing is recorded (the caller decides, without a notification per file).

func (*ReplicatedDriver) Delete

func (r *ReplicatedDriver) Delete(ctx context.Context, path string) error

Delete removes from primary. On success, an async goroutine removes from replica unless the path's rule is append_only or skip.

func (*ReplicatedDriver) Done added in v0.53.0

func (r *ReplicatedDriver) Done() <-chan struct{}

Done is closed once Close has drained the fan-outs and released the drivers.

func (*ReplicatedDriver) Folder added in v0.53.0

func (r *ReplicatedDriver) Folder() string

Folder is the storage's folder on the replica ("" = its root).

func (*ReplicatedDriver) Halted added in v0.53.0

func (r *ReplicatedDriver) Halted() bool

Halted reports whether the wrapper was retired (Stop or Close).

func (*ReplicatedDriver) HasReplica

func (r *ReplicatedDriver) HasReplica() bool

HasReplica reports whether replication is configured.

func (*ReplicatedDriver) Init

func (r *ReplicatedDriver) Init(_ context.Context, _ map[string]any) error

Init is a no-op — primary and replica are already initialized by the caller (server bootstrap).

func (*ReplicatedDriver) List

func (r *ReplicatedDriver) List(ctx context.Context, path string) ([]Object, error)

List always reads from primary (source of truth). On primary error we DO NOT fall back to the replica because a replica list could disagree with primary's state (e.g. mid-replication holes).

func (*ReplicatedDriver) Mkdir

func (r *ReplicatedDriver) Mkdir(ctx context.Context, path string) error

Mkdir creates the directory on primary, then on replica.

func (*ReplicatedDriver) Mode added in v0.53.0

func (r *ReplicatedDriver) Mode(p string) ReplicaMode

Mode is the replica mode the rules give path. A leading "/" is not part of the match: filex writes paths both ways ("/docs/a.txt" from the catalogue, "docs/a.txt" from an upload key) and a rule `docs/**` means both. filex's own folders are ModeSkip before any rule (InternalPath).

func (*ReplicatedDriver) Move

func (r *ReplicatedDriver) Move(ctx context.Context, src, dst string) error

Move renames on primary, then re-issues the same move on replica asynchronously. Falls back to delete+write when the replica's driver doesn't support Mover.

func (*ReplicatedDriver) Name

func (r *ReplicatedDriver) Name() string

Name returns the underlying primary's name (visible to operators).

func (*ReplicatedDriver) Primary

func (r *ReplicatedDriver) Primary() Driver

Primary returns the underlying primary Driver — used by reconcile + report jobs that need direct access (e.g. counting objects).

func (*ReplicatedDriver) ProbeReplica added in v0.53.0

func (r *ReplicatedDriver) ProbeReplica(ctx context.Context) error

ProbeReplica asks the replica one cheap question. nil means it answered - "not found" is an answer; an error means it could not be reached.

func (*ReplicatedDriver) Read

func (r *ReplicatedDriver) Read(ctx context.Context, path string) (io.ReadCloser, error)

Read tries primary, falls back to replica on error and emits a primary_read_fail notification.

func (*ReplicatedDriver) ReadRange added in v0.53.0

func (r *ReplicatedDriver) ReadRange(ctx context.Context, path string, off, length int64) (io.ReadCloser, error)

ReadRange serves a byte window from the primary (falling back to the replica like Read). A driver without a RangeReader of its own is read from the start and the bytes before off are skipped: slower, never wrong.

func (*ReplicatedDriver) RecordFailure added in v0.53.0

func (r *ReplicatedDriver) RecordFailure(ctx context.Context, path, op, code string, err error)

RecordFailure writes a failure row for (path, op) without a notification - for a caller that reports in bulk (the initial copy).

func (*ReplicatedDriver) Repair added in v0.53.0

func (r *ReplicatedDriver) Repair(ctx context.Context, path, op string) error

Repair re-runs one failed fan-out against the replica: a write, move or copy re-copies the path from the primary, a delete removes it from the replica. On success the failure row for (path, op) is resolved. A path the primary no longer has leaves nothing to copy: that is resolved too (the delete that removed it fans out on its own). On failure the row is updated with the new error, without a notification.

⚠ A repaired move writes the new name; the old name stays on the backup (the failure row keeps only the destination).

func (*ReplicatedDriver) Replica

func (r *ReplicatedDriver) Replica() Driver

Replica returns the underlying replica or nil.

func (*ReplicatedDriver) ReplicaHolds added in v0.53.0

func (r *ReplicatedDriver) ReplicaHolds(ctx context.Context, o Object) bool

ReplicaHolds reports whether the replica already has the file o describes: the same size and a modification time that is the same (within two seconds: SMB and FTP keep coarse times) - or, on a replica that cannot keep a modification time (an object store), not older than the primary's. Anything it cannot tell reads as "not there", so the file is copied.

func (*ReplicatedDriver) Shaped added in v0.53.0

func (r *ReplicatedDriver) Shaped() Driver

Shaped returns the wrapper as the Driver the rest of filex is handed: one that has the primary's Toucher, Presigner and PartUploader exactly when the primary has them.

⚠ filex decides what a storage can do by TYPE-ASSERTING optional interfaces at some forty call sites (the reasoning of internal/plugin/gen). The bare *ReplicatedDriver has neither SetMtime nor the multipart calls, so wrapping a disk would silently drop the modification time an SFTP, FTP, NFS or S3 client sends, and wrapping a bucket would turn every large upload into a single PUT; a wrapper that had them ALWAYS would make a storage without them claim abilities it lacks and fail at the last moment. Hence one type per combination (eight), picked here.

What each carried interface does on the wrapper:

  • Toucher: sets the time on the primary, then carries it to the replica (best effort).
  • Presigner: a download link is the primary's. An UPLOAD link is refused (ErrUnsupported): the browser would write to the primary directly and the fan-out would never see the file. No server path asks for one today.
  • PartUploader: the parts go to the primary; CompleteMultipart fans the assembled object out like a Write.

func (*ReplicatedDriver) Stat

func (r *ReplicatedDriver) Stat(ctx context.Context, path string) (Object, error)

Stat reads from primary, with replica fallback on error so the admin UI doesn't 404 if the primary is briefly down.

func (*ReplicatedDriver) Stop

func (r *ReplicatedDriver) Stop()

Stop retires the wrapper and waits for the fan-outs in flight. A change that reaches it afterwards is not fanned out: it is recorded as a failure (REPLICA_DRIVER_REPLACED) so Fix all can replay it.

func (*ReplicatedDriver) WithFolder added in v0.53.0

func (r *ReplicatedDriver) WithFolder(folder string) *ReplicatedDriver

WithFolder puts everything the wrapper writes to, reads from and compares on the replica inside folder (a storage's own folder on a target several storages share, internal/replica folder.go). Paths in failure rows stay the storage's own; the folder is applied on the way to the replica. Call it before the wrapper is used.

func (*ReplicatedDriver) Write

func (r *ReplicatedDriver) Write(ctx context.Context, path string, body io.Reader, size int64) error

Write writes to primary synchronously. On success, an async goroutine reads back from primary and writes to replica.

We deliberately do not buffer the request body to allow a single fan-out write — replica writes happen via a primary read-back so the streaming primary write doesn't have to fork.

type RuleEngine

type RuleEngine interface {
	Match(path string) ReplicaMode
}

RuleEngine returns a ReplicaMode for the given path. Concrete impl is in rules.go.

func DefaultRules

func DefaultRules() RuleEngine

DefaultRules returns a no-op engine (everything mirrors). Useful when callers haven't wired a real engine yet.

func NewRulesEngine

func NewRulesEngine(reloadFn func() ([]RuleSpec, ReplicaMode)) RuleEngine

NewRulesEngine returns an engine that calls reloadFn lazily. If reloadFn is nil the engine returns ModeMirror for every path.

type RuleSpec

type RuleSpec struct {
	ID       int64
	Pattern  string
	Mode     ReplicaMode
	Priority int
	Enabled  bool
}

RuleSpec is the declarative shape of one rule. It mirrors model.ReplicaRule but lives here so the storage package doesn't import model.

type SecretAddressError added in v0.53.0

type SecretAddressError struct{ Field string }

SecretAddressError names the credential that has to be typed again.

func (*SecretAddressError) Error added in v0.53.0

func (e *SecretAddressError) Error() string

func (*SecretAddressError) Is added in v0.53.0

func (e *SecretAddressError) Is(target error) bool

Is makes errors.Is(err, ErrSecretAddressChanged) true.

type SelectOption

type SelectOption struct {
	Value string `json:"value"`
	// Label is the English fallback shown when I18nKey is missing from
	// the surface's catalogue.
	Label   string `json:"label"`
	I18nKey string `json:"i18n_key,omitempty"`
}

SelectOption is one choice of a FieldSelect field.

type Tally added in v0.47.0

type Tally struct {
	// contains filtered or unexported fields
}

Tally counts the objects a driver call works through.

An object store's folder copy, move or delete is ONE driver call that lists the folder and then works through its objects one at a time — for a large folder, minutes — and nothing outside the call could see how far it had got: the operations centre read "0%" until the whole folder was done. A driver that works object by object says so here: Found when it knows how many it has before it, Done after each one.

It travels on the context (WithTally), so no driver interface changes and neither does any caller between the ops worker and the driver (the trash alone has nine). A driver that does not report leaves it untouched.

Nil-safe, and safe for concurrent use: the ops worker runs several deletes of one job at once, all into one Tally, which is also why it counts in deltas rather than setting totals.

func TallyOf added in v0.47.0

func TallyOf(ctx context.Context) *Tally

TallyOf returns the context's tally, or nil — on which every method is a no-op, so a driver calls it unconditionally.

func TallyUnder added in v0.47.0

func TallyUnder(parent *Tally) *Tally

TallyUnder returns a tally of its own whose counts also reach parent as they happen: a caller that must know what one call counted (to take back what it left undone, Forget) without hiding the progress from the tally above.

func (*Tally) Done added in v0.47.0

func (t *Tally) Done(n int)

Done adds n objects the call has finished with.

func (*Tally) Forget added in v0.47.0

func (t *Tally) Forget(n int)

Forget takes back n objects Found counted that the call will not work through after all: it stopped part-way, and whoever carries on counts what is left.

func (*Tally) Found added in v0.47.0

func (t *Tally) Found(n int)

Found adds n objects the call now knows it has to work through.

func (*Tally) Load added in v0.47.0

func (t *Tally) Load() (done, total int64)

Load returns how many objects are done out of how many were found.

type Toucher

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

Toucher sets an object's modification time.

This is what carries a file's own timestamp through a transfer instead of stamping it with the moment it arrived. rclone, s3fs and the desktop sync all send the source mtime alongside the bytes (`x-amz-meta-mtime` over S3); a server that drops it reports every synced file as modified-now, and the next run copies the lot again.

⚠ Optional on purpose. An object store has no settable modification time, and a driver that accepted one and quietly ignored it would be worse than one that does not implement this at all: callers can see the difference between "not supported" and "supported and applied", and cannot see the difference between "applied" and "pretended".

type TreeWalker

type TreeWalker interface {
	WalkTree(ctx context.Context, path string, fn func(Object) error) error
}

TreeWalker lets a backend hand over a whole subtree in ONE pass instead of being asked for every directory separately.

List is one round trip per directory. A poll over an object store with 150,000 objects in a few thousand prefixes therefore made a few thousand ListObjectsV2 calls — each a network round trip — and a scan took longer than its own interval (issue #33). An object store has no directories to descend into: a single un-delimited listing returns every key under a prefix, 1,000 per page, so the same tree costs ~150 calls.

Contract:

  • fn is called once per object below `path`, files AND the directories the driver derives from their paths (every ancestor exactly once, the driver's own markers such as an empty-folder key excluded). Order is the backend's; the caller groups by parent itself.
  • Object.Path is the full logical path (as List would report it); a directory's Kind is KindDirectory.
  • Returning a non-nil error from fn stops the walk and is returned as-is, so a caller can bound how much it is willing to hold.
  • A path that does not exist yields no calls and a nil error (an empty prefix is a listing, not a miss).

Drivers with real directories (local, sftp, …) do not implement this: a recursive readdir is not cheaper than the per-directory walk, and the walk keeps its readdir order guarantees.

type Watcher

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

Watcher streams change events from the backend.

type Writer

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

Writer adds upload support.

Directories

Path Synopsis
drivers
ftp
Package ftp is a Storage Driver fronting a plain FTP (or FTPS) server.
Package ftp is a Storage Driver fronting a plain FTP (or FTPS) server.
local
Package local is a Storage Driver fronting an on-disk directory.
Package local is a Storage Driver fronting an on-disk directory.
s3
Package s3 is a Storage Driver fronting any S3-compatible object store.
Package s3 is a Storage Driver fronting any S3-compatible object store.
sftp
Package sftp is a Storage Driver fronting an SSH/SFTP server.
Package sftp is a Storage Driver fronting an SSH/SFTP server.
smb
Package smb is a Storage Driver fronting an SMB2/3 share — a NAS, a Windows file server, a Samba box.
Package smb is a Storage Driver fronting an SMB2/3 share — a NAS, a Windows file server, a Samba box.
webdav
Package webdav is a Storage Driver fronting a WebDAV server.
Package webdav is a Storage Driver fronting a WebDAV server.
Package stall bounds how long a storage driver waits for a backend that does not answer, without ever cutting a transfer that keeps moving.
Package stall bounds how long a storage driver waits for a backend that does not answer, without ever cutting a transfer that keeps moving.

Jump to

Keyboard shortcuts

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