store

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package store manages a volume's local on-disk state: a content-addressed blob store, the per-device journals, and small JSON state files. Everything a volume needs works offline; the remote is only used to exchange blobs and journals.

Layout under <beardrive home>/volumes/<volume>/:

blobs/<aa>/<sha256>   content-addressed file contents (immutable)
journal/<device>.jsonl per-device op logs (own + cached copies of peers)
state.json            what is currently materialized in the folder
sync.json             lamport clock + push cursor
lock                  flock guarding cycles

Index

Constants

View Source
const (
	AccessOK       = ""          // normal read+write sync
	AccessReadOnly = "read-only" // pushes refused: pull-only
	AccessNone     = "no-access" // pulls refused too: sync paused
)

Access records how the hub answered this device on the last cycle that reached it. It is persisted so `bdrive status` — which never runs a cycle — can report a degraded state, and so the daemon can log a transition once instead of on every tick.

Variables

This section is empty.

Functions

func Paused added in v0.10.0

func Paused(dir string) bool

Paused reports whether syncing is paused for the volume dir.

func SetPaused added in v0.10.0

func SetPaused(dir string, on bool) error

SetPaused sets or clears the paused marker.

func UnderRoot added in v0.15.0

func UnderRoot(root, p string) bool

UnderRoot reports whether p resolves inside root ON DISK — following every symlink on the way, including one planted in a directory that already exists. p need not exist: the deepest existing ancestor is what gets resolved, so the answer is available before anything is created.

A lexical check (filepath.Rel, path.Clean, HasPrefix) answers a question about the STRING. os.MkdirAll, os.CreateTemp, os.Rename and os.Open all answer the question about the FILESYSTEM, and that is the one that decides where the bytes land: "docs/x.md" is a clean relative path by every lexical test and lands outside the root the moment "docs" is a symlink. It lives in this package because the two callers that need it — the syncer materializing a peer's op into a mount, and the file:// backend resolving a key under the hub's storage root — are the same question about different roots.

func WriteFileAtomic

func WriteFileAtomic(path string, data []byte, mode os.FileMode) error

WriteFileAtomic writes data via a temp file in the same directory + rename.

func WriteJSONAtomic

func WriteJSONAtomic(path string, v any) error

WriteJSONAtomic writes v as JSON via temp-file + rename. 0600 for the same reason as the journals: these files list a private project's paths and the accounts that touched them, inside a 0755 $BDRIVE_HOME.

Types

type CachedFile

type CachedFile struct {
	Blob    string `json:"blob"`
	Size    int64  `json:"size"`
	Mode    uint32 `json:"mode"`
	MTimeNS int64  `json:"mtime_ns"`
}

CachedFile records what beardrive last wrote to / observed in the working folder for a path. Size+MTimeNS make change detection cheap; Blob ties it back to content. The cache is per mount (one volume can be materialized into several folders, each with its own stat fingerprints).

type InboundEvent added in v0.15.0

type InboundEvent struct {
	Path    string    `json:"path"`
	Deleted bool      `json:"deleted,omitempty"`
	Time    time.Time `json:"time"`
}

InboundEvent is one path a cycle wrote or removed on a peer's behalf (mount-relative).

type Note added in v0.4.0

type Note struct {
	Text    string    `json:"text"`
	Expires time.Time `json:"expires,omitzero"` // zero = never expires
}

Note is the on-disk shape of the session note.

type ReadEvent added in v0.4.0

type ReadEvent struct {
	Path string `json:"path"`
	// Session is the agent session the read happened in, from the same hook
	// payload the sync hook stamps journal.Op.Session from. Empty for reads
	// with no session (a platform that reports none, or an older client).
	Session string    `json:"session,omitempty"`
	Time    time.Time `json:"time"`
}

ReadEvent is one observed read of a synced file (mount-relative path).

type Store

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

func Open

func Open(dir string) (*Store, error)

func (*Store) AllOps

func (s *Store) AllOps() ([]journal.Op, error)

AllOps returns the union of every journal known locally.

func (*Store) AppendOps

func (s *Store) AppendOps(device string, ops []journal.Op) error

func (*Store) BlobPath

func (s *Store) BlobPath(sum string) string

BlobPath is the on-disk location of a blob. A key that is not a sha256 resolves to a name inside the blob dir that PutBlobReader never writes, so HasBlob and OpenBlob fail naturally instead of reaching somewhere else.

func (*Store) ClearNote added in v0.4.0

func (s *Store) ClearNote() error

ClearNote removes the session note.

func (*Store) ClearPendingReads added in v0.4.0

func (s *Store) ClearPendingReads() error

ClearPendingReads drops the batch PendingReads returned, after a successful report.

