syncer

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: 23 Imported by: 0

Documentation

Overview

Package syncer drives a volume's sync cycle:

scan → commit local ops → pull peer journals → preserve conflicts →
materialize merged state → push blobs + own journal

Scanning always happens before pulling, so local edits are committed to the journal (and their content captured in the blob store) before any remote state can overwrite the working folder. Concurrent edits resolve deterministically last-writer-wins; the losing local version is preserved as a "<name>.bdrive-conflict-<device>-<time>" file that syncs like any other.

Index

Constants

View Source
const IgnoreFile = ".bdriveignore"

IgnoreFile is the per-folder opt-out list at the mount root. It uses a gitignore-style syntax and, unlike the .bdrive settings file, syncs like any other file so every device shares the same rules.

Variables

This section is empty.

Functions

func DisplayTime added in v0.14.0

func DisplayTime(op journal.Op) time.Time

DisplayTime is the timestamp to show a human for an op: when the file was written if we know it, otherwise when the op was committed. Ops written before Op.Mtime existed, and deletes (no file left to stat), fall back.

func EscapeIgnore added in v0.15.0

func EscapeIgnore(rel string) string

EscapeIgnore turns a literal project-relative path into a rule line that matches that path and nothing else. It is compile's inverse and lives beside it on purpose: a caller that spells the escaping itself is a second definition of the dialect, which is how `bdrive forget <name>` came to write a glob, a negation or a comment depending on what a teammate had named a file.

func LogEntries

func LogEntries(st *store.Store, pathFilter string, limit int) ([]journal.Op, error)

LogEntries returns the volume history, newest first.

func Measure added in v0.15.0

func Measure(folder string, include []string) (files int, bytes int64, err error)

