Documentation
¶
Overview ¶
Package rclonestore implements storage.Blobs by driving the external rclone binary as a context-bounded subprocess (argv exec — never a shell string, never librclone/cgo).
Index ¶
- Variables
- type BinaryError
- type LayoutScanError
- type LegacyLayoutError
- type Options
- type OptionsError
- type PersistencePathError
- type ProbeError
- type PutSourceError
- type RcloneError
- type Store
- func (s *Store) Close() error
- func (b Store) Delete(ctx context.Context, key string) error
- func (b Store) Get(ctx context.Context, key string) (io.ReadCloser, error)
- func (b Store) List(ctx context.Context, prefix string) ([]string, error)
- func (b Store) Put(ctx context.Context, key string, r io.Reader) error
- func (b Store) StoragePaths() []string
Constants ¶
This section is empty.
Variables ¶
var ErrLegacyLayout = errors.New("rclonestore: store root holds pre-v0.5.0 data")
ErrLegacyLayout is the sentinel every *LegacyLayoutError matches with errors.Is: the store root holds objects written by rclonestore <= v0.4.x, which stored each key at its bare path. v0.5.0 stores a key at "<key>@blob" so that a key and its "/…" extension can coexist (storage v0.7.0); it does not migrate the old layout and does not read it.
Functions ¶
This section is empty.
Types ¶
type BinaryError ¶
type BinaryError struct {
Binary string
// contains filtered or unexported fields
}
BinaryError reports that the rclone binary could not be resolved on PATH. The binary name/path is operator-supplied configuration (no credential), so it is safe to name; the underlying exec.LookPath error is wrapped for errors.Is/As.
func (*BinaryError) Error ¶
func (e *BinaryError) Error() string
func (*BinaryError) Unwrap ¶
func (e *BinaryError) Unwrap() error
type LayoutScanError ¶ added in v0.5.0
type LayoutScanError struct {
// contains filtered or unexported fields
}
LayoutScanError reports that New's legacy-layout scan — one recursive listing of the store root, run after the reachability probe succeeded — failed. The root is neither known clean nor known legacy, so no Store is returned. It wraps the credential-safe *RcloneError.
func (*LayoutScanError) Error ¶ added in v0.5.0
func (e *LayoutScanError) Error() string
func (*LayoutScanError) Unwrap ¶ added in v0.5.0
func (e *LayoutScanError) Unwrap() error
type LegacyLayoutError ¶ added in v0.5.0
type LegacyLayoutError struct {
Path string
}
LegacyLayoutError reports that New (or a later List) found an object at a bare key path under the store root — the layout rclonestore <= v0.4.x wrote. It is refused rather than ignored because that data would otherwise be silently invisible: Get would report it absent and List would omit it while new writes landed beside it. There is no migration; move or delete the store root (or, if it was written by something other than rclonestore, keep that data out of the root — the root must be dedicated to rclonestore).
Path is the lexically first such object, relative to the store root. It is a valid storage name (that is what makes it legacy-shaped) and carries neither the remote nor the prefix, so the error is safe to log.
func (*LegacyLayoutError) Error ¶ added in v0.5.0
func (e *LegacyLayoutError) Error() string
func (*LegacyLayoutError) Unwrap ¶ added in v0.5.0
func (e *LegacyLayoutError) Unwrap() error
Unwrap returns ErrLegacyLayout.
type Options ¶
type Options struct {
// Remote is required: a named rclone remote (e.g. "myremote") OR a ":backend:"
// connection string (e.g. ":local:/data", ":s3,provider=Minio:/bucket").
Remote string
// Prefix is an optional path fragment under the remote. If set it must be a
// safe relative path: no leading/trailing '/', no empty, "." or ".." segment.
Prefix string
// Binary is the rclone executable; default "rclone", resolved via exec.LookPath.
Binary string
// ConfigPath is an optional --config path. It is referenced by path only —
// never opened, parsed, copied, or logged here (it may hold remote secrets).
ConfigPath string
// Timeout optionally bounds every rclone invocation (including the startup
// probe). Zero means each call is bounded only by the caller's context.
Timeout time.Duration
// DisableListR passes --disable ListR to every rclone invocation. Use it for
// providers whose recursive listing is broken.
DisableListR bool
// PersistencePaths declares local filesystem roots used by the backend. Inline
// :local: remotes are discovered automatically, so this is primarily for named
// remotes, which are never introspected. Entries are already-effective roots;
// Prefix is not appended to them.
PersistencePaths []string
}
Options configures a rclonestore Store. Only Remote is required. No field is ever logged or placed in an error verbatim: a connection-string Remote or a ConfigPath can embed remote credentials.
type OptionsError ¶
OptionsError reports an invalid field in Options. It names the Field and the Rule violated but never the offending value: Remote (a connection string) or a ConfigPath can embed credentials, so no option value is ever surfaced.
func (*OptionsError) Error ¶
func (e *OptionsError) Error() string
type PersistencePathError ¶
type PersistencePathError struct {
Path string
Rule string
// contains filtered or unexported fields
}
PersistencePathError reports an invalid local filesystem root declared by the caller or derived from an inline local remote. Path contains only the local filesystem portion, never a named remote or inline backend parameters.
func (*PersistencePathError) Error ¶
func (e *PersistencePathError) Error() string
func (*PersistencePathError) Unwrap ¶
func (e *PersistencePathError) Unwrap() error
type ProbeError ¶
type ProbeError struct {
// contains filtered or unexported fields
}
ProbeError reports that the startup reachability probe failed: rclone could not list the remote root and the failure was not a benign not-found. It wraps the underlying *RcloneError, which is credential-safe by construction (it excludes the config path, the remote, and every positional). The remote is deliberately NOT carried on ProbeError — a connection-string remote can embed secrets.
func (*ProbeError) Error ¶
func (e *ProbeError) Error() string
func (*ProbeError) Unwrap ¶
func (e *ProbeError) Unwrap() error
type PutSourceError ¶
type PutSourceError struct {
Key string
// contains filtered or unexported fields
}
PutSourceError reports that reading the caller-supplied Put reader failed before the blob could be compared against an existing object or streamed to rclone. It carries the storage key (safe to log — it is a validated canonical name) and wraps the underlying read error.
func (*PutSourceError) Error ¶
func (e *PutSourceError) Error() string
func (*PutSourceError) Unwrap ¶
func (e *PutSourceError) Unwrap() error
type RcloneError ¶
type RcloneError struct {
Subcommand string
Args []string
ExitCode int
Stderr string
// contains filtered or unexported fields
}
RcloneError reports a failed rclone invocation: a non-zero exit, a start failure, or a subprocess killed by its context deadline/cancellation. Callers classify with errors.As.
It deliberately carries only information that cannot embed a credential:
- Subcommand — the rclone subcommand (e.g. "rcat", "cat", "lsf", "deletefile").
- Args — the subcommand's own flags (the subflags). It NEVER contains the positional path arguments (which embed the remote name and the storage key) nor the global --config path (which points at a file that may hold remote credentials). The caller passes only non-secret subflags.
- ExitCode — the process exit status; -1 for a start failure or a signal kill.
- Stderr — a bounded tail (~4 KiB) of the process's stderr, REDACTED before it is kept: rclone echoes the remote it was given, inline ":backend,key=value:" credentials included, so the inline spec is rewritten to ":backend,<redacted>:" and every parameter value and the --config path to "<redacted>" in their verbatim, unquoted and Go-escaped spellings (a secret cut by the tail bound leaves no fragment). Transformed spellings (URL-encoded and the like) are not matched. A named remote's credentials live in the config file, which rclone does not echo. Stderr is diagnostic only: it is never used to classify the failure.
The config path, inline remote parameters, and every positional are excluded by construction, so an RcloneError is safe to log. Credentials supplied any other way rclone might print (e.g. RCLONE_* environment variables) are outside what rclonestore can see and are not redacted.
func (*RcloneError) Error ¶
func (e *RcloneError) Error() string
Error renders the subcommand, the exit status, and a sanitized (quoted) stderr tail. The tail is strconv.Quote'd so newlines or control bytes emitted by the subprocess cannot inject into a log line. No credential-bearing value is ever included.
func (*RcloneError) Unwrap ¶
func (e *RcloneError) Unwrap() error
Unwrap returns the underlying cause so callers can classify with errors.Is / errors.As: the *exec.ExitError (or start error) on a genuine failure, or the context error (context.DeadlineExceeded / context.Canceled) when the call was killed by its deadline or cancellation.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a storage.Blobs backed by the external rclone binary. It embeds the unexported blobStore, so a Store IS-A storage.Blobs (Put/Get/Delete/List are promoted). Construct one with New.
func New ¶
New validates opts, resolves the rclone binary, and probes that the remote is reachable before returning a Store — failing loudly (fail-secure) at construction rather than at first use:
- Remote is validated as a named remote or a connection string (*OptionsError).
- Prefix, if set, is validated as a safe path fragment (*OptionsError).
- Binary (default "rclone") is resolved with exec.LookPath (*BinaryError).
- The remote root is probed with "rclone lsf --max-depth 0 -- <root>", bounded by opts.Timeout. A not-found root is treated as reachable-but-empty (a fresh store creates it on first Put); any other failure is a *ProbeError.
- The root is scanned for objects written by rclonestore <= v0.4.x, which stored each key at its bare path (v0.5.0 stores "<key>@blob"): one recursive "rclone lsf --files-only -R" of the root, bounded by opts.Timeout — the same cost as one List. A legacy object is refused with *LegacyLayoutError (errors.Is ErrLegacyLayout); there is no migration. A scan failure is a *LayoutScanError.
func (*Store) Close ¶
Close releases resources held by the Store. rclone is driven per-call as a short-lived subprocess and holds no persistent connection, so Close is a documented no-op that always returns nil; it exists only so callers can treat a Store uniformly with stores that do hold resources.
func (Store) Delete ¶
Delete removes the object at key via "rclone deletefile". Deleting an absent object is a success (idempotent): a not-found from rclone is classified and mapped to nil; every other failure propagates.
func (Store) Get ¶
Get streams the object at key back to the caller. It runs "rclone cat" to completion into an in-memory buffer and returns an independent io.ReadCloser over those bytes. Buffering (rather than piping rclone's stdout through) is what lets Get satisfy the contract's synchronous not-found: storetest expects Get itself — not a later Read — to return *storage.BlobNotFoundError, which is only knowable once rclone has exited. A missing object is classified from rclone's exit code/stderr and mapped to *storage.BlobNotFoundError. Empty output is confirmed with the exact-leaf existence probe before it is returned as an empty blob, because a bucket-based remote cats an absent key as nothing with exit 0.
func (Store) List ¶
List returns the storage keys of every object under the store's prefix whose key begins with the caller's prefix, lexicographically ascending and duplicate-free. It runs "rclone lsf --files-only -R" rooted at "<remote>:<prefix>": the emitted paths are relative to that root, and each object path is decoded back to its key (decodeLeaf); entries that are not "@blob" leaves are skipped. The caller's prefix is applied to the DECODED key (it need not fall on a directory boundary) and is NOT name-validated. The result is sorted locally — rclone's ordering is not trusted, and the encoded order would put "a/b" before "a". An empty store surfaces as a not-found on the root directory, which maps to an empty listing rather than an error.
List fails closed with *LegacyLayoutError if the listing holds a bare-path object (isLegacyLeaf) anywhere under the store root, whatever the prefix: New refused such a root, so one appearing later was written by an rclonestore <= v0.4.x (a rollback) and would otherwise be silently invisible. The check costs nothing — the recursive listing is already in hand.
func (Store) Put ¶
Put honors storage's content-addressed conflict contract. It first probes for an existing object at the key's encoded path ("<key>@blob"; see exists — a directory there is never taken for the blob):
- ABSENT (the common content-addressed case): stream r straight to "rclone rcat" with no buffering — the blob never lands in memory.
- PRESENT: read r fully, "rclone cat" the existing object, and compare bytes. Byte-identical → success/no-op (the object is NOT re-uploaded). Different → *storage.BlobConflictError with the original left untouched (no upload).
The present branch buffers both the incoming reader (io.ReadAll) and the existing object (into memory) to compare them. This is deliberately the rare path: keys are content-addressed, so a re-Put of DIFFERENT bytes under the same key is pathological, and a re-Put of IDENTICAL bytes is a cheap idempotent no-op. The probe→write sequence is a TOCTOU only under concurrent writers to the same key; the workspace store holds a single-writer lease over the key space, so no concurrent writer exists by construction.
func (Store) StoragePaths ¶
func (b Store) StoragePaths() []string
StoragePaths returns the canonical local filesystem roots used by this blob provider. Remote backends return nil. The result is a defensive copy so callers cannot mutate the provider's construction-time view.