Files
beardrive/internal/webapp/quota.go
T
e02bd5330d feat(cloud): launch pricing guardrails — egress caps, ignore defaults, storage tiering (#114)
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>
2026-08-03 19:16:42 +09:00

68 lines
2.7 KiB
Go

package webapp
import "io"
// QuotaProvider is the seam a managed deployment uses to enforce plan
// limits, exactly like AuthProvider is the seam for identity. The
// open-source server ships only UnlimitedQuota; billing and plan logic live
// outside this repo. Hooks fire on every write path (browser uploads, the
// device sync store proxy) and on seat growth, keyed by org id.
type QuotaProvider interface {
// CheckWrite runs before addedBytes land in the org's storage; a non-nil
// error rejects the write (surfaced to the client as 403).
CheckWrite(org string, addedBytes int64) error
// CheckSeat runs before an invite adds a member; members is the current
// count. A non-nil error rejects the join.
CheckSeat(org string, members int) error
// RecordUsage runs after a write succeeds, for accounting.
RecordUsage(org string, addedBytes int64)
// CheckRead runs before bytes are served to an UNAUTHENTICATED reader —
// today that is public share links (/s/*) and nothing else. bytes is the
// size about to be streamed. A non-nil error refuses the transfer and its
// message is shown to the reader, so write it for a stranger who has no
// idea what BearDrive is.
//
// Deliberately NOT called on the sync proxy or the viewer: a device that
// gets refused mid-sync reads it as "access revoked" and stops touching
// the folder, which is a far worse outcome than an over-quota bill. Those
// paths report through RecordEgress and are governed by fair use.
CheckRead(org string, bytes int64) error
// RecordEgress runs after bytes have been served, with the number
// actually written. Every read path reports here — share links, the sync
// proxy, viewer downloads — so egress is measurable even where it is not
// enforced.
RecordEgress(org string, bytes int64)
}
// UnlimitedQuota is the open-source default: everything is allowed.
type UnlimitedQuota struct{}
func (UnlimitedQuota) CheckWrite(string, int64) error { return nil }
func (UnlimitedQuota) CheckSeat(string, int) error { return nil }
func (UnlimitedQuota) RecordUsage(string, int64) {}
func (UnlimitedQuota) CheckRead(string, int64) error { return nil }
func (UnlimitedQuota) RecordEgress(string, int64) {}
// countingWriter counts what actually reached the client. The journal's Size
// field and the stat size are both claims made before the write; a connection
// that drops halfway must not be billed as a full transfer.
type countingWriter struct {
w io.Writer
n int64
}
func (c *countingWriter) Write(p []byte) (int, error) {
n, err := c.w.Write(p)
c.n += int64(n)
return n, err
}
// quota returns the configured provider, defaulting to unlimited.
func (s *Server) quota() QuotaProvider {
if s.Quota != nil {
return s.Quota
}
return UnlimitedQuota{}
}