Measure reports what a first sync of this folder would actually upload: the number of files and their total bytes, after the same filter the cycle uses. It exists so `bdrive init` can warn about a folder nobody meant to sync (a home directory, a video library, a checkout whose .bdriveignore doesn't cover its build output) BEFORE the first push rather than after.

It is deliberately filter-aware: a 40 GB repo whose bulk is node_modules measures as the few MB that really sync, so the warning fires on the cases that are actually expensive and stays quiet on the ones the starter rules already handle.

Unreadable entries are skipped rather than failing — this is advice, and a permission error in one subtree must never block init.

func NotSyncedFiles added in v0.12.0

func NotSyncedFiles(notSynced []Entry) int

NotSyncedFiles is how many files the not-synced list stands for: collapsed directories count their whole subtree, nested mounts count zero because they do sync — through their own project.

func SortForDisplay added in v0.14.0

func SortForDisplay(ops []journal.Op)

SortForDisplay orders ops newest-first by DisplayTime — the timestamp the user actually sees, so the list reads as a timeline. Ties fall back to reversed journal.Less to stay deterministic. This is deliberately NOT the replay order: LogEntries keeps returning causal order because bdrive restore walks it to find a file's previous version.

func SyncedFiles added in v0.15.0

func SyncedFiles(folder string, include []string, accepted string) ([]string, error)

SyncedFiles lists the mount-relative paths that sync, in walk order. It is the same pure read as Explain — no Session, no volume lock, no network — but without Explain's not-synced accounting, which calls countFiles on every pruned directory: a `bdrive grep` in a repo with node_modules/ would walk node_modules/ in full just to produce a count it throws away.

accepted is the ignore text this device has accepted (store.SyncState's IgnoreAccepted; "" when there is none), for the reason Explain documents: the walk applies Filter.SkipUp, and omitting it would list a file a peer's `!` rule points at that the cycle will not actually send.

Unreadable entries are skipped rather than failing, as everywhere else in this walk.

Types

type Entry added in v0.12.0

type Entry struct {
	Path   string
	Files  int
	Nested bool // syncs through its own project — not excluded
}

Entry is one line of the not-synced list. Path ends in "/" when a whole directory collapsed to a single line; Files is how many files it holds.

func Explain added in v0.12.0

func Explain(folder string, include []string, accepted string) (synced []string, notSynced []Entry, err error)

Explain reports what the sync cycle would and would not send for a folder. It is a pure read: no Session, no volume lock, no network, no writes — the answer comes from the same walk the cycle itself uses, so it cannot drift.

accepted is the ignore text this device has accepted (store.SyncState's IgnoreAccepted; "" when there is none). It is a parameter rather than something loadFilter finds for itself because the answer lives in the volume store and this function deliberately does not open one — but it has to be passed, because the scan applies it (Filter.SkipUp) and an explanation that omits it would report a peer's `!` rule as syncing a file the cycle will not send. That drift is the one thing this function exists not to have.

func (Entry) IsDir added in v0.12.0

func (e Entry) IsDir() bool

IsDir reports whether the entry stands for a whole directory rather than a single file.

type Filter

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

Filter decides which paths sync. A path syncs when it is not ignored and, if an include list is set, matches at least one include pattern.

Pattern syntax (a practical gitignore subset): one pattern per line, blank lines and #-comments skipped, `!` re-includes, a trailing `/` matches directories only, a `/` anywhere else anchors the pattern to the mount root (otherwise it matches at any depth), `*` matches within a path segment, `**` across segments, `?` a single character.

func LoadFilter added in v0.4.0

func LoadFilter(folder string, include []string) (*Filter, error)

LoadFilter builds the filter for a folder from its .bdriveignore (if any) plus the include list from the .bdrive settings file — the exact rules the sync cycle applies, for callers outside the cycle (e.g. `bdrive read-log` deciding whether an agent-read path is part of the project).

func (*Filter) AcceptRules added in v0.15.0

func (f *Filter) AcceptRules(text string)

AcceptRules installs the rules this device has accepted as the floor on what may be uploaded. Passing "" installs no floor.

func (*Filter) Negated added in v0.12.0

func (f *Filter) Negated() bool

Negated reports whether any `!` rule is in play. Scope narrowing is written as negation rules, so this is how callers tell "these rules exclude a few things" from "these rules exclude everything but a few things" — the difference between a safe prune and a destructive one.

func (*Filter) PruneDir

func (f *Filter) PruneDir(rel string) bool

PruneDir reports whether a whole directory can be skipped during the scan walk. Pruning is conservative: never with `!` rules (a child could be re-included) or an include list (a deep child could match).

func (*Filter) Skip

func (f *Filter) Skip(rel string) bool

Skip reports whether a file path should not sync.

func (*Filter) SkipUp added in v0.15.0

func (f *Filter) SkipUp(rel string) bool

SkipUp is Skip for the UPLOAD direction: a path syncs out of this machine only if neither the live rules nor the rules this device has ACCEPTED exclude it.

.bdriveignore syncs, deliberately — scope is team-wide, and round 4 made `sync --prune` refuse when `!` rules are present precisely because of that. That reasoning covered DELETION. Nobody asked what a pulled negation does to the SCAN, and the answer was that one member adding `!.env` to the shared file uploaded every other member's local .env on their next cycle: a file that had never been shared, with no prompt and no local change. The runbook recommends `bdrive init . --only docs,notes` for a repository, which puts the WHOLE repo under the mount with only this synced, teammate-writable file holding the rest of it back.

The asymmetry is the fix, not a ban on shared rules:

  • Pulled rules that NARROW apply immediately, both directions. A teammate excluding something takes effect everywhere, as designed.
  • Pulled rules that WIDEN apply to materialize (a peer's scope decision still delivers their files down) but not to scan. Widening what leaves your disk is a local decision.
  • A device ACCEPTS the current rules the moment it authors them itself — `bdrive init --only`, `bdrive scope add/rm`, or editing the file by hand. That is the acknowledgement, and it is the same act in every case: this machine's user changed the shared rules.

A device joining a project has accepted nothing, so the floor excludes nothing and the pulled rules stand on their own — team-wide scope still works for a new member, which is what a blanket "ignore pulled negations" would have broken.

type Progress added in v0.3.1

type Progress struct {
	Done, Total    int
	Bytes, ToBytes int64
}

Progress reports upload progress during a cycle's push phase, so the CLI can draw a bar. Total/TotalBytes are set once when the push starts; Done/Bytes climb as blobs finish. Nil OnProgress means no reporting (the daemon).

type Result

type Result struct {
	LocalOps     int  // local changes committed to the journal
	PulledOps    int  // ops received from other devices
	Conflicts    int  // conflict copies created
	Pruned       int  // paths removed from the hub by --prune (kept on disk)
	Materialized int  // files written/removed in the working folder
	Pushed       bool // own journal/blobs uploaded
	// Offline reports that the remote leg of this cycle had a problem worth
	// telling the user about — usually "unreachable", but also "the hub served
	// bytes that are not their content address", which is the only signal that
	// case ever produces. It is a REPORT, not a gate: a content-level problem
	// with one object still lets this device push its own work (Pushed may be
	// true alongside it), because otherwise one peer's journal line decides
	// whether anyone else's edits ever leave their machine.
	Offline    bool
	OfflineErr error
	ReadOnly   bool // the hub refused our push: pull-only from here
	NoAccess   bool // the hub refused our pull: sync paused, nothing touched
	AccessErr  error
}

Result summarizes one sync cycle.

Offline, ReadOnly, and NoAccess are three different answers and must not be conflated: offline means the hub could not be reached and everything should be retried; ReadOnly means it refused our push (we keep pulling, local ops stay journaled and unpushed); NoAccess means it refused our pull too, so the cycle does nothing at all and leaves the working folder alone. Regaining access self-heals on a later cycle with no manual step.

func (*Result) Activity

func (r *Result) Activity() bool

func (*Result) Reason added in v0.15.0

func (r *Result) Reason() string

Reason is the hub's own words for a ReadOnly or NoAccess answer, empty when the hub gave none. It is what the CLI prints under the summary line.

type Session

type Session struct {
	Folder  string
	MountID string // the stable project mount id from .bdrive/config.json
	Store   *store.Store
	Device  config.Device
	// Account is the signed-in user (from `bdrive login`); ops carry it so
	// history shows who changed what. Zero on offline/no-auth setups —
	// Device.Author remains the fallback identity.
	Account config.Settings
	// Note, when set, is stamped into every op this session commits — session
	// context like "claude-code session <id>". Empty means fall back to the
	// store's persisted session note (store.LoadNote), which lets a one-shot
	// `bdrive sync --note` leave context that the daemon's later scans also
	// stamp. Conflict-copy ops keep their own explanatory note.
	Note string
	// SessionID is the agent session every op this cycle commits is stamped
	// with (journal.Op.Session). Set only by `bdrive sync --hook`, and
	// deliberately NOT persisted the way Note is (store.SaveNote): the note
	// is context that outlives the hook turn, the session id is an identity
	// that must not be attached to changes the daemon commits on its own
	// later. So a daemon scan after the hook turn carries the note and no
	// session — the asymmetry is intended.
	SessionID string
	// Prune makes this cycle reconcile the hub against the shared ignore
	// rules: every path the remote still holds that .bdriveignore (or a
	// builtin never-sync rule) now excludes is journaled as a delete, so it
	// leaves the hub while staying on disk on every device. Off by default —
	// plain `bdrive sync` and the daemon never set it, because pruning must
	// be a deliberate act, never a side effect of editing .bdriveignore.
	Prune   bool
	Backend remote.Backend // nil = work offline
	// OnProgress, when set, is called during push with upload progress. It may
	// be invoked concurrently from upload workers, so it must be safe to call
	// from multiple goroutines.
	OnProgress func(Progress)
}

Session ties a working folder to its volume store and (optionally) remote.

func (*Session) Cycle

func (s *Session) Cycle(ctx context.Context) (*Result, error)

Cycle runs one full scan/sync/materialize pass under the volume lock.

func (*Session) Restore added in v0.12.0

func (s *Session) Restore(ctx context.Context, path, sha string) error

Restore writes the historical version sha of path back into the working folder as an ordinary local edit. The next Cycle journals it like any other change — nothing here appends to a journal, and no journal is ever rewritten. Restoring is exactly the edit a human could have made by hand, which is why the sync engine needs no new write path for it.

It does not take the volume flock: Cycle does, and holding it here would deadlock the caller that runs both.

Jump to

Keyboard shortcuts

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