Documentation
¶
Overview ¶
Package webapp serves the bdrive web server: a browsable web view of synced files (file tree reconstructed from the journals, rendered markdown, downloads), browser uploads, and — in hub mode — the sync API that lets storage-blind client devices sync whole projects through this server.
Two modes:
- single-volume: Source is set (a DirSource for a plain folder, or a RemoteSource in tests); the classic viewer.
- hub: Root + Projects are set; the server hosts many projects, each a volume stored under <root>/<project-id>/ in the object store, managed by a file-backed project registry.
The client — browser or syncing device — is deliberately told nothing about the storage: no remote URL, bucket, or credentials ever appear in an API response.
Index ¶
- Constants
- func MigrateOrgs(projects *ProjectDB, orgs *OrgDB, accounts []User) error
- func RenderMarkdown(src []byte) (string, error)
- type AccountRepo
- type AuthProvider
- type BuiltinAuth
- func (a *BuiltinAuth) Accounts() []User
- func (a *BuiltinAuth) Approve(id string) error
- func (a *BuiltinAuth) Authenticate(r *http.Request) (User, bool)
- func (a *BuiltinAuth) CLILoginPath() string
- func (a *BuiltinAuth) Deny(id string) error
- func (a *BuiltinAuth) PendingUsers() []User
- func (a *BuiltinAuth) Register(mux *http.ServeMux)
- func (a *BuiltinAuth) SetPolicy(requireVerification, requireApproval bool) error
- func (a *BuiltinAuth) ValidateSignupPolicy() error
- type DeviceInfo
- type DeviceRegistry
- type DeviceRepo
- type DirSource
- type DirectUploader
- type FileInfo
- type HistoryEntry
- type Identity
- type Mailer
- type MetaStore
- type Node
- type Org
- type OrgDB
- func (db *OrgDB) AddMember(orgID, email, role string) error
- func (db *OrgDB) Create(name, ownerEmail string) (Org, error)
- func (db *OrgDB) CreateInvite(orgID, creator string, ttl time.Duration) (OrgInvite, error)
- func (db *OrgDB) Get(id string) (Org, bool)
- func (db *OrgDB) ListInvites(orgID string) []OrgInvite
- func (db *OrgDB) OrgsFor(email string) []Org
- func (db *OrgDB) RecordInviteUse(token string)
- func (db *OrgDB) Redeem(token string) (OrgInvite, bool)
- func (db *OrgDB) RemoveMember(orgID, email string) error
- func (db *OrgDB) Rename(orgID, name string) error
- func (db *OrgDB) RevokeInvite(token string) bool
- func (db *OrgDB) Role(orgID, email string) string
- func (db *OrgDB) SetRole(orgID, email, role string) error
- func (db *OrgDB) ValidInvite(token string) bool
- type OrgInvite
- type OrgRepo
- type Project
- type ProjectDB
- func (db *ProjectDB) Delete(id string) error
- func (db *ProjectDB) Get(id string) (Project, bool)
- func (db *ProjectDB) GetOrCreate(name, org string) (Project, bool, error)
- func (db *ProjectDB) List() []Project
- func (db *ProjectDB) Rename(id, name string) error
- func (db *ProjectDB) SetOrg(id, org string) error
- type ProjectRepo
- type QuotaProvider
- type RemoteSource
- func (r *RemoteSource) Commit(ctx context.Context, p, blob string, size int64, who User) error
- func (r *RemoteSource) Files(ctx context.Context) (map[string]FileInfo, error)
- func (r *RemoteSource) HasBlob(ctx context.Context, blob string) (bool, error)
- func (r *RemoteSource) Open(ctx context.Context, _ string, fi FileInfo) (io.ReadCloser, error)
- func (r *RemoteSource) SignBlobPut(ctx context.Context, blob string, size int64, ttl time.Duration) (*remote.SignedPut, error)
- func (r *RemoteSource) Upload(ctx context.Context, p string, src io.Reader, _ int64, who User) error
- type Server
- type Share
- type ShareDB
- type ShareRepo
- type Source
- type UnlimitedQuota
- type UploadConfig
- type Uploader
- type User
Constants ¶
const ( RoleOwner = "owner" RoleMember = "member" )
const DefaultInviteTTL = 7 * 24 * time.Hour
DefaultInviteTTL bounds invite links that don't ask for an expiry.
DefaultShareRPM is the per-IP sustained rate on /s/* when the config doesn't say otherwise.
const DefaultUploadTTL = 15 * time.Minute
DefaultUploadTTL is used when UploadConfig.TTL is unset: long enough for a slow upload, short enough that a leaked URL goes stale quickly.
Variables ¶
This section is empty.
Functions ¶
func MigrateOrgs ¶ added in v0.3.0
MigrateOrgs assigns every org-less project to a default org so a hub that predates organizations keeps working with zero manual steps. All existing accounts join it — they could all see every project before, so anything narrower would lock someone out — with the oldest account as owner.
func RenderMarkdown ¶
RenderMarkdown converts markdown to HTML (GFM + wikilinks). Raw HTML in the source is escaped by goldmark's safe default.
Types ¶
type AccountRepo ¶ added in v0.3.0
type AccountRepo interface {
Load() (users []*authUser, tokens []authToken, policy *authPolicy, err error)
PutAccount(u *authUser) error
DeleteAccount(id string) error
PutToken(t authToken) error
DeleteToken(hash string) error
PutPolicy(p authPolicy) error
}
AccountRepo persists accounts, device tokens, and the (singleton) signup policy. Load returns everything at open; every other method is one record.
type AuthProvider ¶
type AuthProvider interface {
// CLILoginPath is the page `bdrive login` opens in a browser. The CLI
// appends ?redirect=http://127.0.0.1:<port>/callback&state=<nonce>.
CLILoginPath() string
// Authenticate resolves the request's Bearer token or session cookie.
Authenticate(r *http.Request) (User, bool)
// Register mounts the provider's own pages and endpoints (/auth/*,
// /api/auth/*) on the server mux.
Register(mux *http.ServeMux)
}
AuthProvider is the seam between the server and an identity system.
type BuiltinAuth ¶
type BuiltinAuth struct {
AllowSignup bool
Mail *Mailer // nil → reset links go to the server log
// Public-URL signup gating (all optional; set after Open). A hub reachable
// from the internet should use at least one of these.
AllowedDomains []string // if non-empty, signup email domain must match one
RequireVerification bool // new accounts must click an email link before activation
RequireApproval bool // new accounts wait for an admin to approve them
Admins map[string]bool // hub admins (lowercase emails): approve users, govern shares
Brand string // optional name shown on the sign-in page
// InviteValid, when set, reports whether a token is a live org invite.
// It lets an invite link bootstrap an account on an invite-only hub
// (AllowSignup false) — the one path in without self-signup. Wired to
// OrgDB.ValidInvite by the server. Nil → no invite-based signup.
InviteValid func(token string) bool
// contains filtered or unexported fields
}
BuiltinAuth is the open-source identity provider: email + password + name accounts and long-lived device tokens, persisted in one JSON file (loaded at open, rewritten atomically on every change — same discipline as the project registry). It owns the /auth/* pages the browser sees and the /api/auth/* endpoints the CLI uses.
func NewBuiltinAuth ¶ added in v0.3.0
func NewBuiltinAuth(store AccountRepo, allowSignup bool, mail *Mailer) (*BuiltinAuth, error)
NewBuiltinAuth builds the account service over an AccountRepo, loading its accounts, tokens, and persisted policy.
func OpenBuiltinAuth ¶
func OpenBuiltinAuth(path string, allowSignup bool, mail *Mailer) (*BuiltinAuth, error)
OpenBuiltinAuth loads (or starts) the file-backed account registry at path.
func (*BuiltinAuth) Accounts ¶ added in v0.3.0
func (a *BuiltinAuth) Accounts() []User
Accounts returns every account, oldest first (used by the org migration to pick the default org's owner).
func (*BuiltinAuth) Approve ¶ added in v0.3.0
func (a *BuiltinAuth) Approve(id string) error
Approve activates a pending account.
func (*BuiltinAuth) Authenticate ¶
func (a *BuiltinAuth) Authenticate(r *http.Request) (User, bool)
func (*BuiltinAuth) CLILoginPath ¶
func (a *BuiltinAuth) CLILoginPath() string
func (*BuiltinAuth) Deny ¶ added in v0.3.0
func (a *BuiltinAuth) Deny(id string) error
Deny removes a pending account.
func (*BuiltinAuth) PendingUsers ¶ added in v0.3.0
func (a *BuiltinAuth) PendingUsers() []User
PendingUsers lists accounts awaiting admin approval, oldest first.
func (*BuiltinAuth) Register ¶
func (a *BuiltinAuth) Register(mux *http.ServeMux)
func (*BuiltinAuth) SetPolicy ¶ added in v0.3.0
func (a *BuiltinAuth) SetPolicy(requireVerification, requireApproval bool) error
SetPolicy updates the tunable gating toggles and persists them.
func (*BuiltinAuth) ValidateSignupPolicy ¶ added in v0.3.0
func (a *BuiltinAuth) ValidateSignupPolicy() error
ValidateSignupPolicy rejects incoherent signup configurations at startup so a hub is never accidentally left open to fake-email signups. The three supported postures are: invite-only (AllowSignup false — the default), approval-gated, and domain-restricted with email verification.
- Open self-signup must carry at least one gate (allowed domains, admin approval, or email verification). Without one, anyone can register any address — the exact hole this guards.
- Email verification needs a mailer: without SMTP the link only reaches the server log, so it can't actually gate real users.
type DeviceInfo ¶
type DeviceInfo struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
OS string `json:"os,omitempty"`
User string `json:"user,omitempty"` // account email last seen using this device
IP string `json:"ip,omitempty"` // as observed by the server
LastSeen time.Time `json:"last_seen"`
}
DeviceInfo is what the server knows about one syncing device: self-reported name/OS (headers sent by the client), plus what the server itself observed (public IP of the last push, last activity, the signed-in account). History joins ops against this registry, so IPs are real — as the server saw them — and ops stay small.
type DeviceRegistry ¶
type DeviceRegistry struct {
// contains filtered or unexported fields
}
DeviceRegistry is the in-memory device table over a MetaStore DeviceRepo.
func NewDeviceRegistry ¶ added in v0.3.0
func NewDeviceRegistry(repo DeviceRepo) (*DeviceRegistry, error)
NewDeviceRegistry builds the registry over a repo, loading its contents.
func OpenDeviceRegistry ¶
func OpenDeviceRegistry(path string) (*DeviceRegistry, error)
OpenDeviceRegistry loads the file-backed registry at path.
func (*DeviceRegistry) Get ¶
func (r *DeviceRegistry) Get(id string) (DeviceInfo, bool)
func (*DeviceRegistry) Observe ¶
func (r *DeviceRegistry) Observe(d DeviceInfo)
Observe merges what a request revealed about a device. Disk writes are throttled: identity changes persist immediately, bare last-seen bumps at most once a minute.
type DeviceRepo ¶ added in v0.3.0
type DeviceRepo interface {
Load() ([]DeviceInfo, error)
Put(d DeviceInfo) error
}
type DirSource ¶
type DirSource struct {
Root string
}
DirSource serves a plain local folder straight from disk — no bdrive remote or volume needed. Meant for debugging the webapp (and as a quick local markdown browser): the tree reflects the folder live, provenance is just file mtimes, and content streams from the filesystem.
type DirectUploader ¶
type DirectUploader interface {
Uploader
SignBlobPut(ctx context.Context, blob string, size int64, ttl time.Duration) (*remote.SignedPut, error)
HasBlob(ctx context.Context, blob string) (bool, error)
Commit(ctx context.Context, path, blob string, size int64, who User) error
}
DirectUploader is additionally implemented by sources whose storage can accept presigned direct uploads.
type FileInfo ¶
FileInfo is the resolved state of one path: content identity (Blob doubles as the ETag), plus provenance where the source knows it.
type HistoryEntry ¶
type HistoryEntry struct {
Time string `json:"time"`
Kind string `json:"kind"` // put | delete
Path string `json:"path"`
Size int64 `json:"size,omitempty"`
Blob string `json:"blob,omitempty"` // sha256; fetch via the blob endpoint
User string `json:"user,omitempty"`
UserName string `json:"user_name,omitempty"`
Author string `json:"author,omitempty"` // offline/git fallback identity
Device DeviceInfo `json:"device"`
Note string `json:"note,omitempty"`
}
HistoryEntry is one change as the history API reports it.
type Identity ¶
type Identity struct {
ID, Name, Author string
}
Identity is the device identity uploads are journaled under.
type Mailer ¶
type Mailer struct {
Host string // e.g. smtp.gmail.com
Port int // e.g. 587 (STARTTLS)
User string
Pass string
From string // e.g. drive@example.com
}
Mailer sends plain-text mail over SMTP — the lowest-common-denominator transport a self-hoster can point at anything (Gmail app password, SES, Mailgun, a local relay). No SDK, stdlib only. A nil Mailer reports itself as unconfigured so callers can fall back to logging the message.
type MetaStore ¶ added in v0.3.0
type MetaStore interface {
Accounts() AccountRepo
Projects() ProjectRepo
Orgs() OrgRepo
Devices() DeviceRepo
Close() error
}
MetaStore is the hub's metadata persistence, split into one typed repository per entity. It holds ONLY the control plane — accounts, tokens, projects, orgs, invites, shares, devices. File content and the append-only journals live in the object store and never touch this; ephemeral state (one-time login and device codes, rate-limit buckets) stays in memory.
A deployment chooses the backend: `file` (JSON on disk, the zero-dependency default) or `sql` (SQLite locally, Postgres/Supabase in production). The service structs (BuiltinAuth, OrgDB, …) keep their in-memory maps, mutexes, and business logic and persist each change through these repos — so reads stay in memory and writes are a single record apiece, which every backend implements as one real row.
func OpenFileStore ¶ added in v0.3.0
OpenFileStore builds the file backend over dir, using the historical filenames (auth.json, projects.json, orgs.json, shares.json, devices.json).
func OpenSQLStore ¶ added in v0.3.0
OpenSQLStore opens (and migrates) a SQL metadata store. driver is "sqlite" or "pgx" (Postgres/Supabase); dsn is the connection string / file path.
type Node ¶
type Node struct {
Name string `json:"name"`
Path string `json:"path"`
Dir bool `json:"dir"`
Size int64 `json:"size,omitempty"`
Time time.Time `json:"time,omitzero"`
Author string `json:"author,omitempty"`
Device string `json:"device,omitempty"`
Children []*Node `json:"children,omitempty"`
}
Node is one entry of the file tree returned by the tree endpoint.
type Org ¶ added in v0.3.0
type Org struct {
ID string `json:"id"`
Name string `json:"name"`
Members map[string]string `json:"members"` // lowercase email → role
Created time.Time `json:"created"`
}
Org is one organization.
type OrgDB ¶ added in v0.3.0
type OrgDB struct {
// contains filtered or unexported fields
}
OrgDB is the in-memory org registry over a MetaStore OrgRepo (orgs + invites).
func (*OrgDB) AddMember ¶ added in v0.3.0
AddMember adds (or keeps) the account in the org with the given role. An existing member's role is never downgraded by an invite.
func (*OrgDB) CreateInvite ¶ added in v0.3.0
CreateInvite mints a join link for the org.
func (*OrgDB) ListInvites ¶ added in v0.3.0
ListInvites returns the org's live (non-expired) invites.
func (*OrgDB) OrgsFor ¶ added in v0.3.0
OrgsFor returns the orgs the account belongs to, sorted by name.
func (*OrgDB) RecordInviteUse ¶ added in v0.3.0
RecordInviteUse bumps the join counter for an invite (best effort).
func (*OrgDB) Redeem ¶ added in v0.3.0
Redeem consumes nothing — an invite link can onboard a whole team until it expires — it just resolves the token to its live invite.
func (*OrgDB) RemoveMember ¶ added in v0.3.0
RemoveMember drops an account from the org. The last owner cannot be removed (an org must always have someone who can administer it).
func (*OrgDB) RevokeInvite ¶ added in v0.3.0
RevokeInvite deletes an invite so its link stops working immediately.
func (*OrgDB) Role ¶ added in v0.3.0
Role returns the account's role in the org, or "" for non-members.
func (*OrgDB) SetRole ¶ added in v0.3.0
SetRole changes an account's role. Demoting the last owner is refused.
func (*OrgDB) ValidInvite ¶ added in v0.3.0
ValidInvite reports whether a token is a live invite, without consuming it. It lets the signup page permit account creation from an invite link even when public self-signup is closed (invite-only hubs).
type OrgInvite ¶ added in v0.3.0
type OrgInvite struct {
Token string `json:"token"`
Org string `json:"org"`
Creator string `json:"creator,omitempty"` // account email
Created time.Time `json:"created"`
Expires time.Time `json:"expires"`
Uses int `json:"uses"` // how many accounts have joined via this link
}
OrgInvite is a mint-once join link. Redeeming it while signed in adds the account to the org as a member.
type Project ¶
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Org string `json:"org,omitempty"` // owning organization
Created time.Time `json:"created"`
}
Project is one synced project hosted by this server. Its storage lives under <root>/<id>/ in the object store; the id is permanent, the name is a renameable label.
type ProjectDB ¶
type ProjectDB struct {
// contains filtered or unexported fields
}
ProjectDB is the server's project registry: an in-memory index over a MetaStore ProjectRepo. Reads are served from memory; every change is persisted as one record through the repo (file or SQL).
func NewProjectDB ¶ added in v0.3.0
func NewProjectDB(repo ProjectRepo) (*ProjectDB, error)
NewProjectDB builds the registry over a repo, loading its current contents.
func OpenProjectDB ¶
OpenProjectDB loads the file-backed registry at path (a missing file is an empty registry) — the zero-dependency default.
func (*ProjectDB) Delete ¶ added in v0.3.0
Delete removes a project from the registry. Its storage prefix (blobs, journals) is left in the object store — the id is retired, not scrubbed — so the caller decides whether to reclaim that space out of band.
func (*ProjectDB) GetOrCreate ¶
GetOrCreate returns the project with the given name in the org, creating it (with a fresh id) if none exists. Names are matched exactly, scoped to the org: two organizations can each have a "wiki".
type ProjectRepo ¶ added in v0.3.0
type QuotaProvider ¶ added in v0.3.0
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)
}
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 RemoteSource ¶
type RemoteSource struct {
Backend remote.Backend
// Device identifies this server in ops it journals for uploads. Required
// for uploads; irrelevant for reading.
Device Identity
// contains filtered or unexported fields
}
RemoteSource reads a beardrive remote: it fetches every journal and folds the ops into the current volume state (same total order as journal.Replay, but keeping author/device/time of the winning op per path). With Device set it also accepts uploads, journaled under that identity.
func (*RemoteSource) Commit ¶
Commit appends a put op for path→blob to this server's own journal. It refuses if the blob is not in the store yet (a peer must never see an op whose content is missing). Only this server writes this journal key, so the read-modify-write below has a single writer; upmu serializes it across concurrent requests.
func (*RemoteSource) Open ¶
func (r *RemoteSource) Open(ctx context.Context, _ string, fi FileInfo) (io.ReadCloser, error)
type Server ¶
type Server struct {
// Single-volume mode: serve exactly this source.
Source Source
Volume string // display only
// Hub mode (when Root is set): many projects on one storage root.
Root remote.Backend
Projects *ProjectDB
// Device identifies this server in ops it journals for browser uploads.
Device Identity
Refresh time.Duration
Upload UploadConfig
// Auth, when set, gates the whole API behind sign-in. Nil means the
// historical trusted-network behavior: no accounts, everyone welcome.
Auth AuthProvider
// Devices, when set, records what the server observes about syncing
// devices (name, OS, public IP, last activity) for history.
Devices *DeviceRegistry
Shares *ShareDB
// Orgs, when set, walls projects off by organization membership.
Orgs *OrgDB
// Quota, when set, enforces plan limits (managed deployments). Nil
// means UnlimitedQuota: the open-source server never says no.
Quota QuotaProvider
// 0 means DefaultShareRPM.
ShareRPM int
// contains filtered or unexported fields
}
Server renders volumes as a website and, in hub mode, brokers sync for client devices.
type ShareDB ¶
type ShareDB struct {
// contains filtered or unexported fields
}
ShareDB is the in-memory share registry over a MetaStore ShareRepo.
func NewShareDB ¶ added in v0.3.0
NewShareDB builds the registry over a repo, loading its contents.
func OpenShareDB ¶
OpenShareDB loads the file-backed registry at path.
func (*ShareDB) Create ¶
Create returns a share for (project, path), reusing an existing live one so repeated shares of the same file hand out the same URL.
type Source ¶
type Source interface {
Files(ctx context.Context) (map[string]FileInfo, error)
Open(ctx context.Context, path string, fi FileInfo) (io.ReadCloser, error)
}
Source supplies the file set and content of one volume. Implementations: RemoteSource (a beardrive remote) and DirSource (a plain local folder).
type UnlimitedQuota ¶ added in v0.3.0
type UnlimitedQuota struct{}
UnlimitedQuota is the open-source default: everything is allowed.
func (UnlimitedQuota) CheckSeat ¶ added in v0.3.0
func (UnlimitedQuota) CheckSeat(string, int) error
func (UnlimitedQuota) CheckWrite ¶ added in v0.3.0
func (UnlimitedQuota) CheckWrite(string, int64) error
func (UnlimitedQuota) RecordUsage ¶ added in v0.3.0
func (UnlimitedQuota) RecordUsage(string, int64)
type UploadConfig ¶
type UploadConfig struct {
Enabled bool
// TTL bounds the lifetime of presigned direct-upload URLs.
TTL time.Duration
}
UploadConfig controls whether and how clients may write.