func (*Store) DeviceOps

func (s *Store) DeviceOps(device string) ([]journal.Op, error)

func (*Store) Devices

func (s *Store) Devices() ([]string, error)

Devices lists device IDs that have a local journal copy.

func (*Store) Dir

func (s *Store) Dir() string

func (*Store) DrainInbound added in v0.15.0

func (s *Store) DrainInbound() ([]InboundEvent, error)

DrainInbound returns the queued batch and clears it, deduplicated by path (latest event wins, so a path written and then deleted reports as deleted). The spool is rotated aside first, so events logged during the drain land in a fresh spool — the drain runs outside the volume flock, and a daemon on the same mount may be appending.

One call, unlike PendingReads/ClearPendingReads: those are two steps because a read report can fail over the network and must be retried, and rendering a string onto stdout cannot.

func (*Store) HasBlob

func (s *Store) HasBlob(sum string) bool

func (*Store) JournalPath

func (s *Store) JournalPath(device string) string

func (*Store) LoadCache

func (s *Store) LoadCache(mountID string) (map[string]CachedFile, error)

func (*Store) LoadNote added in v0.4.0

func (s *Store) LoadNote() string

LoadNote returns the live session note, or "" if none is set or it has expired. Never errors: a missing or unreadable note is just no note.

func (*Store) LoadSync

func (s *Store) LoadSync() (SyncState, error)

func (*Store) Lock

func (s *Store) Lock() (func() error, error)

Lock takes an exclusive flock for the volume, serializing sync cycles between the daemon and one-shot commands. Blocks until acquired.

func (*Store) LogInbound added in v0.15.0

func (s *Store) LogInbound(rel string, deleted bool) error

LogInbound appends one materialized path to the spool. Single-line O_APPEND writes keep the daemon and a concurrent CLI cycle from interleaving.

func (*Store) LogRead added in v0.4.0

func (s *Store) LogRead(rel, session string) error

LogRead appends one read event to the spool. Single-line O_APPEND writes keep concurrent hook invocations from interleaving.

func (*Store) OpenBlob

func (s *Store) OpenBlob(sum string) (*os.File, error)

func (*Store) PendingReads added in v0.4.0

func (s *Store) PendingReads() ([]ReadEvent, error)

PendingReads returns the queued batch awaiting report, deduplicated by (path, session) — latest time wins. Not by path alone: two agent sessions on one device between syncs both reading wiki/a.md are two reads by two sessions, and collapsing them would report one, carrying whichever session happened to flush last — one session's reads silently credited to another. The spool is rotated aside first, so events logged after this call land in a fresh spool; the batch survives until ClearPendingReads — a failed report is simply retried next cycle.

func (*Store) PutBlobBytes

func (s *Store) PutBlobBytes(b []byte) (string, int64, error)

func (*Store) PutBlobFile

func (s *Store) PutBlobFile(path string) (string, int64, error)

func (*Store) PutBlobReader

func (s *Store) PutBlobReader(r io.Reader) (string, int64, error)

PutBlobReader streams r into the blob store, returning its sha256 and size.

func (*Store) SaveCache

func (s *Store) SaveCache(mountID string, c map[string]CachedFile) error

func (*Store) SaveNote added in v0.4.0

func (s *Store) SaveNote(text string, ttl time.Duration) error

SaveNote sets the session note. ttl > 0 bounds its life; empty text clears.

func (*Store) SaveSync

func (s *Store) SaveSync(st SyncState) error

type SyncState

type SyncState struct {
	Lamport   int64  `json:"lamport"`
	PushedOps int64  `json:"pushed_ops"`       // how many of our own ops the remote has
	Access    string `json:"access,omitempty"` // "", "read-only", or "no-access"
	// AccessReason is the hub's own words for the last refusal. Without it every
	// 403 renders as the same "read-only (pull only)" line, and the hub's most
	// actionable answer — "this device is not registered to your account on this
	// hub; run `bdrive login`" — reached nobody: the one state a user cannot
	// diagnose from the outside was the one the CLI summarized away.
	AccessReason string `json:"access_reason,omitempty"`

	// IgnoreAccepted is the .bdriveignore text whose scan scope THIS device has
	// accepted, and IgnorePulled is the text a peer's version last wrote here.
	// Together they tell a locally-authored rule change from one that arrived
	// over the wire, which is what keeps a teammate's `!` from widening what
	// leaves this disk. See syncer.Filter.SkipUp.
	IgnoreAccepted string `json:"ignore_accepted,omitempty"`
	IgnorePulled   string `json:"ignore_pulled,omitempty"`
}

Jump to

Keyboard shortcuts

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