webapp

package
v0.14.0 Latest Latest
Warning

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

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

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

View Source
const (
	RoleOwner  = "owner"
	RoleMember = "member"
)
View Source
const (
	PermNone  = "none"  // the project is hidden: absent from the list, 403 everywhere
	PermRead  = "read"  // browse, view, download, history, heat
	PermWrite = "write" // + upload, sync push, share links
	PermAdmin = "admin" // + rename, delete, edit this project's permissions
)
View Source
const (
	ReadKindHuman = "human"
	ReadKindAgent = "agent"
	ReadKindShare = "share"
)

Read kinds.

View Source
const DefaultAnalyticsHost = "https://us.i.posthog.com"

DefaultAnalyticsHost is PostHog's US cloud ingestion host.

View Source
const DefaultInviteTTL = 7 * 24 * time.Hour

DefaultInviteTTL bounds invite links that don't ask for an expiry.

View Source
const (

	// DefaultReadRetentionDays is how long daily buckets keep per-day
	// resolution before folding into the all-time row.
	DefaultReadRetentionDays = 400
)
View Source
const DefaultShareRPM = 120

DefaultShareRPM is the per-IP sustained rate on /s/* when the config doesn't say otherwise.

View Source
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

View Source
var ErrManagedElsewhere = errors.New("this organization is managed outside this hub")

ErrManagedElsewhere is returned by a directory that does not own its organizations. Handlers turn it into 409 plus the org's ManageURL — the request was well-formed, it is the state of the world that makes it wrong.

Functions

func MigrateOrgs added in v0.3.0

func MigrateOrgs(projects *ProjectDB, orgs orgWriter, accounts []User) error

func RenderMarkdown

func RenderMarkdown(src []byte) (string, error)

RenderMarkdown converts markdown to HTML (GFM + wikilinks). Raw HTML in the source is escaped by goldmark's safe default. A leading YAML frontmatter block renders as a small key/value table instead of the broken thematic-break soup goldmark would make of it.

Types

type AccountApprover added in v0.9.0

type AccountApprover interface {
	PendingUsers() []User
	Approve(id string) error
	Deny(id string) error
	SetPolicy(requireVerification, requireApproval bool) error
	// Policy reports the signup gates as configured. The provider assembles
	// it, so the hub never reaches into provider fields to render the page.
	Policy() SignupPolicy
}

AccountApprover is the optional half of account administration: signup policy and the approval queue behind /api/admin/*. A provider whose accounts live in an external identity system does not implement it, and those routes say so (503) rather than pretending the queue is empty.

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 AnalyticsConfig added in v0.13.0

type AnalyticsConfig struct {
	Key  string // PostHog project key; empty disables analytics entirely
	Host string // PostHog API host; empty means DefaultAnalyticsHost
}

AnalyticsConfig points the frontend at a PostHog project. The key is a public write-only project token, not a credential — it is served to signed- out visitors too, because the app shell loads before login.

func (AnalyticsConfig) Endpoint added in v0.13.0

func (a AnalyticsConfig) Endpoint() string

Endpoint is Host with the default applied. Exported because the same config drives more than the app shell in a managed deployment (the cloud module's marketing pages render their own loader from it).

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)
	// Accounts lists every account the provider knows, oldest first. Startup
	// tasks (the org migration) need it, and both implementations already had
	// it — declaring it here stops callers reaching for a concrete type.
	Accounts() []User
}

AuthProvider is the seam between the server and an identity system.

type Brander added in v0.9.0

type Brander interface{ Branding() string }

Brander is the optional hub-name half: a provider that renders its own sign-in pages knows what to call this hub.

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) Branding added in v0.9.0

func (a *BuiltinAuth) Branding() string

Branding is the hub name this provider renders on its own pages.

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) Policy added in v0.9.0

func (a *BuiltinAuth) Policy() SignupPolicy

Policy reports this provider's signup gates (webapp.AccountApprover). The provider assembles it so the hub never reaches into these fields itself.

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 CLIAuth added in v0.14.0

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

CLIAuth is the CLI-facing half of signing in, whole: the loopback browser flow (/auth/cli → one-time code → /api/auth/exchange), the headless device flow (/api/auth/device/start → approval link → /api/auth/device/poll), and the approval page both of them show.

It is its own type rather than methods on an AuthProvider because this half of the protocol is identical no matter where the accounts live: `bdrive login` POSTs fixed paths and expects fixed JSON, so a provider differs only in who the browser session is and how a device token is minted — the two hooks below. The managed hub's provider used to carry its own copy of all of this, and the copy drifted: months after the OSS flow moved to a single approval link that names the device, the copy was still printing a four-byte code to retype into a text box. One implementation, every provider, nothing to keep in sync.

func NewCLIAuth added in v0.14.0

func NewCLIAuth(session func(*http.Request) (User, bool), issue func(w http.ResponseWriter, userID, device string)) *CLIAuth

NewCLIAuth wires the two provider-specific pieces. session resolves the browser session — cookie only, never a Bearer token, or a device token could approve the next device. issue writes the CLI's {token, user} response for an approved grant.

func (*CLIAuth) Register added in v0.14.0

func (c *CLIAuth) Register(mux *http.ServeMux)

Register mounts the paths `bdrive login` knows. They are fixed: an older CLI on a newer hub must still find them.

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 ops stay small — but it reports only id/name/os (historyDevice, history.go): the IP is recorded here, not repeated to every project member on every change.

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.

func (*DirSource) Files

func (d *DirSource) Files(_ context.Context) (map[string]FileInfo, error)

func (*DirSource) Open

func (d *DirSource) Open(_ context.Context, path string, _ FileInfo) (io.ReadCloser, error)

Open streams a file from disk. Paths are only ever snapshot map keys (produced by Files above), so they cannot escape Root.

func (*DirSource) Upload

func (d *DirSource) Upload(_ context.Context, p string, src io.Reader, _ int64, _ User) error

Upload writes the file atomically under Root. There is no journal here; on a mounted folder the daemon scans, journals, and syncs it like any local edit.

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)
	// note rides along on the journaled op — "" for an ordinary upload,
	// "restore <path>@<sha8>" when the write is a restore.
	Commit(ctx context.Context, path, blob string, size int64, who User, note string) error
}

DirectUploader is additionally implemented by sources whose storage can accept presigned direct uploads.

type Directory added in v0.9.0

type Directory interface {
	// ---- reads (request path) ----
	Role(orgID, email string) string
	Get(orgID string) (Org, bool)
	OrgsFor(email string) []Org
	ListInvites(orgID string) []OrgInvite
	ValidInvite(token string) bool
	// ManageURL is where this org is administered: a path within this hub
	// when it owns its orgs, an external page when it does not. The client
	// follows it and never has to know which kind of hub it is talking to.
	ManageURL(orgID string) string

	// ---- writes (ErrManagedElsewhere when the directory is read-only) ----
	Create(name, ownerEmail string) (Org, error)
	Rename(orgID, name string) error
	AddMember(orgID, email, role string) error
	SetRole(orgID, email, role string) error
	RemoveMember(orgID, email string) error
	CreateInvite(orgID, creator string, ttl time.Duration) (OrgInvite, error)
	RevokeInvite(token string) bool
	Redeem(token string) (OrgInvite, bool)
	RecordInviteUse(token string)
}

Directory is where a hub's organizations live. The built-in one is this package's OrgDB (LocalDirectory); a deployment whose users, orgs and memberships are owned by an external identity system supplies its own, alongside its AuthProvider.

Two rules shape this interface:

Reads are on the request path. Role is called for every project request, including the /store/* sync endpoints a device hits every few seconds with a device token that carries no identity claims. An implementation backed by a remote system therefore has to answer Role from a local cache, and that cache is the implementation's business — the hub does not keep one, does not refresh one, and must never be written to from the side. (It used to be: the hub owned the mirror and the auth provider poked at it, which is how a hub-invented org that the identity system had never heard of could exist.)

Writes are optional. A directory that does not own its data returns ErrManagedElsewhere and the handler answers 409 with ManageURL, so the hub never needs to know WHY it cannot write — only where the user should go.

type FileInfo

type FileInfo struct {
	Blob string
	Size int64
	Time time.Time
	// User/UserName are the signed-in account behind the change; Author is
	// the git/OS identity an offline device falls back to. History renders
	// the account and falls back to Author, so the viewer needs all three
	// to give the same answer — see whoChanged() in the frontend.
	User     string
	UserName string
	Author   string
	Device   string
}

FileInfo is the resolved state of one path: content identity (Blob doubles as the ETag), plus provenance where the source knows it.

type HeatEntry added in v0.4.0

type HeatEntry struct {
	Human    int64     `json:"human,omitempty"`
	Agent    int64     `json:"agent,omitempty"`
	Share    int64     `json:"share,omitempty"`
	Readers  int       `json:"readers,omitempty"` // distinct human readers
	LastRead time.Time `json:"last_read,omitzero"`
}

HeatEntry is the per-path aggregate the heat API returns. Counts only — never identities.

type HistoryEntry

type HistoryEntry struct {
	Time     string        `json:"time"`
	Kind     string        `json:"kind"` // add | edit | 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   historyDevice `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 LocalDirectory added in v0.9.0

type LocalDirectory struct{ *OrgDB }

LocalDirectory is the built-in directory: organizations owned by this hub, stored in its own metadata store. This is what every self-hosted install runs, and its behavior is exactly OrgDB's — the type exists to add the one thing an org store has no opinion about, which is where to send a browser to administer an org.

func (LocalDirectory) ManageURL added in v0.9.0

func (LocalDirectory) ManageURL(orgID string) string

ManageURL is the hub's own org page (a route in the frontend).

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.

func (*Mailer) Send

func (m *Mailer) Send(to, subject, body string) error

type MetaStore added in v0.3.0

type MetaStore interface {
	Accounts() AccountRepo
	Projects() ProjectRepo
	Orgs() OrgRepo
	Shares() ShareRepo
	Devices() DeviceRepo
	Reads() ReadRepo
	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

func OpenFileStore(dir string) (MetaStore, error)

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

func OpenSQLStore(driver, dsn string) (MetaStore, error)

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"`
	// Same three-field "who" shape as HistoryEntry (history.go), so the
	// frontend has one attribution helper for every surface.
	User     string  `json:"user,omitempty"`
	UserName string  `json:"user_name,omitempty"`
	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 NewOrgDB added in v0.3.0

func NewOrgDB(repo OrgRepo) (*OrgDB, error)

NewOrgDB builds the registry over a repo, loading orgs and invites.

func OpenOrgDB added in v0.3.0

func OpenOrgDB(path string) (*OrgDB, error)

OpenOrgDB loads the file-backed registry at path.

func (*OrgDB) AddMember added in v0.3.0

func (db *OrgDB) AddMember(orgID, email, role string) error

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) Create added in v0.3.0

func (db *OrgDB) Create(name, ownerEmail string) (Org, error)

Create makes a new org owned by ownerEmail.

func (*OrgDB) CreateInvite added in v0.3.0

func (db *OrgDB) CreateInvite(orgID, creator string, ttl time.Duration) (OrgInvite, error)

CreateInvite mints a join link for the org.

func (*OrgDB) Get added in v0.3.0

func (db *OrgDB) Get(id string) (Org, bool)

func (*OrgDB) ListInvites added in v0.3.0

func (db *OrgDB) ListInvites(orgID string) []OrgInvite

ListInvites returns the org's live (non-expired) invites.

func (*OrgDB) OrgsFor added in v0.3.0

func (db *OrgDB) OrgsFor(email string) []Org

OrgsFor returns the orgs the account belongs to, sorted by name.

func (*OrgDB) RecordInviteUse added in v0.3.0

func (db *OrgDB) RecordInviteUse(token string)

RecordInviteUse bumps the join counter for an invite (best effort).

func (*OrgDB) Redeem added in v0.3.0

func (db *OrgDB) Redeem(token string) (OrgInvite, bool)

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

func (db *OrgDB) RemoveMember(orgID, email string) error

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) Rename added in v0.3.0

func (db *OrgDB) Rename(orgID, name string) error

Rename changes the org's display name.

func (*OrgDB) RevokeInvite added in v0.3.0

func (db *OrgDB) RevokeInvite(token string) bool

RevokeInvite deletes an invite so its link stops working immediately.

func (*OrgDB) Role added in v0.3.0

func (db *OrgDB) Role(orgID, email string) string

Role returns the account's role in the org, or "" for non-members.

func (*OrgDB) SetRole added in v0.3.0

func (db *OrgDB) SetRole(orgID, email, role string) error

SetRole changes an account's role. Demoting the last owner is refused.

func (*OrgDB) ValidInvite added in v0.3.0

func (db *OrgDB) ValidInvite(token string) bool

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 OrgRepo added in v0.3.0

type OrgRepo interface {
	Load() (orgs []Org, invites []OrgInvite, err error)
	PutOrg(o Org) error
	DeleteOrg(id string) error
	PutInvite(i OrgInvite) error
	DeleteInvite(token string) error
}

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"`
	Description string    `json:"description,omitempty"` // optional one-line subtitle
	Icon        string    `json:"icon,omitempty"`        // optional lucide icon name
	// Creator is the account that first created the project; it gets an
	// explicit admin grant at creation. Empty on projects that predate
	// per-project permissions — those are governed by org owners.
	Creator string `json:"creator,omitempty"`
	// Template is the starting structure the project was created from
	// (internal/templates), empty for an empty project. Set once, at
	// creation, by whoever seeded it — it is what stops a second surface
	// seeding a second copy.
	Template string `json:"template,omitempty"`
	// Default is the level every org member gets without an explicit grant.
	// Empty means write: the historical behavior, so no row needs migrating.
	Default string `json:"default,omitempty"`
	// Perms are the explicit grants, lowercase email → level.
	Perms map[string]string `json:"perms,omitempty"`
}

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

func OpenProjectDB(path string) (*ProjectDB, error)

OpenProjectDB loads the file-backed registry at path (a missing file is an empty registry) — the zero-dependency default.

func (*ProjectDB) ClearPerm added in v0.11.0

func (db *ProjectDB) ClearPerm(id, email string) error

ClearPerm drops an explicit grant, reverting the account to the default.

func (*ProjectDB) Delete added in v0.3.0

func (db *ProjectDB) Delete(id string) error

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) Get

func (db *ProjectDB) Get(id string) (Project, bool)

func (*ProjectDB) GetOrCreate

func (db *ProjectDB) GetOrCreate(name, org string) (Project, bool, error)

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".

func (*ProjectDB) List

func (db *ProjectDB) List() []Project

func (*ProjectDB) Rename added in v0.3.0

func (db *ProjectDB) Rename(id, name string) error

Rename changes a project's display name (its id and storage are permanent).

func (*ProjectDB) SetCreator added in v0.11.0

func (db *ProjectDB) SetCreator(id, email string) error

SetCreator records who created a project (and is its first admin).

func (*ProjectDB) SetDefault added in v0.11.0

func (db *ProjectDB) SetDefault(id, level string) error

SetDefault sets the level org members get without an explicit grant.

func (*ProjectDB) SetOrg added in v0.3.0

func (db *ProjectDB) SetOrg(id, org string) error

SetOrg moves a project into an org (used by the startup migration).

func (*ProjectDB) SetPerm added in v0.11.0

func (db *ProjectDB) SetPerm(id, email, level string) error

SetPerm grants one account an explicit level on the project. Demoting the last explicit admin is refused, the same shape as OrgDB's last-owner rule: a project must keep someone who can administer it (org owners aside, who are implicitly admin and never appear in this list).

func (*ProjectDB) SetTemplate added in v0.14.0

func (db *ProjectDB) SetTemplate(id, name string) error

SetTemplate records the starting structure a project was seeded from.

func (*ProjectDB) Update added in v0.11.0

func (db *ProjectDB) Update(id string, name, description, icon *string) error

Update changes a project's editable metadata. Each field is a pointer so that "absent" (nil, leave alone) is distinguishable from "present and empty" (clear it) — the whole point of a partial update. One lock, one repo write, whatever the caller changed.

type ProjectRepo added in v0.3.0

type ProjectRepo interface {
	Load() ([]Project, error)
	Put(p Project) error
	Delete(id string) error
}

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 ReadLedger added in v0.4.0

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

ReadLedger is the in-memory read-telemetry service over a ReadRepo, in the mold of DeviceRegistry: reads stay in memory, writes are throttled. There is no background goroutine — flushes piggyback on Record calls, and telemetry failures never surface to the request that triggered them.

func NewReadLedger added in v0.4.0

func NewReadLedger(repo ReadRepo, retentionDays int) (*ReadLedger, error)

NewReadLedger loads the ledger and immediately folds buckets older than the retention horizon into their all-time rows. retentionDays <= 0 means the default.

func OpenReadLedger added in v0.4.0

func OpenReadLedger(path string, retentionDays int) (*ReadLedger, error)

OpenReadLedger loads the file-backed ledger at path.

func (*ReadLedger) AgentHeat added in v0.4.0

func (l *ReadLedger) AgentHeat(project string, since time.Time) map[string]map[string]int64

AgentHeat aggregates agent reads per device per top-level folder ("" for root files) — the coverage-matrix data. Agent buckets only, by design: agent actors are device ids, which history already exposes; human actors (emails) must never leave the server, so human/share buckets are not consulted at all.

func (*ReadLedger) Close added in v0.4.0

func (l *ReadLedger) Close() error

Close flushes any pending buckets.

func (*ReadLedger) Heat added in v0.4.0

func (l *ReadLedger) Heat(project, prefix string, since time.Time) map[string]HeatEntry

Heat aggregates reads per path for one project. since bounds the window (zero = all time, including retention folds); prefix "" means the whole project, otherwise paths under "<prefix>/".

func (*ReadLedger) Record added in v0.4.0

func (l *ReadLedger) Record(project, path, kind, actor string)

Record counts one read. Nil-safe and never fails: telemetry must not break the page view (or sync cycle) that triggered it.

type ReadRepo added in v0.4.0

type ReadRepo interface {
	Load() ([]ReadStat, error)
	PutBatch(stats []ReadStat) error // upsert by (project, path, day, kind, actor)
	DeleteBatch(keys []ReadStatKey) error
}

ReadRepo persists read-telemetry buckets (see ReadStat). Unlike the other repos it is batch-oriented: reads are telemetry, and the ledger flushes many dirty buckets at once — one file rewrite / one SQL transaction per flush, not one write per bucket.

type ReadStat added in v0.4.0

type ReadStat struct {
	Project string    `json:"project"`
	Path    string    `json:"path"`
	Day     string    `json:"day"` // "2006-01-02" UTC, or "" for all-time
	Kind    string    `json:"kind"`
	Actor   string    `json:"actor"`
	Count   int64     `json:"count"`
	Last    time.Time `json:"last"`
}

ReadStat is one aggregation bucket: reads of one path by one actor on one day. Day == "" is the all-time fold that survives retention.

type ReadStatKey added in v0.4.0

type ReadStatKey struct {
	Project, Path, Day, Kind, Actor string
}

ReadStatKey identifies one bucket.

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

func (r *RemoteSource) Commit(ctx context.Context, p, blob string, size int64, who User, note string) error

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) Files

func (r *RemoteSource) Files(ctx context.Context) (map[string]FileInfo, error)

func (*RemoteSource) HasBlob

func (r *RemoteSource) HasBlob(ctx context.Context, blob string) (bool, error)

func (*RemoteSource) Open

func (r *RemoteSource) Open(ctx context.Context, _ string, fi FileInfo) (io.ReadCloser, error)

func (*RemoteSource) Remove added in v0.14.0

func (r *RemoteSource) Remove(ctx context.Context, p string, who User, note string) error

Remove appends a delete op for p to this server's own journal. A delete references no content, so there is no blob to push first.

func (*RemoteSource) SignBlobPut

func (r *RemoteSource) SignBlobPut(ctx context.Context, blob string, size int64, ttl time.Duration) (*remote.SignedPut, error)

SignBlobPut presigns a direct upload of the blob, if the backend can sign.

func (*RemoteSource) Upload

func (r *RemoteSource) Upload(ctx context.Context, p string, src io.Reader, _ int64, who User) error

Upload stores content through the server: spool to disk while hashing, push the blob, then journal the op.

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, when set, enables public share links (/s/<token>).
	Shares *ShareDB
	// Reads, when set, aggregates read telemetry (viewer, share, and agent
	// reads) for the heat API. Nil means read tracking is off.
	Reads *ReadLedger
	// Dir, when set, walls projects off by organization membership and owns
	// every org read and write the hub performs. LocalDirectory is the
	// built-in implementation; a managed deployment supplies its own so that
	// orgs come from the same place identities do. Nil means single-volume
	// mode: no orgs, every authenticated request passes.
	Dir Directory
	// Quota, when set, enforces plan limits (managed deployments). Nil
	// means UnlimitedQuota: the open-source server never says no.
	Quota QuotaProvider
	// Billing, when set, surfaces a billing entry in the frontend's account
	// menu: the billing page URL plus the signed-in user's current plan name
	// (/api/config `billing`). The OSS hub has no billing; managed
	// deployments plug this in. Nil — or ok=false for a user with no org —
	// hides the entry. The mirror of the Quota seam: Quota enforces the
	// plan, Billing displays it.
	Billing func(email string) (plan, url string, ok bool)
	// Analytics, when its Key is set, tells the frontend to load PostHog
	// (/api/config `analytics`). The third managed-deployment seam beside
	// Quota and Billing, and deliberately server-supplied rather than
	// bundled: with no key the OSS frontend ships no analytics code and
	// makes no third-party request, so a self-hosted hub cannot phone home
	// even by accident.
	Analytics AnalyticsConfig
	// ShareRPM is the per-IP request rate on public share links (/s/*);
	// 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.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the HTTP handler: /api/* plus the embedded frontend.

type Share

type Share struct {
	Token   string    `json:"token"`
	Project string    `json:"project"`
	Path    string    `json:"path"`
	Creator string    `json:"creator,omitempty"` // account email
	Created time.Time `json:"created"`
	Expires time.Time `json:"expires,omitzero"` // zero = permanent until revoked
}

Share is one public link.

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

func NewShareDB(repo ShareRepo) (*ShareDB, error)

NewShareDB builds the registry over a repo, loading its contents.

func OpenShareDB

func OpenShareDB(path string) (*ShareDB, error)

OpenShareDB loads the file-backed registry at path.

func (*ShareDB) Create

func (db *ShareDB) Create(project, p, creator string, ttl time.Duration) (Share, error)

Create returns a share for (project, path), reusing an existing live one so repeated shares of the same file hand out the same URL.

func (*ShareDB) Get

func (db *ShareDB) Get(token string) (Share, bool)

Get resolves a live (non-expired) share.

func (*ShareDB) List

func (db *ShareDB) List(project string) []Share

List returns a project's live shares, newest first. The order has to be a total one: byToken is a map, so without a sort every call reshuffles the rows — and these rows carry a Revoke button, so "the second one" must mean the same link on every load.

func (*ShareDB) Revoke

func (db *ShareDB) Revoke(token string) bool

func (*ShareDB) SetExpiry added in v0.12.0

func (db *ShareDB) SetExpiry(token string, ttl time.Duration) (Share, bool, error)

SetExpiry re-dates a live share in place: ttl > 0 sets the expiry, ttl == 0 makes it permanent again. The token is untouched, so a URL already on someone's clipboard keeps working — which is the point, and why this is not revoke-and-remint.

type ShareRepo added in v0.3.0

type ShareRepo interface {
	Load() ([]Share, error)
	Put(s Share) error
	Delete(token string) error
}

type SignupPolicy added in v0.9.0

type SignupPolicy struct {
	RequireVerification bool     `json:"require_verification"`
	RequireApproval     bool     `json:"require_approval"`
	AllowSignup         bool     `json:"allow_signup"`
	AllowedDomains      []string `json:"allowed_domains"` // read-only
	Admins              []string `json:"admins"`          // read-only
	Mailer              bool     `json:"mailer"`          // SMTP configured?
}

SignupPolicy is what /api/admin/policy reports: which gates are on, and which of them are server-config owned (read-only to a browser session, so that no one can widen access by clicking).

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.

type Uploader

type Uploader interface {
	Upload(ctx context.Context, path string, r io.Reader, size int64, who User) error
}

Uploader is implemented by sources that accept writes through the server. who is the signed-in account the write should be attributed to (zero when auth is off).

type User

type User struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
	Admin bool   `json:"admin,omitempty"` // hub admin (approve users, govern shares)
}

User is an authenticated account as the rest of the server sees it.

Jump to

Keyboard shortcuts

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