Help us shape the future of Go! Take the 2026 Go Developer Survey

remote

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 30 Imported by: 0

Documentation

Overview

Package remote abstracts the cloud object store a volume syncs through. beardrive is provider-agnostic: any backend that can put/get/list immutable objects works. Built-in schemes:

file:///abs/path      local or network-drive directory (also used in tests)
s3://bucket/prefix    Amazon S3 (or S3-compatible via AWS_ENDPOINT_URL)
gs://bucket/prefix    Google Cloud Storage
https://host:4173     a bdrive web server brokering one of the above —
                      the device needs no storage credentials at all

Remote layout: blobs/<sha256> for content, journal/<device>.jsonl for op logs. Each device writes only its own journal, so there are no concurrent writers per object and no server-side coordination is needed.

Index

Constants

View Source
const (
	ReadKindAgent = "agent"
	ReadKindHuman = "human"
)

ReadReporter is the optional read-telemetry capability, in the PutSigner mold: backends that sync through a hub report the device's agent reads so the heat view can split human from agent traffic. Object-store backends simply don't implement it — there is no hub to tell. The read kinds a client may report. They mirror internal/webapp's own constants; duplicated rather than imported because remote must not depend on the server package it talks to.

View Source
const PermsCapability = "X-Bdrive-Perms"

do sends the request with this device's credential attached, plus the identity headers the server's device registry records for history (name, OS; the server observes the IP itself).

It deliberately does NOT set Accept-Encoding. net/http adds `gzip` itself whenever the caller has not, and transparently inflates the response — which is the entire pull half of transport compression, free and backward compatible. Setting the header here turns that off silently: the hub would still answer `Content-Encoding: gzip`, nothing would inflate it, and every blob would fail its sha check while looking like a corrupt hub. PermsCapability is what a client sends to say it understands a hub that serves per-account filtered journals: it tracks the scope tag from the store listing and re-pulls from zero when it changes. A hub with folder rules refuses a client that does not send it, because such a client resumes from a byte offset into a stream whose shape it cannot know has moved.

Variables

View Source
var ErrForbidden = errors.New("forbidden")

ErrForbidden marks a refusal by the hub's authorization — the device asked correctly and was told no, which is a different thing from being offline. The syncer keys its degraded states off it: a refused push means read-only (keep pulling), a refused pull means access is gone (pause, touch nothing).

View Source
var ErrNoScope = errors.New("this server does not report project scope")

ErrNoScope is Scope's answer from a hub too old to have folder permissions. Distinct from a transport error on purpose: "this hub has no opinion" means sync everything, "the hub is unreachable" means keep the last answer.

View Source
var ErrVersionMismatch = errors.New("object changed since it was read")

ErrVersionMismatch is PutIf's answer when the object moved since the caller read it. It is not a failure so much as an instruction: re-read, redo the work against what is there now, and try again.

Functions

func AcceptsGzip added in v0.16.0

func AcceptsGzip(r *http.Request) bool

AcceptsGzip reports whether a request's Accept-Encoding allows a gzipped answer. Go's own transport sets that header on every request this package makes, which is why old devices get the compressed pull leg for free.

func Compressible added in v0.16.0

func Compressible(r io.Reader) (io.Reader, bool, error)

Compressible reports whether a stream is worth gzipping, by compressing its first probeWindow bytes and checking that the sample actually shrank.

It returns the stream REJOINED — the sampled bytes followed by whatever is left — because the probe has to consume the bytes it judges. That identity is the property this helper lives or dies on: a probe that eats bytes silently corrupts every push and every pull that runs through it, and the failure shows up as a sha mismatch far from here. compress_test.go asserts it for a stream longer than the window, one shorter, and an empty one.

func IsNotExist added in v0.16.1

func IsNotExist(err error) bool

IsNotExist reports whether err means "no such object", across backends that each spell it differently (GCS has its own sentinel, a file backend uses os.ErrNotExist). One place, so a caller does not have to know which backend it is holding to tell "absent" from "broken" — and getting that wrong means treating a transient failure as an empty journal.

func SameOrigin added in v0.15.0

func SameOrigin(a, b string) bool

SameOrigin is the exported form, for the CLI. It is the one rule that decides who may receive this device's bearer token, and it is needed at two doors: the sync backend's (here) and `bdrive share`/`init`'s HTTP client (cmd/bdrive). It used to be spelled twice — which is exactly how round 7's journal-path finding happened — so there is one copy and cmd/bdrive calls it.

Types

type Backend

type Backend interface {
	Put(ctx context.Context, key string, r io.Reader, size int64) error
	Get(ctx context.Context, key string) (io.ReadCloser, error)
	List(ctx context.Context, prefix string) ([]Object, error)
	Exists(ctx context.Context, key string) (bool, error)
	Close() error
}

func Open

func Open(ctx context.Context, raw string) (Backend, error)

Open creates a backend from a remote URL.

func Prefixed

func Prefixed(be Backend, prefix string) Backend

Prefixed namespaces a backend under prefix: every key is stored at <prefix>/<key>. Used by the web server to host many projects in one storage root. Closing the returned backend does not close the underlying one (it is shared across prefixes).

The optional capabilities pass through too. A capability the namespace dropped is not an error anywhere: callers type-assert for it and take the slower path when it is missing. That is how every hub project lost compare-and-swap — the journal append looked for ConditionalPutter on this wrapper, never found it, and appended last-writer-wins on GCS as well.

type ConditionalPutter added in v0.16.1

