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
- Variables
- func CloseDriver(d Driver)
- func ConfigInt(v any) (int, bool)
- func ConfigLookup(cfg map[string]any, key string, aliases ...string) (any, bool)
- func ConfigString(cfg map[string]any, key string, aliases ...string) string
- func DetachMutation(ctx context.Context) (context.Context, context.CancelFunc)
- func EmptyReadCloser() io.ReadCloser
- func EnsureDirTarget(ctx context.Context, d Driver, p string) error
- func EnsureFileTarget(ctx context.Context, d Driver, p string) error
- func Exists(ctx context.Context, d Driver, p string) bool
- func FenceAt(ctx context.Context, d Driver, paths ...string) []string
- func FenceFor(d Driver, dir bool, paths ...string) []string
- func InternalPath(p string) bool
- func IsSecretField(driver, key string) bool
- func KeepSecrets(driver string, next json.RawMessage, prevDriver string, prev json.RawMessage) (json.RawMessage, error)
- func Landed(ctx context.Context, d Driver, p string, size int64) (int64, string, time.Time)
- func LimitReadCloser(rc io.ReadCloser, n int64) io.ReadCloser
- func MaskSecrets(driver string, raw json.RawMessage) json.RawMessage
- func Names() []string
- func ObjectByObject(d Driver) bool
- func ReadAhead(fr io.ReadCloser, buffer int, length int64) io.ReadCloser
- func RefineOfficeMime(detected, filename string) string
- func Register(name string, f Factory)
- func RegisterDescriptor(d Descriptor)
- func SeekOpened(f interface{ ... }, off int64) error
- func StillAsSeen(ctx context.Context, drv Driver, rel string, seen Object) error
- func StillFree(ctx context.Context, drv Driver, dst, src string) error
- func Unregister(name string)
- func UnregisterDescriptor(name string)
- func ValidateLazyConfig(cfg map[string]any) error
- func ValidateNonRootPath(driver string, cfg map[string]any) error
- func WithTally(ctx context.Context, t *Tally) context.Context
- type Capabilities
- type Collision
- type Copier
- type CycleGuard
- type Deleter
- type Descriptor
- type Driver
- type Event
- type EventNotifier
- type Factory
- type FailureRecorder
- type Field
- type FieldType
- type Mkdirer
- type Mover
- type MultipartUploader
- type Object
- type ObjectKind
- type PartCompletion
- type PartUploader
- type PresignedUpload
- type Presigner
- type RangeReader
- type Reloader
- type ReplicaMode
- type ReplicatedDriver
- func (r *ReplicatedDriver) Capabilities() Capabilities
- func (r *ReplicatedDriver) Close() error
- func (r *ReplicatedDriver) Copy(ctx context.Context, src, dst string) error
- func (r *ReplicatedDriver) CopyNow(ctx context.Context, path string) (code string, err error)
- func (r *ReplicatedDriver) Delete(ctx context.Context, path string) error
- func (r *ReplicatedDriver) Done() <-chan struct{}
- func (r *ReplicatedDriver) Folder() string
- func (r *ReplicatedDriver) Halted() bool
- func (r *ReplicatedDriver) HasReplica() bool
- func (r *ReplicatedDriver) Init(_ context.Context, _ map[string]any) error
- func (r *ReplicatedDriver) List(ctx context.Context, path string) ([]Object, error)
- func (r *ReplicatedDriver) Mkdir(ctx context.Context, path string) error
- func (r *ReplicatedDriver) Mode(p string) ReplicaMode
- func (r *ReplicatedDriver) Move(ctx context.Context, src, dst string) error
- func (r *ReplicatedDriver) Name() string
- func (r *ReplicatedDriver) Primary() Driver
- func (r *ReplicatedDriver) ProbeReplica(ctx context.Context) error
- func (r *ReplicatedDriver) Read(ctx context.Context, path string) (io.ReadCloser, error)
- func (r *ReplicatedDriver) ReadRange(ctx context.Context, path string, off, length int64) (io.ReadCloser, error)
- func (r *ReplicatedDriver) RecordFailure(ctx context.Context, path, op, code string, err error)
- func (r *ReplicatedDriver) Repair(ctx context.Context, path, op string) error
- func (r *ReplicatedDriver) Replica() Driver
- func (r *ReplicatedDriver) ReplicaHolds(ctx context.Context, o Object) bool
- func (r *ReplicatedDriver) Shaped() Driver
- func (r *ReplicatedDriver) Stat(ctx context.Context, path string) (Object, error)
- func (r *ReplicatedDriver) Stop()
- func (r *ReplicatedDriver) WithFolder(folder string) *ReplicatedDriver
- func (r *ReplicatedDriver) Write(ctx context.Context, path string, body io.Reader, size int64) error
- type RuleEngine
- type RuleSpec
- type SecretAddressError
- type SelectOption
- type Tally
- type Toucher
- type TreeWalker
- type Watcher
- type Writer
Constants ¶
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.
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.
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.
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.
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.
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.
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 ¶
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.
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.
var ErrNotFound = errors.New("storage: not found")
ErrNotFound is returned by drivers when a path does not exist.
var ErrReadOnly = errors.New("storage: read-only")
ErrReadOnly is returned when a write op hits a read-only mount.
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.
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.
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).
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.
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 ¶
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 ¶
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 ¶
ConfigString is ConfigLookup narrowed to strings.
func DetachMutation ¶ added in v0.47.0
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 ¶
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 ¶
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 ¶
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
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
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
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
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 ¶
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 ObjectByObject ¶ added in v0.55.0
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 ¶
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 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
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
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
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
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 ¶
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.
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 ¶
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 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.
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 UnavailableDriver ¶ added in v0.53.0
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 ¶
Mkdirer creates directories — drivers without a directory concept may no-op or persist a placeholder.
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 ¶
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
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 ¶
Init is a no-op — primary and replica are already initialized by the caller (server bootstrap).
func (*ReplicatedDriver) List ¶
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 ¶
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
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
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) Forget ¶ added in v0.47.0
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.
type Toucher ¶
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.
Source Files
¶
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. |