mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
QuotaProvider grows a read half: CheckRead(org, bytes) and RecordEgress(org, bytes). CheckRead is enforced on /s/* only — a public share link is the sole unauthenticated door to stored bytes, so it is the only egress a plan can cap. The sync proxy and viewer merely RecordEgress: refusing a device mid-cycle surfaces as ErrForbidden, which the syncer reads as "access is gone — pause and touch nothing", and sync must never break over a bill. UnlimitedQuota stays the OSS default. countingWriter bills what actually reached the client rather than a size claimed before the write. bdrive init warns past 1 GiB or 20k files and says how to narrow scope; syncer.Measure sizes that through the real Filter and the one walkFolder predicate. starterIgnore gains video/archive/disk-image globs and Library/ — every version is kept forever, so a big binary committed once is paid for forever on every device. deploy: a Nearline-at-30-days lifecycle rule and the arithmetic for why it stops there. Coldline and Archive only pay off below roughly one read per month, and a first sync pulls every historical blob rather than just the current tree, so blob read rate tracks device onboarding. docs/launch-plan.md said Cloud was waitlist-only and framed Product Hunt as an OSS launch whose goal was not signups or revenue; both are stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package syncer
|
|
|
|
import (
|
|
"io/fs"
|
|
"path/filepath"
|
|
|
|
"github.com/runbear-io/beardrive/internal/config"
|
|
"github.com/runbear-io/beardrive/internal/journal"
|
|
)
|
|
|
|
// verdict is what the sync predicate decided about one entry on disk.
|
|
type verdict int
|
|
|
|
const (
|
|
vDescend verdict = iota // directory, walk into it
|
|
vPruneDir // directory excluded whole (.git, .bdrive, PruneDir)
|
|
vNested // directory is a mount of its own (syncs separately)
|
|
vSkipFile // file excluded (non-regular, .DS_Store/.bdrive-tmp-*, Skip)
|
|
vSync // file syncs
|
|
)
|
|
|
|
// walkFolder walks a mount applying the exact predicate the sync cycle uses.
|
|
// fn sees every entry with its verdict; pruning happens here, so no caller can
|
|
// descend where scan would not. This is the only copy of the rules: scan and
|
|
// Explain both go through it, so what `bdrive scope --explain` reports cannot
|
|
// drift from what actually leaves the machine.
|
|
func walkFolder(folder string, filter *Filter, fn func(abs, rel string, d fs.DirEntry, v verdict) error) error {
|
|
return filepath.WalkDir(folder, func(p string, d fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return nil // skip unreadable entries
|
|
}
|
|
rel, err := filepath.Rel(folder, p)
|
|
if err != nil || rel == "." {
|
|
return nil
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
|
|
var v verdict
|
|
switch {
|
|
case !d.IsDir():
|
|
// ReservedPath, not ignoredFile(name): the builtin exclusions
|
|
// include a whole-path rule (agent hook config) that a base name
|
|
// cannot express, and the outbound half has to match the inbound
|
|
// one or a file lands on the hub that no peer will ever materialize.
|
|
// SkipUp, not Skip: this walk is the upload door, and a negation a
|
|
// TEAMMATE pushed must not widen what leaves this machine. See
|
|
// Filter.SkipUp.
|
|
if !d.Type().IsRegular() || !journal.SafePath(rel) ||
|
|
config.ReservedPath(rel) || filter.SkipUp(rel) {
|
|
v = vSkipFile
|
|
} else {
|
|
v = vSync
|
|
}
|
|
case ignoredDir(d.Name()) || filter.PruneDir(rel):
|
|
v = vPruneDir
|
|
case config.IsMount(p):
|
|
// A mount of its own: it syncs through its own project.
|
|
filter.addNestedMount(rel)
|
|
v = vNested
|
|
default:
|
|
v = vDescend
|
|
}
|
|
|
|
if err := fn(p, rel, d, v); err != nil {
|
|
return err
|
|
}
|
|
if v == vPruneDir || v == vNested {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// 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 Measure(folder string, include []string) (files int, bytes int64, err error) {
|
|
filter, err := LoadFilter(folder, include)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
err = walkFolder(folder, filter, func(_, _ string, d fs.DirEntry, v verdict) error {
|
|
if v != vSync {
|
|
return nil
|
|
}
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
files++
|
|
bytes += info.Size()
|
|
return nil
|
|
})
|
|
return files, bytes, err
|
|
}
|