type ConditionalPutter interface {
	// GetVersioned reads an object and reports the version it was at, so a
	// later PutIf can say "only if this has not moved".
	GetVersioned(ctx context.Context, key string) (io.ReadCloser, Version, error)
	// PutIf writes only if the object is still at expect, and reports the
	// version it wrote. ErrVersionMismatch means somebody else got there.
	PutIf(ctx context.Context, key string, r io.Reader, size int64, expect Version) (Version, error)
}

ConditionalPutter is the optional compare-and-swap capability, in the

PutSigner and Watcher mold: a backend that cannot do it simply does not
implement it, and callers keep exactly the behaviour they had.

It exists because a journal append is a READ-MODIFY-WRITE of one object,
and Put is last-writer-wins. Inside one process a mutex is enough and that
is what the hub has always used. Across two it is not: both read the same
journal, both append their own ops, and whichever writes second silently
erases the other's — no error anywhere, because both writes succeeded. That
is the lost update the "each device writes only its own journal" invariant
has always prevented BETWEEN devices, reappearing inside one device id the
moment the hub runs more than once (docs/hub-load-prd.md Stage 10).

Implemented for GCS, which is what the managed hub stores in. Not for S3
(conditional PutObject is recent and unevenly supported), not for file://
(a rename is atomic but a compare-and-swap across processes is not, and
inventing one would be a worse lie than not offering the capability), and
not for the https:// client — that one talks to a hub, and the hub is the
process doing the conditional write.

type Object

type Object struct {
	Key  string
	Size int64
	// Modified is when the store last wrote this object, when the backend
	// reports it (S3, GCS) and the zero time when it does not. It is how the
	// hub decides an object can no longer change — see RemoteSource.verify.
	Modified time.Time
}

type PutSigner

type PutSigner interface {
	SignPut(ctx context.Context, key string, size int64, ttl time.Duration) (*SignedPut, error)
}

PutSigner is implemented by backends that can mint presigned upload URLs so clients write to storage directly. Backends without that capability (file://) simply don't implement it, and callers fall back to uploading through the server.

type ReadEvent added in v0.4.0

type ReadEvent struct {
	Path string `json:"path"`
	// Session is the agent session the read happened in, so the hub can join
	// a run's reads to the writes journal.Op.Session carries. A client string
	// — the hub pins each recorded row to the device it validated, never to
	// anything in this body (see handleReadReport).
	Session string    `json:"session,omitempty"`
	Time    time.Time `json:"time,omitzero"`
}

ReadEvent is one agent file read reported to the hub for its read heatmap.

type ReadReporter added in v0.4.0

type ReadReporter interface {
	// ReportReads sends queued reads. kind is "" or "agent" for device
	// traffic (the actor is this device) and "human" for a person reading in
	// a viewer (the actor is the signed-in account, resolved by the hub).
	ReportReads(ctx context.Context, kind string, reads []ReadEvent) error
}

type Scope added in v0.16.0

type Scope struct {
	// Tag identifies this account's current visibility. It changes whenever
	// what this account can see does, and only then.
	Tag string `json:"scope"`
	// ReadOnly are slash-terminated prefixes this device may sync down but
	// must never journal a change to.
	ReadOnly []string `json:"readonly"`
	// Deny are slash-terminated prefixes this account may not read at all.
	// The hub sends no ops for them, so nothing materializes there; this list
	// exists so the device also never JOURNALS anything a local file at such a
	// path would otherwise produce — which the hub would refuse, wedging the
	// device's whole journal. See the note on handleProjectScope for the
	// disclosure this represents and why there is no alternative.
	Deny []string `json:"deny"`
}

Scope is what one account may do inside one project, as the hub sees it. Only ReadOnly is carried: a prefix the caller may not READ at all is deliberately not reported, because naming it would hand every member of a project the name of every hidden folder. See docs/folder-permissions-prd.md.

type Scoper added in v0.16.0

type Scoper interface {
	Scope(ctx context.Context) (Scope, error)
}

Scoper is the optional "what may I write here?" capability, in the PutSigner mold. A hub knows about folder permissions; a raw object store has no account to answer for, so the object-store backends simply do not implement it and nothing is restricted — which is right, since a device talking straight to a bucket already holds credentials for all of it.

type SignedPut

type SignedPut struct {
	URL     string            // upload here
	Method  string            // always "PUT"
	Headers map[string]string // headers that must be sent verbatim (they are signed)
	Expires time.Time
}

SignedPut is a presigned direct-upload request: whoever holds the URL can PUT that one object until Expires, without ever seeing storage credentials.

type Version added in v0.16.1

type Version string

Version identifies one stored revision of an object.

Opaque on purpose: a GCS generation, an S3 ETag and a file backend's
fingerprint have nothing in common but equality, and the moment anything
parses one it has made a promise the next backend cannot keep. Only ever
compared, never inspected.
const VersionAbsent Version = ""

VersionAbsent is the version of an object that does not exist. Passing it to PutIf means "create this, and fail if somebody beat me to it".

type Watcher added in v0.16.0

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

Watcher is the optional change-notification capability, in the PutSigner mold: a hub can tell a device that a peer pushed, so the daemon syncs on the news instead of waiting out its remote interval. Object-store backends simply don't implement it — a bucket has nobody to do the telling, and the daemon keeps polling exactly as it does today.

The channel carries no payload on purpose. The only question the daemon asks is "is it worth talking to the remote now", and the answer to a burst of changes is one cycle, not one per change — so an implementation coalesces and the daemon can never fall behind the stream. A closed channel means the notification path is gone (hub down, connection dropped, an older hub with no such route): callers fall back to polling and may re-dial. It is an accelerator, never a correctness input — nothing may depend on a signal arriving, because for most backends none ever will.

Jump to

Keyboard shortcuts

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