Help us shape the future of Go! Take the 2026 Go Developer Survey

model

package
v0.54.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AppPluginSourceUpload = "upload"
	AppPluginSourceURL    = "url"
	AppPluginSourceGitHub = "github"
	AppPluginSourceBundle = "bundle"
)

App plugin sources — where the module came from (migration 00042).

View Source
const (
	AppPluginJobPending   = "pending"
	AppPluginJobRunning   = "running"
	AppPluginJobOK        = "ok"
	AppPluginJobFailed    = "failed"
	AppPluginJobCancelled = "cancelled"
)

App plugin job statuses. They mirror the ops row the job rides on; the job row is where the plugin-specific facts live (which plugin/action, the parameters, the outputs).

View Source
const (
	AppPluginScheduleDue     = "due"
	AppPluginScheduleRunning = "running"
	// AppPluginScheduleQueued is the terminal state of a piece of WORK: the
	// ops job exists and owns the outcome from here. The schedule's promise
	// ends at "handed to the queue at the right minute".
	AppPluginScheduleQueued = "queued"
	AppPluginScheduleFailed = "failed"
	// AppPluginScheduleSkipped is a row that came due while the app could
	// not run it — stopped, uninstalled, or the schedule permission
	// withdrawn. Not an error and not retried: the next wake-up asks again.
	AppPluginScheduleSkipped = "skipped"
)

App-plugin schedule statuses (migration 00050). `status` doubles as the lease two filex processes claim a due row through: only the UPDATE that moved a row from due to running may run it.

View Source
const (
	// AuthOriginEnvironment: built from FILEX_AUTH_DRIVERS / the config file.
	// The row carries only its bindings; its configuration is the
	// environment's and is never stored.
	AuthOriginEnvironment = "environment"
	// AuthOriginPage: the platform operator made it on Admin → Identity
	// providers (or through the API / MCP).
	AuthOriginPage = "page"
	// AuthOriginTenant: a tenant's administrator made it for their own tenant
	// (OwnerProviderID). It is bound to that tenant only, ever.
	AuthOriginTenant = "tenant"
)

Where a sign-in provider instance came from (auth_instances.origin, migration 00076, docs/TENANT-ADMIN.md).

View Source
const (
	AuthBindExplicit = "explicit"
	// AuthBindUpgrade: the one-time boot step bound every instance that existed
	// to every tenant that existed, so the upgrade changed nobody's sign-in.
	AuthBindUpgrade = "upgrade"
	// AuthBindPin: a 0.50 `provider` pin (FILEX_LDAP_PROVIDER,
	// FILEX_HEADER_PROVIDER), read as an implicit binding.
	AuthBindPin = "pin"
)

Why a tenant is bound to an instance (provider_auth_instances.source).

View Source
const (
	// E2EPolicyOff: nobody encrypts anything new, administrators included.
	// Folders that are encrypted already keep working.
	E2EPolicyOff = "off"
	// E2EPolicyAdmins: administrators only.
	E2EPolicyAdmins = "admins"
	// E2EPolicyPermitted: whoever holds files.encrypt on the path — what
	// everybody who could write could do before 00080. The default.
	E2EPolicyPermitted = "permitted"
	// E2EPolicyApproval: whoever holds files.encrypt, once an administrator
	// approved it for that person and that folder.
	E2EPolicyApproval = "approval"
)

The tenant's policies (providers.e2e_policy, the e2e.policy setting).

View Source
const (
	E2ERequestPending  = "pending"
	E2ERequestApproved = "approved"
	E2ERequestRejected = "rejected"
	E2ERequestExpired  = "expired"
	E2ERequestUsed     = "used"

	// E2ERequestFolder: encrypt the folder at Path itself, where it is (a
	// folder that holds something: what is in it is encrypted in place).
	E2ERequestFolder = "folder"
	// E2ERequestNewFolder: make one new encrypted folder directly inside the
	// folder at Path (operator decision 2026-10-03: a separate kind, so an
	// approval for a new folder cannot be spent on encrypting one that is
	// there already).
	E2ERequestNewFolder = "new_folder"
	// E2ERequestFile: one new single encrypted file (`.fxe`) in the folder
	// at Path.
	E2ERequestFile = "file"
)

Encryption request states and kinds (e2e_requests columns).

View Source
const (
	GrantViewer = "viewer"
	GrantEditor = "editor"
	GrantOwner  = "owner"
)

Grant levels for a per-file/per-folder ACL entry (file_grants table). Ordered by privilege: viewer < editor < owner.

  • GrantViewer — view + download only (no mutation, no convert/edit).
  • GrantEditor — read + write + rename/move/delete + convert.
  • GrantOwner — everything an editor can do PLUS manage the item's permission set (grant/revoke to other users on that item & its subtree).
View Source
const (
	GroupSourceManual = "manual"
	GroupSourceSSO    = "sso"
	GroupSourceLDAP   = "ldap"
)

Group membership sources (user_group_members.source, migration 00074). A person is in a group because an administrator put them there, or because an outside directory the group is linked to says so. Each link kind owns its own rows: a sign-in through SSO replaces the "sso" rows and never touches a "manual" one.

View Source
const (
	// LoginThrottleAccount counts wrong attempts against one normalized
	// identifier — whether or not an account by that name exists.
	LoginThrottleAccount = "account"
	// LoginThrottleIP counts wrong attempts from one client address.
	LoginThrottleIP = "ip"
)

Login throttle scopes: what a counter row counts.

View Source
const (
	PermAllow = "allow"
	PermDeny  = "deny"
)

Permission effects, as stored in a per-user override or a rule. A permission missing from the map is "inherit": the layer below decides.

View Source
const (
	// PluginKindBinary is a program filex launches and supervises from
	// <data-dir>/plugins/<name>/.
	PluginKindBinary = "binary"
	// PluginKindRemote is a service the operator runs; filex only connects.
	PluginKindRemote = "remote"
)

Plugin kinds.

View Source
const (
	PluginRequestKindApp     = "app"
	PluginRequestKindStorage = "storage"

	PluginRequestOpInstall = "install"
	PluginRequestOpUpgrade = "upgrade"

	PluginRequestPending    = "pending"
	PluginRequestApproved   = "approved"
	PluginRequestRejected   = "rejected"
	PluginRequestExpired    = "expired"
	PluginRequestSuperseded = "superseded"
)

Plugin request kinds, operations and states (plugin_requests columns).

View Source
const (
	AuthTypeOIDC  = "oidc"
	AuthTypeLocal = "local"
)

Auth types a provider (tenant) can use to authenticate its users.

View Source
const (
	// DomainPending: added, its CNAME not seen yet. It does not route.
	DomainPending = "pending"
	// DomainActive: its CNAME points at the tenant's platform subdomain. It
	// routes to the tenant and is certified.
	DomainActive = "active"
	// DomainSuspended: it was active and a later check found the CNAME gone.
	// It stops routing and stops being certified; the row stays, and the next
	// check that finds the CNAME back makes it active again.
	DomainSuspended = "suspended"
)

The states of a tenant's own domain (provider_domains.status, migration 00076, docs/TENANT-ADMIN.md).

View Source
const (
	// DomainWhyNoRecord: the name does not resolve at all. Params: domain.
	DomainWhyNoRecord = "no_record"
	// DomainWhyNoCNAME: the name has address records of its own, no CNAME.
	// Params: domain.
	DomainWhyNoCNAME = "no_cname"
	// DomainWhyPointsElsewhere: its CNAME leads somewhere else. Params:
	// found, target.
	DomainWhyPointsElsewhere = "points_elsewhere"
	// DomainWhyWildcardCNAME: the platform's own record for the tenant domain
	// is a CNAME, so nothing can be proven. Params: target, found,
	// tenant_domain.
	DomainWhyWildcardCNAME = "wildcard_cname"
	// DomainWhyDNSFailed: the DNS could not be asked (a timeout, a server
	// failure); nothing was decided. Params: detail.
	DomainWhyDNSFailed = "dns_failed"
)

Why a check did not find a domain pointed at its tenant (ProviderDomain.LastErrorCode).

View Source
const (
	ReplicaModeMirror     = "mirror"
	ReplicaModeAppendOnly = "append_only"
	ReplicaModeSkip       = "skip"
)

ReplicaMode controls how replica writes/deletes are handled per path pattern. Default-on rule is ModeMirror (owner decision E2 / SPEC §4.4).

View Source
const (
	// ReplicaCopyPending: queued, nothing walked yet.
	ReplicaCopyPending = "pending"
	// ReplicaCopyCounting: walking the storage to count its files.
	ReplicaCopyCounting = "counting"
	// ReplicaCopyCopying: walking it again, copying what the target lacks.
	ReplicaCopyCopying = "copying"
	// ReplicaCopyWaiting: the target stopped answering; the copy resumes
	// where it stopped on its own (LastError says why it waits).
	ReplicaCopyWaiting = "waiting"
	// ReplicaCopyDone: every file was looked at once.
	ReplicaCopyDone = "done"
)

Phases of a storage's initial copy (ReplicaInitialCopy.Phase).

View Source
const (
	ShareKindDownload = "download"
	ShareKindDrop     = "drop"
)

Share kinds. A "download" share grants outbound read access to a node (the classic /s/{token} link). A "drop" share is the inverse: a public upload link that lets anonymous visitors write files INTO a folder without ever seeing its contents (the /d/{token} file-drop link).

View Source
const (
	// ShareStateActive: the link opens.
	ShareStateActive = "active"
	// ShareStateExpired: its time is up.
	ShareStateExpired = "expired"
	// ShareStateExhausted: it was used as often as it was allowed to be -
	// the download (or visit) cap of a link, the upload cap of a file
	// request.
	ShareStateExhausted = "exhausted"
	// ShareStateRevoked: a person (or the app that opened it) ended it.
	ShareStateRevoked = "revoked"
)

Share states, as every list of links names them (Share.StateAt).

View Source
const (
	// StagedUploadStaging — chunks are being received.
	StagedUploadStaging = "staging"
	// StagedUploadCommitting — the client committed; the background op is
	// streaming staging → driver.
	StagedUploadCommitting = "committing"
	// StagedUploadFailed — the transfer failed. The staging directory is KEPT
	// so the transfer can be retried without re-uploading a byte.
	StagedUploadFailed = "failed"
)

Staged upload states.

A successful commit DELETES the row (the bytes are on the driver and the staging directory is gone), so there is deliberately no "stored" state here — the node's TransferState carries that fact instead.

View Source
const (
	// TransferStateStaged — the node exists and is listed, but its bytes are
	// still in filex's staging area.
	TransferStateStaged = "staged"
	// TransferStateStored — the bytes are on the storage driver. Every node
	// written before staged uploads existed is, by definition, stored.
	TransferStateStored = "stored"
	// TransferStateFailed — the transfer to the storage driver failed. The
	// bytes are still in staging (the transfer is retryable) but they are NOT
	// on the backend, and the node must say so: a node left at "staged"
	// forever is indistinguishable from one still in flight, which is how a
	// dead upload passed for a healthy one in issue #16.
	TransferStateFailed = "failed"
)

Node transfer states (nodes.transfer_state).

View Source
const (
	TagPersonal = "personal"
	TagTeam     = "team"
)

Tag kinds (migration 00055). See docs/SEARCH.md → "Tags" for the product rules; the short form is that a PERSONAL tag is one person's own label, like a star, and a TEAM tag is shared with everyone in the tenant who can see the file.

⚠ Before v0.43.0 there was one kind and it was shared by EVERY user on the instance, across tenants, although the code called it per-user (tester, 2026-09-22: a non-admin's "müşteri teklifi" showed in another user's and the admin's sidebar, and the other user could remove it). The upgrade turns those tags into TEAM tags, so nothing anybody could see disappears.

View Source
const (
	RoleAdmin = "admin"
	RoleUser  = "user"
	// RoleViewer is a read-only account: it may only ever hold viewer-level
	// item grants and can view/download but never mutate (convert/edit/upload/
	// delete). Added with the RBAC/ACL feature (migration 00012).
	RoleViewer = "viewer"
)

Role names — also stored in DB roles table.

View Source
const (
	// AuthSourceLocal: made here — on the Users page, by an invitation, at
	// first run.
	AuthSourceLocal = "local"
	// AuthSourceSSO: an OpenID Connect sign-in made it.
	AuthSourceSSO = "sso"
	// AuthSourceLDAP: an LDAP / Active Directory sign-in made it.
	AuthSourceLDAP = "ldap"
	// AuthSourceProxy: a trusted reverse proxy's headers made it.
	AuthSourceProxy = "proxy"
)

Where an account comes from (users.auth_source, migration 00084).

View Source
const (
	// TokenKindUser — a person's own credential. Acts as that person and hides
	// nothing: they may list and revoke their own tokens, and the explorer
	// draws Recent / Starred / Shared with me / API keys as usual.
	TokenKindUser = "user"
	// TokenKindApp — an integration. Identity-bearing surfaces are suppressed
	// because there is no single person behind the call: in the embeds we run,
	// ONE shared token injected by the host's proxy serves every visitor, so
	// "your API keys" would mean the proxy's own credential and "your Recent"
	// would mean the token owner's history shown to a stranger.
	TokenKindApp = "app"
)

Token kinds. A token declares what it is, because "who is calling" and "is there a person behind this call" are different questions and filex only ever answered the first one.

View Source
const AppPluginScheduleWakeupKey = ""

AppPluginScheduleWakeupKey is the reserved key of the hourly wake-up row. An app's own keys are 1..64 characters (see wasmplugin.ValidScheduleKey), so nothing an app returns can ever land on this row.

View Source
const DefaultProviderSlug = "default"

DefaultProviderSlug is the always-present "original org" tenant. It is created by migration 00014 and is inert while multi-tenant mode is off. On a multi-tenant install it doubles as the platform supertenant by default (see docs/MULTI-TENANCY.md); operators may add further providers as tenants.

View Source
const DisabledByDirectory = "directory"

DisabledByDirectory is the DisabledReason of an account LDAP directory sync switched off — its person switched off in the directory, or no longer listed there (sync_disable_missing). Sync switches it back on when the directory lets the person back in.

View Source
const DisabledPendingApproval = "pending_approval"

DisabledPendingApproval is the DisabledReason of an account opened, switched off, by an SSO sign-in whose identity provider did not say the e-mail address was verified: an administrator switches it on to approve it.

View Source
const GroupDirectoryRemoved = "removed"

GroupDirectoryRemoved is a synced group whose directory group is gone.

View Source
const GroupLinkLDAP = "ldap"

GroupLinkLDAP links a group to an LDAP / Active Directory group, named by its distinguished name (cn=finance,ou=groups,dc=example,dc=com) or its common name alone (finance). Compared without case, and a DN without the spaces around its commas and equals signs (group.LDAPValue).

View Source
const GroupLinkSSO = "sso"

GroupLinkSSO links a group to a value of the SSO provider's role claim (the same claim the admin mapping and a role's starting-role targets read). Compared exactly.

View Source
const MainDirectory = "ldap"

MainDirectory is the provider name of the first LDAP directory.

View Source
const PermTargetSSOGroup = "sso_group"

PermTargetSSOGroup is the one rule target kind: it makes the role the STARTING role of a new account whose first SSO sign-in carries this group. Compared exactly. A person's role lives in user_custom_roles, one each.

View Source
const RecentSearchKeep = 20

RecentSearchKeep is how many searches a person's list keeps per surface; writing one more drops the oldest.

View Source
const RecentSearchMaxLen = 200

RecentSearchMaxLen is the longest query kept, in characters. A longer one is refused rather than cut: a cut query is not what was searched for.

View Source
const RecentSearchSurfaceAdmin = "admin"

RecentSearchSurfaceAdmin is the admin panel's search (docs/ADMIN-PANEL.md → Search).

View Source
const SettingE2EPolicy = "e2e.policy"

SettingE2EPolicy holds a single-tenant install's policy. A multi-tenant install reads each tenant's providers.e2e_policy instead.

View Source
const SettingPermissionAppDefaults = "permissions.app_defaults"

SettingPermissionAppDefaults holds the built-in roles' decisions about app permissions (perm/app.go): {"user": {"app.sign.request": "deny"}, …}.

View Source
const SettingPermissionCatalogue = "permissions.catalogue"

SettingPermissionCatalogue is the settings key holding the permission catalogue this install last started with: a JSON array of keys, written by perm.UpgradeCatalogue. Absent means v0.49.0's, the release before it was recorded.

View Source
const SettingPermissionDefaults = "permissions.defaults"

SettingPermissionDefaults is the settings key holding the permissions a role=user account starts from: a JSON array of permission keys. Absent means the Standard preset (perm.PresetStandard), which is everything a user could do before permissions existed.

View Source
const SettingPermissionGapsDismissed = "permissions.gaps_dismissed"

SettingPermissionGapsDismissed holds the permission gaps (perm/gaps.go) an administrator said were on purpose: a JSON array of gap ids ("builtin:user:files.encrypt", "role:12:files.encrypt"). A gap that closes leaves the list; one that opens again is shown again.

View Source
const SettingPermissionGapsNotified = "permissions.gaps_notified"

SettingPermissionGapsNotified holds the permission gaps administrators were last told of in the bell: a JSON array of gap ids. A gap not in it is new.

View Source
const SettingPermissionViewerDefaults = "permissions.viewer_defaults"

SettingPermissionViewerDefaults is the same for the built-in Viewer role: a JSON array of permission keys, capped at what a viewer can ever hold. Absent means the Read-only preset (perm.ReadOnly) — what a viewer could do before roles were editable.

View Source
const TimezoneUnset = ""

TimezoneUnset is the account time zone of somebody who has not chosen one.

⚠⚠ It is the EMPTY string, and that is not a placeholder for "UTC". A timestamp is an instant; the reader picks the clock (packages/core lib/timezone). An empty zone tells every client "use the clock of the device this person is looking through" — the owner's rule, verbatim: "ben 03'te video yükledim, GMT 0 eleman o videonun yüklenme saatini kendi zaman diliminde görecek."

Every account used to be created with the literal "UTC" (six call sites, and the column default), so a person who had never opened the setting could not be told apart from one who had deliberately picked UTC. The web app honours the account's zone and therefore drew every date in UTC, while the same explorer embedded on another page — which cannot see the account — drew the browser's clock. Measured 2026-09-14 on one fresh account: 11:57 PM UTC in the app beside 4:57 PM PDT in the embed, for the same file. Migration 00040 clears the rows that default wrote.

View Source
const TokenSourceDesktop = "desktop"

TokenSourceDesktop marks a token minted by the desktop app's pairing (handlers/desktop_auth.go).

View Source
const TransferLandedSkew = 2 * time.Second

TransferLandedSkew is how far the storage's clock may lag filex's when TransferLanded compares an object's modification time with the commit.

Variables

View Source
var LazyDrivers = []string{"local"}

LazyDrivers are the drivers a storage may be catalogued lazily on. The lazy catalogue lists one directory at a time and watches visited folders with fsnotify, both of which assume a local filesystem.

Functions

func LocalizedText added in v0.49.0

func LocalizedText(base string, texts map[string]string, lang string) string

LocalizedText is base as a reader of lang sees it: the translation for lang itself, then for its primary language ("pt-br" reads "pt"), then base. The tag is compared lower-cased with "_" read as "-"; a blank translation is none. The web admin's roleName (web/src/lib/roleName.ts) is its twin.

func NewStorageUID

func NewStorageUID() string

NewStorageUID mints the identifier a storage keeps for life.

A v4 uuid rather than a slug derived from the name: a slug looks like a name and invites the assumption that it follows the name, which is the whole problem this exists to solve. This one is visibly not a label.

func NormalizeTokenKind

func NormalizeTokenKind(s string) string

NormalizeTokenKind maps a stored/incoming value to a canonical kind.

⚠ Anything that is not exactly "user" is app — including the empty string. Rows written before migration 00030, and any caller that forgets the field, must land on the restricting side: the failure mode of guessing "user" is an embed visitor listing and revoking the token the embed itself runs on, while the failure mode of guessing "app" is one admin edit.

func PersonLabel

func PersonLabel(displayName, username, email string) string

IsAdmin returns true if the user has the admin role. PersonLabel is how filex names a person to anybody reading a screen: the display name, else the username, else the e-mail address — trimmed, the first one that is not empty.

⚠⚠ ONE rule, everywhere a person is shown (QA, 2026-09-21): the same administrator was "admin2" in the Owner column, "admin@local" in notifications and the signing app, and a full name on signatures — the Owner column's lookup said display → username → e-mail, the presence strip display → e-mail local part, the permissions panel display → e-mail. This is the rule; the browser's twin is personName() (packages/core/src/lib/personName.ts) and web/tests/lib/personName.test.ts holds the two to the same cases. docs/CONTRIBUTING.md → "A person is named one way".

func TransferLanded

func TransferLanded(n *Node, size int64, mtime time.Time) bool

TransferLanded reports whether an object found at an unstored node's key — transfer_state "staged" or "failed" — can be taken as the bytes that node's staged upload committed, given the object's size and modification time.

It exists for ONE situation: the upload's staging session is gone, so nothing will ever transfer the bytes again, and the only possible copy is the one on the storage. A caller must have checked that no session remains: while one does, the session is the truth, and this is not asked.

The evidence is two facts, both required:

  • the size is the committed size (n.Size is what the client committed);
  • the object is not older than the commit. An overwrite stamps backend_mtime with the commit time when it publishes the row, and a new file's row is created at the commit (db_mtime). The bytes a transfer writes land after that; the version an overwrite was REPLACING, still at the key because the new bytes never arrived, is older.

⚠ This is not a read-path fallback, and filebody must not become one: while a node is unstored its reads come from staging, and "the driver has an object of the right size" alone is exactly the silent wrong answer filebody refuses to give. This rule only settles the catalogue's state when the evidence is complete, and refuses when a time cannot be compared.

func ValidE2EPolicy added in v0.51.0

func ValidE2EPolicy(s string) bool

ValidE2EPolicy reports whether s is one of the four policies.

func ValidE2ERequestKind added in v0.51.0

func ValidE2ERequestKind(s string) bool

ValidE2ERequestKind reports whether s is one of the three request kinds.

func ValidGrantLevel

func ValidGrantLevel(s string) bool

ValidGrantLevel reports whether s is a known grant level.

func ValidPermEffect added in v0.49.0

func ValidPermEffect(s string) bool

ValidPermEffect reports whether s is a storable effect.

func ValidRole

func ValidRole(name string) bool

ValidRole reports whether name is one of the known, seeded roles. The roles table ships `admin`, `user` and `viewer` (see 00001_init.sql + 00012_rbac_acl.sql) and there is no dynamic role-creation surface, so anything else is rejected at the API boundary rather than silently writing an unresolvable role.

func ValidTagKind

func ValidTagKind(k string) bool

ValidTagKind reports whether k is one of the two kinds.

func ValidateSyncMode

func ValidateSyncMode(m SyncMode) error

ValidateSyncMode rejects a sync_mode nothing implements — a typo (which used to store and silently poll) and `push` (declared, never built).

The empty string is accepted: it means "unset", and the write path defaults it to poll, which is also the column default.

func ValidateSyncModeFor added in v0.44.2

func ValidateSyncModeFor(m SyncMode, driver string) error

ValidateSyncModeFor is ValidateSyncMode plus the one rule that depends on the driver: `lazy` is for local storages only. The admin API calls it on create and update; a row written some other way falls back to polling at runtime, with a warning (sync.Worker).

Types

type APIToken

type APIToken struct {
	ID        int64  `json:"id"`
	UserID    int64  `json:"user_id"`
	Label     string `json:"label"`
	TokenHash string `json:"-"`
	// Scopes is the comma-separated allow-list of what this token may do.
	// ⚠⚠ An EMPTY list grants NOTHING (HasScope answers false). Until
	// v0.43.0 it meant "every scope", `admin` included, and a token created
	// with nothing ticked could read the admin API; every door that issues a
	// token now refuses an empty list (400 scopes_required), and a stored
	// empty row fails closed. Do not restore the old reading — the reasoning
	// is in auth/drivers/apitoken/issue.go.
	Scopes string `json:"scopes"`
	// Usernames is the comma-separated allow-list of identities a caller may
	// act under (X-Filex-Token-User); the FIRST entry is the default. One
	// durable token often serves several consumers (work panel, PWA, a PC MCP
	// client) — the chosen username is what audit, shares and presence show.
	// Empty == only the token's label is usable (legacy behavior).
	Usernames string `json:"usernames"`
	// Kind is what this credential IS — TokenKindUser (a person's own: CLI,
	// WebDAV, SFTP, S3, `filex mount`) or TokenKindApp (an integration: a host
	// app's proxy, a bot, an MCP client). Every token acts as its owner either
	// way; kind only decides whether the identity-bearing surfaces are drawn
	// for it. See NormalizeTokenKind for why "" reads as app.
	Kind string `json:"kind"`
	// Source is which door minted the token (migration 00060): TokenSourceDesktop
	// for a desktop pairing, "" for every other. It decides which permission
	// USING the token falls under — access.desktop or access.api (package perm).
	Source string `json:"source,omitempty"`
	// Permissions is the level this token holds of each permission of the
	// catalogue (package tokenperm): the level its Scopes name, the
	// permission's default for the rest. Never stored - the token lists and
	// mint answers fill it (WithPermissions), so a screen draws a token's
	// levels without a copy of the default rule.
	Permissions map[string]tokenperm.Level `json:"permissions,omitempty"`
	LastUsedAt  *time.Time                 `json:"last_used_at,omitempty"`
	ExpiresAt   *time.Time                 `json:"expires_at,omitempty"`
	CreatedAt   time.Time                  `json:"created_at"`
}

APIToken is a long-lived bearer credential for non-interactive callers (AI agents, the work.example.com FilexClient, the MCP server). It is bound to a user so every authenticated call inherits that user's role. The plaintext value is shown only once at creation; only TokenHash (sha256 hex) is persisted.

func (*APIToken) DefaultUsername

func (t *APIToken) DefaultUsername() string

DefaultUsername is the identity used when the caller doesn't pick one: the first configured username, else the token's label.

func (*APIToken) HasScope

func (t *APIToken) HasScope(want string) bool

HasScope reports whether the token grants `want` — only when `want` is in its list.

⚠⚠ An EMPTY list grants NOTHING (fail closed). Until v0.43.0 it granted everything, admin included, and a token minted on the admin screen with nothing ticked read /api/ai/admin/users (release-candidate sweep, 2026-09-21). No door issues an empty list any more (apitoken.ParseIssued) and migration 00054 wrote every old empty list out as the explicit full one, so a row that is empty now is a mistake — and a mistake in an authorization check must fall towards "no".

func (*APIToken) IsApp

func (t *APIToken) IsApp() bool

IsApp reports whether this token is an integration rather than a person. A nil token is NOT an app: no token at all means a cookie/OIDC session, which is always a person.

func (*APIToken) PermLevel added in v0.53.0

func (t *APIToken) PermLevel(key string) tokenperm.Level

PermLevel is the level this token holds of the permission key (package tokenperm): the level its list names, else the permission's default - the level of every token minted before the permission existed. A nil token holds nothing; so does an empty list (HasScope).

func (*APIToken) ResolveUsername

func (t *APIToken) ResolveUsername(requested string) (string, bool)

ResolveUsername maps the caller's requested identity to an effective one. Empty request → the default (ok). A request matching the allow-list (or the default itself) → that name (ok). Anything else → not ok; callers must reject the request so a typo'd integration surfaces immediately instead of silently blending into the default identity.

func (*APIToken) UsernameList

func (t *APIToken) UsernameList() []string

UsernameList returns the parsed, trimmed username allow-list (may be empty).

func (*APIToken) WithPermissions added in v0.53.0

func (t *APIToken) WithPermissions() *APIToken

WithPermissions fills Permissions from Scopes and returns t.

type AppPlugin

type AppPlugin struct {
	ID int64 `json:"id"`
	// Name is the manifest's name, unique, [a-z0-9][a-z0-9_-]{0,31}. It names
	// the plugin's directory under <data-dir>/app-plugins/ and every menu key
	// (`plugin:<name>/<action>`).
	Name    string `json:"name"`
	Version string `json:"version"`
	// LabelJSON is the manifest label ({"en": …, "tr": …}) kept as-is so a
	// list can show it without parsing the whole manifest.
	LabelJSON string `json:"-"`
	// ManifestJSON is filex-app.json exactly as installed (validated).
	ManifestJSON string `json:"-"`
	// WasmPath is the module file inside the plugin's directory.
	WasmPath string `json:"-"`
	// SHA256 of the module as installed; checked again at every load. (Of
	// the manifest when there is no module: a language pack, an app that is
	// only an interface.)
	SHA256 string `json:"sha256"`
	// UISHA256 is the interface bundle's sha256 (ui.zip), checked again at
	// every load; "" when the app has no interface of its own (migration
	// 00065). Its first 16 hex digits are the interface's address.
	UISHA256 string `json:"ui_sha256,omitempty"`
	// Signature is the detached signature this version was installed with
	// ("" without one), kept so a roll-back to it can be held to the same
	// rule on an instance that only runs signed apps (migration 00066).
	Signature string `json:"-"`
	Source    string `json:"source"`
	SourceURL string `json:"source_url,omitempty"`
	Signed    bool   `json:"signed"`
	// PermissionsJSON is the grant the admin approved at install, a JSON
	// array of permission strings. It is what host functions check — never
	// the manifest's list, so an upgrade cannot widen it without a new
	// approval.
	PermissionsJSON string `json:"-"`
	Enabled         bool   `json:"enabled"`
	LastError       string `json:"last_error,omitempty"`
	// ManifestURL is where a URL install read filex-app.json (SourceURL is
	// the module's address for an app with one) — what the update check
	// re-reads. Empty for GitHub (SourceURL says repo@ref) and uploads
	// (migration 00063).
	ManifestURL string `json:"manifest_url,omitempty"`
	// AutoUpdate is filex 0.47's per-app switch for installing a newer version
	// by itself. Nothing reads it since 0.48 — nothing updates itself
	// (wasmplugin/updates.go); the column stays for older rows.
	AutoUpdate bool `json:"-"`
	// UpdateJSON is what the last update check found (wasmplugin.UpdateInfo),
	// kept so the Apps list can say it after a restart. "" = never checked.
	UpdateJSON string    `json:"-"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

AppPlugin is one installed in-process WebAssembly app plugin — the admin's intent (module, manifest as installed, the permission grant, on/off). Whether it is compiled and answering right now is runtime state the wasmplugin registry derives by loading it; only the last load failure is kept so a stopped plugin still shows something useful.

Not a storage plugin: those are model.Plugin (00029, out of process).

type AppPluginJob

type AppPluginJob struct {
	// ID is a random 32-hex token; pending_ops.dest carries it so the worker
	// can find this row from the queue row.
	ID         string `json:"id"`
	OpID       *int64 `json:"op_id,omitempty"`
	PluginID   int64  `json:"plugin_id"`
	PluginName string `json:"plugin"`
	ActionID   string `json:"action"`
	StorageID  int64  `json:"storage_id"`
	// PathsJSON is the JSON array of storage-relative input paths.
	PathsJSON string `json:"-"`
	// ParamsJSON is the JSON object of action parameters (from the form).
	ParamsJSON string `json:"-"`
	ActorID    *int64 `json:"actor_id,omitempty"`
	Locale     string `json:"locale,omitempty"`
	// Label is the action label in the actor's locale, resolved at submit so
	// the tray never has to know the manifest.
	Label   string `json:"label"`
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
	// OutputsJSON is the JSON array of {path} objects written once the
	// outputs are committed to the storage.
	OutputsJSON string     `json:"-"`
	Error       string     `json:"error,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	FinishedAt  *time.Time `json:"finished_at,omitempty"`
}

AppPluginJob is one action run.

type AppPluginLock

type AppPluginLock struct {
	StorageID  int64      `json:"storage_id"`
	PathHash   string     `json:"-"`
	Rel        string     `json:"path"`
	PluginID   int64      `json:"plugin_id"`
	PluginName string     `json:"plugin"`
	Reason     string     `json:"reason,omitempty"`
	Until      *time.Time `json:"until,omitempty"`
	CreatedBy  *int64     `json:"created_by,omitempty"`
	CreatedAt  time.Time  `json:"created_at"`
}

AppPluginLock makes one file read-only for EVERYONE — owner and administrators included — until the holding plugin lifts it or Until passes (migration 00045). A signature request is the canonical holder: the document must not change under the signers. The ACL caps every caller's effective level on Rel at viewer while a lock is live; the plugin's own jobs write through the ops worker, which does not consult the ACL, so the signing itself still lands. One lock per file; only the plugin that holds it (or an administrator through the admin API) may lift it.

func (*AppPluginLock) Live

func (l *AppPluginLock) Live(now time.Time) bool

Live reports whether the lock still holds at now (nil Until = until lifted).

type AppPluginOverride

type AppPluginOverride struct {
	PluginID    int64  `json:"-"`
	ActionID    string `json:"id"`
	Enabled     bool   `json:"enabled"`
	AdminOnly   bool   `json:"admin_only"`
	AppliesJSON string `json:"-"`
}

AppPluginOverride is the admin's change to one action of a plugin. A missing row means "as the manifest says". AppliesJSON "" keeps the manifest's rule; otherwise it is what the admin CHANGED about it (wasmplugin.appliesDelta, `"v":2`), resolved against the current manifest and engines at every read. A row without `"v"` is a whole rule written before v0.43.0; the registry rewrites those as changes at start.

type AppPluginScheduleItem

type AppPluginScheduleItem struct {
	PluginID int64  `json:"plugin_id"`
	Key      string `json:"key"`
	// DueAt is when this runs, to the second. Nothing runs before it.
	DueAt time.Time `json:"due_at"`
	// ActionID, StorageID, PathsJSON and ParamsJSON are the job to queue;
	// all four are empty on a wake-up row.
	ActionID   string `json:"action_id,omitempty"`
	StorageID  int64  `json:"storage_id,omitempty"`
	PathsJSON  string `json:"-"`
	ParamsJSON string `json:"-"`
	Status     string `json:"status"`
	// Attempts counts claims, so a row that keeps coming back says so.
	Attempts int `json:"attempts"`
	// ClaimedBy names the filex process that took the row (Registry.InstanceID).
	ClaimedBy string     `json:"claimed_by,omitempty"`
	ClaimedAt *time.Time `json:"claimed_at,omitempty"`
	// JobID is the app_plugin_jobs row this item became, once queued.
	JobID string `json:"job_id,omitempty"`
	// Error is why the last attempt did not queue work — or, on a wake-up
	// row, what the last wake-up decided.
	Error     string    `json:"error,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AppPluginScheduleItem is one row that comes due: either the hourly wake-up of an app (Key == AppPluginScheduleWakeupKey) or one piece of work that wake-up asked for. See db/migrations/sqlite/00050_app_plugin_schedule.sql.

type AppPluginSigningKey

type AppPluginSigningKey struct {
	ID          string     `json:"id"`
	TenantID    int64      `json:"tenant_id"`
	PluginID    int64      `json:"plugin_id"`
	Purpose     string     `json:"purpose"`
	Subject     string     `json:"subject"`
	CertPEM     string     `json:"cert_pem"`
	KeySealed   string     `json:"-"`
	CreatedAt   time.Time  `json:"created_at"`
	ExpiresAt   *time.Time `json:"expires_at,omitempty"`
	DestroyedAt *time.Time `json:"destroyed_at,omitempty"`
	RetiredAt   *time.Time `json:"retired_at,omitempty"`
}

AppPluginSigningKey is a key the host holds for a signing plugin (migration 00044): the tenant CA (purpose "ca") or a per-signer leaf (purpose "leaf"). KeySealed is the PKCS#8 key sealed with the instance secret; a destroyed leaf keeps its certificate and loses the key.

type AppPluginStateFile

type AppPluginStateFile struct {
	StorageID   int64  `json:"storage_id"`
	StorageName string `json:"storage"`
	Path        string `json:"path"`
	Name        string `json:"name"`
	Key         string `json:"key"`
	Value       string `json:"value"`
}

AppPluginStateFile is one file an app plugin keeps state on, with the file still there to show for it. The state table is keyed by a path hash, so the path itself comes from the node row it joins — which also means a deleted file drops out of the listing by itself.

type AppPluginVersion added in v0.48.0

type AppPluginVersion struct {
	ID              int64     `json:"id"`
	PluginID        int64     `json:"plugin_id"`
	Version         string    `json:"version"`
	ManifestJSON    string    `json:"-"`
	WasmPath        string    `json:"-"`
	SHA256          string    `json:"sha256"`
	UISHA256        string    `json:"ui_sha256,omitempty"`
	PermissionsJSON string    `json:"-"`
	Source          string    `json:"source"`
	SourceURL       string    `json:"source_url,omitempty"`
	ManifestURL     string    `json:"manifest_url,omitempty"`
	Signed          bool      `json:"signed"`
	Signature       string    `json:"-"`
	Dir             string    `json:"-"`
	ReplacedBy      *int64    `json:"replaced_by,omitempty"`
	ReplacedAt      time.Time `json:"replaced_at"`
}

AppPluginVersion is a version an upgrade replaced, kept so an administrator can go back to it without approving it again (migration 00066). Dir is its folder under <data-dir>/app-plugins/_versions/<name>/, holding its module, manifest, interface bundle and mirrored files.

type AppPluginsCapabilities

type AppPluginsCapabilities struct {
	Enabled bool `json:"enabled"`
	// Engines lists which heavy engines (ffmpeg, imagemagick, …) a plugin
	// may be handed on this host — informational for the admin panel.
	Engines map[string]bool `json:"engines,omitempty"`
}

AppPluginsCapabilities is the app-plugin block of the snapshot.

type AppThumbLimits added in v0.50.0

type AppThumbLimits struct {
	PluginID    int64     `json:"plugin_id"`
	MaxInputMB  int       `json:"max_input_mb"`
	TimeoutS    int       `json:"timeout_s"`
	MemoryMB    int       `json:"memory_mb"`
	Concurrency int       `json:"concurrency"`
	UpdatedAt   time.Time `json:"updated_at"`
}

AppThumbLimits are the administrator's limits for one app's thumbnail calls (migration 00077). A zero field is "the default".

type AuditEntry

type AuditEntry struct {
	ID         int64                  `json:"id"`
	UserID     *int64                 `json:"user_id,omitempty"`
	Action     string                 `json:"action"`
	TargetType string                 `json:"target_type,omitempty"`
	TargetID   string                 `json:"target_id,omitempty"`
	Metadata   map[string]interface{} `json:"metadata,omitempty"`
	IP         string                 `json:"ip,omitempty"`
	CreatedAt  time.Time              `json:"created_at"`
}

AuditEntry is a record in audit_log.

type AuthBinding added in v0.50.0

type AuthBinding struct {
	ProviderID int64     `json:"provider_id"`
	InstanceID int64     `json:"instance_id"`
	Source     string    `json:"source"`
	CreatedAt  time.Time `json:"created_at"`
}

AuthBinding is one row of provider_auth_instances: a tenant signs in through an instance.

type AuthInstance added in v0.50.0

type AuthInstance struct {
	ID int64 `json:"id"`
	// Slug is the stable name. The first instance of each driver keeps the
	// driver's name, so the routes that addressed "the ldap" still do.
	Slug   string `json:"slug"`
	Driver string `json:"driver"`
	// Label is what the sign-in page calls it; "" = the driver's own words.
	Label  string `json:"label"`
	Origin string `json:"origin"`
	// OwnerProviderID is the tenant that made it (Origin == tenant), else nil.
	OwnerProviderID *int64 `json:"owner_provider_id,omitempty"`
	Enabled         bool   `json:"enabled"`
	// Legacy: saved before v0.43.0 and never applied; imported switched off.
	Legacy bool `json:"legacy,omitempty"`
	// ConfigJSON holds the driver's fields: field → value, every value a
	// string as the settings rows held them, a secret SEALED. Never sent to a
	// client as it is (authsetup redacts it).
	ConfigJSON string `json:"-"`
	// Promoted lists the scopes (tenant ids; 0 = the platform's own tenant) in
	// which an operating-system instance has already promoted its test
	// account: only the first switch-on in a scope promotes.
	Promoted  []int64   `json:"promoted,omitempty"`
	CreatedBy *int64    `json:"created_by,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AuthInstance is one configured sign-in provider: a driver (oidc, ldap, pam, windows, proxy-header) with its configuration, bound to the tenants it signs people in for (AuthBinding).

func (*AuthInstance) PromotedIn added in v0.50.0

func (a *AuthInstance) PromotedIn(scope int64) bool

PromotedIn reports whether the instance already promoted a test account in a scope (0 = the platform's own tenant).

type BroadcastFilter

type BroadcastFilter struct {
	// Only admits just the broadcasts of these events. Wins over Except.
	// Only and Except narrow a per-user read and are ignored by the
	// admin-global one; ReaderID applies to both.
	Only []string
	// Except leaves out the broadcasts of these events.
	Except []string
	// ReaderID is whose read state a broadcast carries in this read
	// (migration 00056). For that reader a broadcast is read when it is at or
	// below their "mark all read" point, when they marked it on its own, or
	// when the row's own read_at was stamped before per-reader state existed.
	// 0 reads the row's own column only. A row addressed to a user always
	// carries that column.
	ReaderID int64
	// OwnOnly takes no broadcast at all, BroadcastsOnly none of the reader's
	// own rows — the two halves of a per-user read, read apart. A bell whose
	// broadcasts get a per-row pass after the store (a member's: the grants)
	// counts its own rows in SQL and walks only the broadcasts, so its total
	// is exact and its pages are cut where the reader sees them
	// (notify.Service.ListVisible). Ignored by the admin-global read.
	OwnOnly        bool
	BroadcastsOnly bool
	// Digest is the reader's notification digest (internal/notify digest.go,
	// migration 00087): which of their rows are "quiet" — held for the digest
	// rather than told one by one. Nil reads every row as it always was. Like
	// Only and Except it narrows a per-user read and is ignored by the
	// admin-global one, which is the audit.
	Digest *DigestFilter
}

BroadcastFilter decides how a notification read treats broadcasts — rows with no user_id. The zero value reads every broadcast, with the read state stored on the row.

It is applied IN SQL, like the mute list, so that a bell's page is filled with rows its reader can be shown, not with rows a filter throws away after the page was cut.

type Capabilities

type Capabilities struct {
	// Storage write operations.
	Upload  bool `json:"upload"`
	Move    bool `json:"move"`
	Copy    bool `json:"copy"`
	Delete  bool `json:"delete"`
	Mkdir   bool `json:"mkdir"`
	Presign bool `json:"presign"` // browser-direct uploads

	// Search & versioning.
	Search   bool `json:"search"`
	Versions bool `json:"versions"`

	// Thumbnail backends present in the runtime.
	Thumbs ThumbCapabilities `json:"thumbs"`

	// OCR reports whether the optional `tesseract` binary was found
	// (FILEX_TESSERACT_BIN or $PATH) — image text extraction for the
	// content search index (v0.2 "Bul").
	OCR bool `json:"ocr"`

	// Antivirus reports whether async upload scanning (v0.4 "Koru") is both
	// switched on and reachable-in-principle: the `antivirus.enabled`
	// setting is true AND either a scanner binary resolved or a clamd
	// address is configured.
	//
	// ⚠ It does not mean clamd ANSWERED. Reachability costs a network
	// round-trip and is probed where an operator is waiting for the answer
	// (GET /api/admin/protection), not on every capabilities fetch.
	Antivirus bool `json:"antivirus"`

	// AntivirusMode names HOW ClamAV is reached — "binary" (an executable on
	// filex's $PATH) or "daemon" (clamd over TCP / a unix socket); empty when
	// Antivirus is false. Two very different deployments produce the same
	// green light, and the person looking at it has to know which one
	// answered before they can debug it.
	AntivirusMode string `json:"antivirus_mode,omitempty"`

	// Per-storage capability probe — keyed by storage ID (string for JSON).
	Storage map[string]StorageCapabilities `json:"storage,omitempty"`

	// Plug-and-play external services.
	External map[string]ExternalServiceState `json:"external"`

	// Backend-effective limits.
	MaxUploadSize int64 `json:"max_upload_size"`
	ChunkSize     int64 `json:"chunk_size"`

	// Driver inventory (UI uses these to render the right login form,
	// drop-down for storage create wizard, etc.). Frontend has matching
	// defaults in stores/capabilities.ts so a missing field never
	// crashes a caller.
	AuthDrivers    []string `json:"auth_drivers"`
	StorageDrivers []string `json:"storage_drivers"`
	DBDriver       string   `json:"db_driver"`
	SearchEnabled  bool     `json:"search_enabled"`

	// OIDCAutoRedirect tells the login page to start the OIDC flow
	// immediately instead of showing the password form (SSO-first
	// installs, FILEX_OIDC_AUTO_REDIRECT). The form stays reachable via
	// ?local=1.
	OIDCAutoRedirect bool `json:"oidc_auto_redirect"`

	// AuthRecoveryLogin is true when password sign-in is off (no `local`
	// driver) but the bootstrap administrator may still use it, for recovery.
	// The login page then keeps the password form behind a recovery link
	// instead of hiding it altogether.
	AuthRecoveryLogin bool `json:"auth_recovery_login"`

	// Build metadata.
	Version string `json:"version"`
	Build   string `json:"build"`

	// Demo-mode toggles a public landing on Login.vue with an "Open
	// the demo" CTA that auto-submits the supplied creds.
	DemoMode bool   `json:"demo_mode"`
	DemoUser string `json:"demo_user,omitempty"`
	// DemoPass is the password that CTA submits (FILEX_DEMO_PASS). It is
	// emitted ONLY when DemoMode is on, where the credentials are published
	// on the landing page anyway - that is what a demo is. Without it the
	// button hardcodes "demo" and an operator who changes the password
	// breaks the button while believing they secured the instance.
	DemoPass string `json:"demo_pass,omitempty"`

	// DefaultLocale, when set (FILEX_DEFAULT_LOCALE), pins the initial UI
	// language for users who haven't picked one — overriding browser detection.
	DefaultLocale string `json:"default_locale,omitempty"`

	// AppPlugins says whether in-process WebAssembly app plugins are on
	// (internal/wasmplugin). When Enabled is false the explorer makes no
	// plugin calls at all.
	AppPlugins AppPluginsCapabilities `json:"app_plugins"`
}

Capabilities is the runtime feature snapshot returned by /api/capabilities. The frontend uses this to enable/disable UI affordances.

type CatalogueCounts added in v0.44.2

type CatalogueCounts struct {
	Uncatalogued int64 `json:"uncatalogued"`
	Catalogued   int64 `json:"catalogued"`
	Watched      int64 `json:"watched"`
	// HeldBack counts folders whose last reconcile refused a deletion.
	HeldBack int64 `json:"held_back"`
	// RootCatalogued: the storage root's listing has been applied.
	RootCatalogued bool `json:"root_catalogued"`
}

CatalogueCounts is how far a storage's lazy catalogue has come.

type CatalogueFolder added in v0.44.2

type CatalogueFolder struct {
	StorageID int64
	PathHash  string
	// Path is canonical: "/" for the storage root, "/a/b" below it.
	Path  string
	Depth int
	State CatalogueFolderState
	// ReconciledAt is when the folder's last complete listing was applied.
	ReconciledAt *time.Time
	// VisitedAt is when a person last opened the folder.
	VisitedAt *time.Time
	// WatchedAt is when its current watch was placed; nil = no watch.
	WatchedAt *time.Time
	// ReconcileOnOpen: the watch was evicted, expired or lost with the
	// process, so the catalogue may have drifted from the disk.
	ReconcileOnOpen bool
	// Entries is how many entries the last listing held; HeldBack how many
	// deletions its guard refused to make.
	Entries  int
	HeldBack int
}

CatalogueFolder is one row of catalogue_folders.

func (*CatalogueFolder) Catalogued added in v0.44.2

func (f *CatalogueFolder) Catalogued() bool

Catalogued reports whether the folder's listing has been applied at least once.

type CatalogueFolderFilter added in v0.44.2

type CatalogueFolderFilter struct {
	// State, when set, keeps rows in that state only.
	State CatalogueFolderState
	// ReconciledBefore, when set, keeps catalogued rows whose last listing is
	// older than it — oldest first — instead of the shallowest-first frontier
	// order.
	ReconciledBefore *time.Time
	// Under, when set, keeps the folder itself and every folder below it.
	Under string
	Limit int
}

CatalogueFolderFilter selects catalogue_folders rows for the lazy catalogue's work lists.

type CatalogueFolderState added in v0.44.2

type CatalogueFolderState string

CatalogueFolderState is where one folder of a lazily catalogued storage stands (migration 00059, docs/LAZY-CATALOGUE.md). A folder with no row at all is not catalogued.

const (
	// FolderUncatalogued: seen in its parent's listing, never listed itself.
	// These rows are the background filler's work list.
	FolderUncatalogued CatalogueFolderState = "uncatalogued"
	// FolderCatalogued: its listing has been applied to the catalogue.
	FolderCatalogued CatalogueFolderState = "catalogued"
	// FolderWatched: catalogued, and under an fsnotify watch in the running
	// process. Demoted to catalogued (with ReconcileOnOpen) at every start.
	FolderWatched CatalogueFolderState = "watched"
)

type ChunkedUpload

type ChunkedUpload struct {
	ID         string       `json:"id"`
	StorageID  int64        `json:"storage_id"`
	StorageKey string       `json:"storage_key"`
	UploadID   string       `json:"upload_id"`
	TotalSize  int64        `json:"total_size"`
	Parts      []UploadPart `json:"parts"`
	ExpiresAt  time.Time    `json:"expires_at"`
}

ChunkedUpload tracks an in-flight multipart upload.

type CustomTheme

type CustomTheme struct {
	ID          int64             `json:"-"`
	Key         string            `json:"key"`
	Name        string            `json:"name"`
	TokensLight map[string]string `json:"light"`
	TokensDark  map[string]string `json:"dark"`
	CreatedAt   time.Time         `json:"-"`
	UpdatedAt   time.Time         `json:"-"`
}

CustomTheme is an operator-defined palette — migration 00051.

It is the same shape as a built-in palette in packages/core/src/lib/themes.ts (`ThemeDef`): a name and two maps of `--fe-*` custom properties, one per light/dark variant. The browser is handed built-ins and custom themes in one list and cannot tell them apart beyond the `custom:` prefix on the id.

⚠ Key is the operator's slug WITHOUT the prefix (what is stored in custom_themes.theme_key). The palette id the browser persists is `custom:<Key>` — see handlers.CustomThemePrefix. Keeping the prefix out of the column means the slug pattern is the only thing the database has to agree with, and a built-in id can never be typed into it by accident.

type DigestFilter added in v0.53.0

type DigestFilter struct {
	// After is the reader's digest point (notify_digest_state.through_id):
	// every row at or below it has been told, on its own or in a digest.
	After int64
	// Quiet is the event kinds this reader holds for the digest. Empty: no row
	// is quiet.
	Quiet []string
	// Only reads the quiet rows alone, whatever their read state — what a
	// digest is made of — and UpTo, when above zero, stops at that id.
	Only bool
	UpTo int64
}

DigestFilter says which rows of a reader's bell are QUIET: a row above the reader's digest point (After) of one of the kinds they hold for the digest (Quiet). A quiet row is in the reader's list, as read, and counted by neither the unread list nor the badge — the digest that carries it is what is unread. Applied in SQL, so the badge and the list agree.

func (*DigestFilter) IsQuiet added in v0.53.0

func (f *DigestFilter) IsQuiet(n *Notification) bool

IsQuiet reports whether a row read through f is quiet for its reader — the Go twin of the SQL predicate, for a row already in hand.

type DigestPolicy added in v0.53.0

type DigestPolicy struct {
	Scope int64 `json:"-"`
	// WindowMinutes is how long the non-urgent notifications are held before
	// the digest that carries them is sent: 1-15.
	WindowMinutes int `json:"window_minutes"`
	// Urgent is the kinds that are told at once for everybody who did not
	// choose otherwise. Nil: the built-in list (notify.DefaultUrgentEvents).
	Urgent    []string  `json:"urgent_events"`
	UpdatedAt time.Time `json:"updated_at"`
}

DigestPolicy is an administrator's defaults for the notification digest, for the people of one scope: the instance (Scope 0, a single-tenant install) or one tenant (its provider id). Table notify_digest_policy.

type DomainCheck added in v0.50.0

type DomainCheck struct {
	// Text is the English sentence (ProviderDomain.LastError).
	Text   string
	Code   string
	Params map[string]string
}

DomainCheck is what one check records about a domain besides its state.

type Draft added in v0.48.0

type Draft struct {
	ID int64
	// Key is the `<draft key>` folder segment, and the draft's address in the
	// API (/api/files/drafts/{key}).
	Key       string
	UserID    int64
	StorageID int64
	NodeID    int64
	// TargetDir is the storage-relative folder the document is meant for,
	// "" for the storage root, no leading slash.
	TargetDir string
	// TargetName is the file name it is meant to have there.
	TargetName string
	// DocType is the New-document type it was made as (internal/newdoc key),
	// so a name with no extension still opens in the right editor.
	DocType   string
	CreatedAt time.Time
	UpdatedAt time.Time

	// Read from the draft file's catalogue row by the store's queries: where
	// the bytes are, how many there are, when they last changed, and whether
	// the row is live (a discarded draft's row is in the trash).
	Path     string
	Size     int64
	Mime     string
	Mtime    *time.Time
	NodeLive bool
}

Draft is a new document that has not been saved yet (issue #71, migration 00064). Its bytes are a real file in the storage's drafts area — `.filex-drafts/<UserID>/<Key>/<name>` (internal/syspath.Drafts) — with a catalogue row of its own (NodeID); this is what the file cannot say about itself: whose it is and where it is meant to go.

⚠ Not serialised as it is. The API answers with its own shape (internal/drafts.View), which never carries the internal path or the owner.

type E2ERequest added in v0.51.0

type E2ERequest struct {
	ID int64
	// ProviderID is the tenant whose administrators decide; nil on a
	// single-tenant install.
	ProviderID *int64
	// UserID is who asked; Requester their name as shown when they asked.
	UserID    int64
	Requester string
	// StorageID and Path say where: the folder P approved. What it opens is
	// Kind's: P itself, encrypted in place (E2ERequestFolder); one NEW
	// encrypted folder directly inside P (E2ERequestNewFolder); or one single
	// encrypted file in P (E2ERequestFile). Storage-relative, no leading
	// slash, "" = the root.
	StorageID int64
	Path      string
	// Kind is E2ERequestFolder, E2ERequestNewFolder or E2ERequestFile.
	Kind string
	// Reason is the requester's own words.
	Reason string
	// Status is one of the E2ERequest* states.
	Status string
	// DecidedBy/Decider/DecidedAt: who closed the request and when (an
	// expired one: nobody).
	DecidedBy *int64
	Decider   string
	DecidedAt *time.Time
	// DecisionNote is a rejection's reason.
	DecisionNote string
	// ExpiresAt: a pending request past it expires, and so does an approval
	// nobody used.
	ExpiresAt time.Time
	// UsedAt is when the approved encryption happened (E2ERequestUsed).
	UsedAt    *time.Time
	CreatedAt time.Time
	UpdatedAt time.Time
}

E2ERequest is a person's request to encrypt in one folder, left where the policy is E2EPolicyApproval, for an administrator of the tenant to approve or reject. An approval is good for ONE encryption of its kind in its folder, by that person, before ExpiresAt.

type E2ERequestFilter added in v0.51.0

type E2ERequestFilter struct {
	ProviderID *int64
	UserID     *int64
	Status     string
	// ExpiresBefore keeps what is due — an expiry at or before it — and lists
	// it oldest expiry first instead of newest first, so a sweep reading a
	// batch at a time reaches every due row, however many newer ones there
	// are.
	ExpiresBefore time.Time
	Limit         int
}

E2ERequestFilter narrows Store.ListE2ERequests. A nil pointer, "" or a zero time is no condition; Limit <= 0 (or over 500) is 500.

type ExternalServiceState

type ExternalServiceState struct {
	Enabled   bool       `json:"enabled"`
	URL       string     `json:"url,omitempty"`
	State     string     `json:"state"` // "ok", "unreachable", "unauthorized", "disabled"
	LastCheck *time.Time `json:"last_check,omitempty"`
	// Detail says what the probe saw when it did not get a healthy answer:
	// the HTTP status, a timeout, or the connection error (issue #17).
	// "unreachable" alone sent an operator to the network when the answer was
	// a 502 from the service's own web server.
	Detail string `json:"detail,omitempty"`
}

ExternalServiceState describes a plug-and-play integration's runtime status.

type FileAssociation added in v0.50.0

type FileAssociation struct {
	// Capability is "open" or "thumbnail".
	Capability string `json:"capability"`
	// Ext is the kind: a file name's extension, lower-case, no dot.
	Ext string `json:"ext"`
	// Handlers are the handler ids in the order they are asked
	// ("builtin", "app:<app>/<view>", "app:<app>").
	Handlers []string `json:"handlers"`
	// Off are the handler ids switched off for this kind.
	Off       []string  `json:"off"`
	UpdatedBy *int64    `json:"updated_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
}

FileAssociation is the administrator's rule for one kind of file and one capability (migration 00077, internal/assoc): which handlers are asked, in which order, and which are switched off. A kind with no row keeps the default order (assoc.DefaultOrder).

type FileGrant

type FileGrant struct {
	ID         int64  `json:"id"`
	StorageID  int64  `json:"storage_id"`
	PathPrefix string `json:"path_prefix"`
	IsDir      bool   `json:"is_dir"`
	UserID     int64  `json:"user_id,omitempty"`
	// GroupID is set instead of UserID on a group's grant
	// (group_file_grants, migration 00074): every member of the group holds
	// it. The two tables have their own ids — a grant is named by its id AND
	// which of the two it is.
	GroupID   int64     `json:"group_id,omitempty"`
	Level     string    `json:"level"`
	CreatedBy *int64    `json:"created_by,omitempty"`
	CreatedAt time.Time `json:"created_at"`

	// Enrichment fields (not persisted; filled by the permissions handler for
	// the UI panel). Left empty by the store scanners.
	UserEmail       string `json:"user_email,omitempty"`
	UserDisplayName string `json:"user_display_name,omitempty"`
	// UserName is the person as every screen names them (PersonLabel):
	// display name, else username, else e-mail. The panels print this; the
	// e-mail stays for the tooltip.
	UserName  string `json:"user_name,omitempty"`
	GroupName string `json:"group_name,omitempty"`
	Inherited bool   `json:"inherited,omitempty"`
}

FileGrant is one row of the per-file/per-folder ACL. It grants a single user — or, from group_file_grants, a group — a level on a path within one storage. PathPrefix is stored in confine-form (cleaned, no leading/trailing slash; "" == storage root); for a directory grant the level cascades to every descendant, for a file grant it applies to that exact path only. Grants are only consulted on storages with RBACEnabled=true. See internal/acl for resolution.

type Group added in v0.50.0

type Group struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	ProviderID  *int64 `json:"provider_id,omitempty"`
	RoleID      *int64 `json:"role_id"`
	// GivesAdmin: the group makes its members administrators — the
	// built-in Administrator role, full access — instead of giving a role
	// (migration 00086). RoleID is then nil.
	GivesAdmin bool `json:"gives_admin"`
	// Priority orders the groups a member's role can come from: the
	// highest first, then the lowest id (perm.EffectiveRole).
	Priority int         `json:"priority"`
	Links    []GroupLink `json:"links"`
	// A group directory sync brought in (migration 00084): DirectoryID is
	// the directory group's permanent id ("ldap:…"), DirectoryName the name
	// it last had there, DirectoryState "" while the directory has it and
	// GroupDirectoryRemoved once it does not. All empty for every other
	// group.
	DirectoryID    string    `json:"directory_id,omitempty"`
	DirectoryName  string    `json:"directory_name,omitempty"`
	DirectoryState string    `json:"directory_state,omitempty"`
	CreatedBy      *int64    `json:"created_by,omitempty"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

Group is one row of user_groups (migration 00074): a named set of people in one tenant, which can be given folder grants (GroupFileGrant) and a custom role (RoleID — the role of every member who has none of their own).

ProviderID scopes the group to one tenant; nil is install-wide.

func (*Group) Synced added in v0.52.0

func (g *Group) Synced() bool

Synced reports whether directory sync brought this group in and the directory still has it — its LDAP link is then the directory's.

type GroupLink struct {
	Kind  string `json:"kind"`
	Value string `json:"value"`
}

GroupLink names a group of an outside directory whose people are members of a filex group. Kind is the directory (GroupLinkSSO, GroupLinkLDAP), Value its group's name there.

type GroupMember added in v0.50.0

type GroupMember struct {
	GroupID int64     `json:"group_id"`
	UserID  int64     `json:"user_id"`
	Source  string    `json:"source"`
	AddedAt time.Time `json:"added_at"`
}

GroupMember is one row of user_group_members.

type LoginThrottle added in v0.50.0

type LoginThrottle struct {
	ID int64 `json:"id"`
	// Scope is LoginThrottleAccount or LoginThrottleIP.
	Scope string `json:"scope"`
	// Subject is the normalized identifier, or the address.
	Subject string `json:"subject"`
	// Fails is the wrong attempts inside the current window.
	Fails int `json:"fails"`
	// LockLevel is how many locks in a row this subject has earned; the next
	// lock lasts twice as long as the previous, up to a ceiling.
	LockLevel   int        `json:"lock_level"`
	WindowStart time.Time  `json:"window_start"`
	LockedUntil *time.Time `json:"locked_until,omitempty"`
	LastFailAt  time.Time  `json:"last_fail_at"`
	// LastIP is the address of the most recent wrong attempt.
	LastIP string `json:"last_ip,omitempty"`
	// LastProtocol is the door it came through: web, dav, ftp, sftp, s3.
	LastProtocol string    `json:"last_protocol,omitempty"`
	UpdatedAt    time.Time `json:"updated_at"`
}

LoginThrottle is one sign-in attempt counter (table login_throttle, migration 00072; internal/loginguard owns what the numbers mean).

func (*LoginThrottle) Locked added in v0.50.0

func (t *LoginThrottle) Locked(now time.Time) bool

Locked reports whether the lock is in force at now.

type NFSExport

type NFSExport struct {
	ID     int64 `json:"id"`
	UserID int64 `json:"user_id"`
	// APITokenID ties this export to the token it inherits from, or nil when
	// minted from the account. The FK cascades, so revoking the token revokes
	// the mount with it.
	APITokenID *int64 `json:"api_token_id,omitempty"`
	Label      string `json:"label"`
	// TokenHash is sha256 of the path secret. The plaintext is shown once, at
	// creation, and never stored — this is a bearer credential the server only
	// has to compare, unlike an S3 secret which SigV4 must recompute from.
	TokenHash string `json:"-"`
	// StorageName/Prefix confine the mount, in the same shape the S3 keys use.
	// Empty StorageName means every storage the account can see.
	StorageName string `json:"storage_name,omitempty"`
	Prefix      string `json:"prefix,omitempty"`
	// ReadOnly makes this export refuse every write regardless of the
	// account's own permissions. An NFS mount is usually consumed by a MACHINE,
	// and "this one may only read" is the commonest thing to want to say.
	ReadOnly bool `json:"read_only"`
	// AllowCIDRs is a comma-separated allow-list. Empty means any address the
	// listener itself accepts.
	AllowCIDRs string `json:"allow_cidrs,omitempty"`

	CreatedAt  time.Time  `json:"created_at"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"`
	DisabledAt *time.Time `json:"disabled_at,omitempty"`
}

NFSExport is one NFSv3 mount point, bound to one account (migration 00028).

The path is the credential

NFSv3 cannot authenticate a caller in any way filex can use: real identity means RPCSEC_GSS and therefore Kerberos, and AUTH_SYS is the client asserting its own uid with nothing to check it against. So the binding moves: the mount handshake names an export path, and that path carries 32 bytes of entropy. Whoever knows it may mount, and the mount then runs as this account — the same shape as a share link.

⚠ Consequences worth saying out loud rather than discovering:

  • the path is a SECRET, so it belongs in a config file with the same care as a password, and a mount table is a place secrets end up readable;
  • NFSv3 is unencrypted, so this is a LAN or VPN protocol. `filex mount` (FUSE over HTTPS) is the answer for anything else;
  • revoking is deleting the row, and it takes effect on the next request rather than tearing down a live mount.

func (*NFSExport) Usable

func (e *NFSExport) Usable(now time.Time) bool

Usable reports whether this export may be mounted right now.

type NameMatch

type NameMatch struct {
	// Words must ALL be in the name, compared the way internal/namefold
	// compares — composed, the four i's one letter, case folded — on both
	// sides. Each word is already folded that way. Checked after Runs.
	Words []string
	// Runs are ASCII text every stored spelling of a matching name holds as
	// it is (see namefold.Plain), so the database can reject most rows with
	// its own case-insensitive LIKE before the costlier comparison above.
	// They narrow nothing Words would not; they only make it cheap.
	Runs []string
	// Prefer decides which rows survive the LIMIT: names equal to it, or to
	// it plus an extension, first; then names starting with it; then the
	// rest — shorter names first within a tier, then by name. Folded like
	// Words. "" orders by name.
	Prefer string
}

NameMatch is what a search without the index asks the database for: the live nodes of one storage whose NAME holds every word (search.PlanFallback builds it; db.Store.SearchNodes answers it). Every field is literal text, never LIKE grammar: the store escapes what its dialect needs.

type Node

type Node struct {
	ID           int64      `json:"id"`
	StorageID    int64      `json:"storage_id"`
	ParentID     *int64     `json:"parent_id,omitempty"`
	Name         string     `json:"name"`
	Path         string     `json:"path"`
	PathHash     string     `json:"path_hash"`
	StorageKey   string     `json:"storage_key,omitempty"`
	Type         NodeType   `json:"type"`
	Size         int64      `json:"size"`
	Mime         string     `json:"mime,omitempty"`
	Etag         string     `json:"etag,omitempty"`
	BackendMtime *time.Time `json:"backend_mtime,omitempty"`
	DBMtime      time.Time  `json:"db_mtime"`
	SyncState    SyncState  `json:"sync_state"`
	// TransferState is "stored" (the bytes are on the driver) or "staged" (the
	// row exists and is listed, but the bytes are still in filex's staging area
	// while a background upload-commit op transfers them). Everything written
	// before staged uploads existed reads back "stored".
	TransferState string     `json:"transfer_state,omitempty"`
	SeenAt        time.Time  `json:"seen_at"`
	DeletedAt     *time.Time `json:"deleted_at,omitempty"`
	CreatedAt     time.Time  `json:"created_at"`
	UpdatedAt     time.Time  `json:"updated_at"`

	// ─── Ownership (migration 00004 + 00038) ───
	//
	// OwnerID is who PUT THE THING HERE. nil means SYSTEM: nobody put it here
	// through filex — the storage scanner found it, it was written straight
	// into the bucket, or the row predates the column. "System" is the honest
	// word for ownerless, and no user is invented to stand in for it.
	//
	// It is set by the acting identity on every write surface (browser upload,
	// new folder, save, WebDAV/FTPS/SFTP/S3/CLI/desktop under the account whose
	// token was used) and it does NOT move afterwards: a move or a rename is
	// the same file, and an overwrite changes the bytes, not whose file it is.
	// The one exception is adoption — a system row (nil) that a user writes
	// becomes that user's, because "nobody's" is not somebody else's.
	OwnerID *int64 `json:"owner_id,omitempty"`
	// LastActorID is who touched it LAST. nil means system for the same reason
	// OwnerID does: a change that arrived from outside filex (the bucket side
	// changed, a sync found new bytes) has no actor to name.
	LastActorID *int64 `json:"last_actor_id,omitempty"`
	// ExternalUpload marks a thing that arrived through an anonymous drop link
	// / file request. The owner is the person who CREATED the link — they asked
	// for the file, it lands in their storage and it is billed to their quota —
	// but the row still has to be able to say the bytes were handed over by
	// somebody else. That somebody is anonymous by design and gets no identity.
	ExternalUpload bool `json:"external_upload,omitempty"`
	// DeletedBy is who put it in the TRASH (migration 00061): the person whose
	// delete it was, named on the row and on every row trashed with it, since
	// the trash lists a folder's contents as rows of their own. nil when
	// nobody in filex did it (the scanner found the object gone, the virus
	// scan quarantined it) or the row was trashed before filex kept this. A
	// restore clears it; so does every soft delete, before the person is
	// named, so a name never outlives the trip through the trash it was for.
	DeletedBy *int64 `json:"deleted_by,omitempty"`

	// UnavailableReason is what the storage answered when the sync last asked
	// whether this row's object still exists and got neither "yes" nor "not
	// found" (migration 00078, issue #104): a plugin that does not speak Stat
	// for it, a permission it lacks, a backend error. Empty for an ordinary
	// row. A row that carries one is listed with a warning, and every
	// operation on it - and below it, for a folder - is refused with 409
	// ENTRY_UNAVAILABLE until the storage answers for it again.
	UnavailableReason string `json:"unavailable_reason,omitempty"`
	// UnavailableAt is when the storage last gave that answer.
	UnavailableAt *time.Time `json:"unavailable_at,omitempty"`
	// Unavailable is UnavailableReason != "", set by the store when it reads
	// the row, so a JSON reader (the AI/MCP answers) has a flag to test.
	Unavailable bool `json:"unavailable,omitempty"`

	// LinkState is why a symlink row (NodeTypeSymlink) is a link filex will
	// not follow: the driver's storage.MetaLinkState - outside_root, broken,
	// unresolved - as the sync last listed it (migration 00098). Empty when no
	// reason is known: a row catalogued before 0.54 until the next sync of its
	// folder, or a driver that gives none.
	//
	// ⚠ The common node reads (GetNode, ListNodesByParent, ...) leave it
	// empty. A listing asks Store.NodeLinkStates for its link rows
	// (handlers hydrateLinkStates); db.NodeLinkStateSQL says why.
	LinkState string `json:"link_state,omitempty"`

	// OwnerName is the owner's display name, resolved in one batched lookup by
	// the API layer for the rows it is about to return. Never persisted, and
	// empty for a system row — the client decides what to call "nobody".
	OwnerName string `json:"owner_name,omitempty"`
	// LastActorName is the same for LastActorID.
	LastActorName string `json:"last_actor_name,omitempty"`

	// Optional joined data — populated by API layer, never persisted.
	Thumb *Thumbnail        `json:"thumb,omitempty"`
	Meta  map[string]string `json:"meta,omitempty"`
	// Storage is the NAME of the storage holding this node, filled by the
	// handlers that return nodes outside a folder listing (starred, recently
	// opened). Those rows carry only a numeric storage_id, and a client in
	// multi-storage mode cannot build the `name://path` it needs to open one —
	// so the recently-opened tray listed files that did nothing when clicked.
	Storage string `json:"storage,omitempty"`
}

Node is the canonical representation of a file or directory in DB cache.

func (*Node) ContentFingerprint added in v0.50.0

func (n *Node) ContentFingerprint() string

ContentFingerprint identifies the version of a node's content without reading it: the backend's etag when it reports one, otherwise size and modification time (to the millisecond).

⚠⚠ The ONE definition of "the content changed". The search index decides whether to extract a file's text again with it, and the thumbnail pipeline whether to draw a file again. A second, slightly different rule would have one of them re-running on every pass while the other never noticed. What each driver gives it, and what slips through, is in docs/thumbnails.md (Design notes) and sync/etag.go objectDrift.

type NodeComment

type NodeComment struct {
	ID        int64      `json:"id"`
	NodeID    int64      `json:"node_id"`
	UserID    int64      `json:"user_id"`
	Body      string     `json:"body"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt time.Time  `json:"updated_at"`
	DeletedAt *time.Time `json:"deleted_at,omitempty"`

	// AuthorName is the joined users.display_name (falling back to email)
	// filled by ListNodeComments. Not persisted.
	AuthorName string `json:"author_name,omitempty"`
	// CanDelete marks whether the CURRENT caller may delete this comment
	// (author or admin). Computed by the API layer. Not persisted.
	CanDelete bool `json:"can_delete,omitempty"`
}

NodeComment is one comment on a file/folder node (v0.6 "Çalışma" (Work) wave, migration 00020). Comments form a flat chronological thread per node, surfaced in the inspector panel.

AuthorName / CanDelete are API-layer projections (joined display name, caller-specific delete right) — never persisted.

type NodeType

type NodeType string

NodeType enumerates node kinds.

const (
	NodeTypeFile      NodeType = "file"
	NodeTypeDirectory NodeType = "dir"
	NodeTypeSymlink   NodeType = "symlink"
)

type NodeVersion

type NodeVersion struct {
	ID         int64     `json:"id"`
	NodeID     int64     `json:"node_id"`
	VersionN   int       `json:"version_n"`
	StorageKey string    `json:"storage_key,omitempty"`
	Size       int64     `json:"size"`
	Etag       string    `json:"etag,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
}

NodeVersion is a historical snapshot of a node's content.

type Notification

type Notification struct {
	ID            int64           `json:"id"`
	Event         string          `json:"event"`
	Severity      string          `json:"severity"`
	Title         string          `json:"title"`
	Body          string          `json:"body"`
	MetaJSON      json.RawMessage `json:"meta"`
	UserID        *int64          `json:"user_id,omitempty"`
	ReadAt        *time.Time      `json:"read_at,omitempty"`
	WebhookStatus string          `json:"webhook_status"`
	WebhookError  string          `json:"webhook_error,omitempty"`
	CreatedAt     time.Time       `json:"created_at"`

	// WebhookReason is, at read time, the webhook cell's second half in the
	// reader's language (notify.WebhookReason): why a delivery was skipped,
	// in the server's words - WebhookError keeps the code (no_destination,
	// digest_unnamed, sibling, stopped) - or a failed delivery's error as the
	// receiver gave it. Never stored.
	WebhookReason string `json:"webhook_reason,omitempty"`

	// Target is derived from MetaJSON at read time (HydrateTarget), never
	// stored in a column of its own. Absent on rows that have nothing to
	// open — the client reads a missing `target` as kind "none".
	Target *NotificationTarget `json:"target,omitempty"`

	// UserName is the display name of the person a user-scoped row belongs
	// to, filled on the admin list only (notify.Service.List with no user).
	// The audit page's Scope column printed `user #2` — a database id is not
	// who somebody is (release-candidate sweep, 2026-09-21).
	UserName string `json:"user_name,omitempty"`

	// AdminsOnly marks, on the admin list, a broadcast only administrators'
	// bells show (an operator alarm such as update_available): its Scope is
	// "Administrators", not "Everyone". Kept beside Audience for clients that
	// read only this flag; it is exactly Audience == "admins".
	AdminsOnly bool `json:"admins_only,omitempty"`

	// Audience says, on the admin list, who a BROADCAST reaches — the same
	// rule the bells apply (notify.BroadcastAudience): "everyone", "viewers"
	// (administrators and the members who can see the file it names),
	// "admins", or "nobody" (routine file activity recorded without the
	// person who did it — kept in the history, shown in no bell). Empty on a
	// row addressed to somebody, and on every row a bell hands out.
	Audience string `json:"audience,omitempty"`

	// E2E is set, at read time, on a row whose sentence names an item inside
	// an end-to-end encrypted folder (notify.SayRows): Title and Body then say
	// "🔒 Encrypted item" there, and E2E says where those words stand, so a
	// browser that has the folder unlocked puts the item's real name in their
	// place. Never stored.
	E2E *NotificationE2E `json:"e2e,omitempty"`

	// Opens says, at read time, whether a click on the row goes somewhere
	// (notify.Opens - the one rule the bell's rows, the page's pop-up and a
	// push follow). Never stored.
	Opens *bool `json:"opens,omitempty"`
}

Notification is one row in the notifications table — either a broadcast (UserID nil) or scoped to a single user.

func (*Notification) HydrateTarget

func (n *Notification) HydrateTarget()

HydrateTarget fills the read-only Target field from MetaJSON. Called on every row the bell hands out (notify.Service.List), so the API item carries `target` as a top-level field instead of making each client dig through `meta`.

type NotificationE2E added in v0.54.0

type NotificationE2E struct {
	Title []NotificationTextPart `json:"title"`
	Body  []NotificationTextPart `json:"body"`
	Names []NotificationE2EName  `json:"names"`
}

NotificationE2E is the one part of a notification's text the server cannot say: the name of an item inside an end-to-end encrypted folder, which it only has scrambled. The sentence is still the server's (internal/notify say.go); Title and Body here are that sentence cut at the names, and Names is what each name is. A reader with the folder's key fills a name in; a reader without it uses the name's Locked text, which is exactly what the row's own title and body already say.

type NotificationE2EName added in v0.54.0

type NotificationE2EName struct {
	// Wire is the item as listings address it, "<storage>://<path>", its
	// encrypted segments as stored.
	Wire string `json:"wire"`
	// Root is the encrypted folder it is in, "<storage>://<root>".
	Root string `json:"root"`
	// Part is "name" (the item's own name) or "path" (where it is, from the
	// encrypted folder down).
	Part string `json:"part"`
	// Locked is what the name reads as without the folder's key: the lock
	// word, or "<root>/…/<lock word>" for a path.
	Locked string `json:"locked"`
}

NotificationE2EName is one name inside an encrypted folder.

type NotificationInput

type NotificationInput struct {
	Event    string
	Severity string
	Title    string
	Body     string
	MetaJSON json.RawMessage
	UserID   *int64
}

NotificationInput is the new-row payload — DB drivers turn this into an INSERT. ID + WebhookStatus default ("pending") are filled in by the store.

type NotificationOpen

type NotificationOpen struct {
	Plugin string `json:"plugin"`
	Action string `json:"action,omitempty"`
	View   string `json:"view,omitempty"`
	// Section is the part of a home page to land on (TargetApp only).
	Section string `json:"section,omitempty"`
}

NotificationOpen is the app-plugin deep link carried by a target: the plugin and either an action id or a view id.

type NotificationSettings

type NotificationSettings struct {
	UserID         int64           `json:"user_id"`
	InAppEnabled   bool            `json:"in_app_enabled"`
	MutedEventsRaw json.RawMessage `json:"muted_events"`
	// UrgentOverridesRaw is the person's own urgent choices (migration
	// 00087): a JSON object {"<event>": true|false}; a kind it does not name
	// follows the administrator's default. ⚠ Nil on a write keeps what is
	// stored: a client that knows nothing of the digest (an older desktop
	// app) resends the two fields above and must not wipe these.
	UrgentOverridesRaw json.RawMessage `json:"urgent_overrides,omitempty"`
}

NotificationSettings captures per-user notification preferences. Stored in the notification_settings table. Default-on for a fresh user — a missing row is treated as InAppEnabled=true with no muted events.

func (*NotificationSettings) IsMuted

func (s *NotificationSettings) IsMuted(event string) bool

IsMuted reports whether this user has muted the given event id. An empty list mutes nothing — the inverse of MatchesEvent's empty-means-everything.

func (*NotificationSettings) MutedList

func (s *NotificationSettings) MutedList() []string

MutedList decodes MutedEventsRaw into trimmed, non-empty event names.

It is the read-side twin of WebhookTarget.EventList below: one place that turns the stored form into a list, so every consumer agrees on what "muted" means.

⚠ Malformed JSON resolves to "nothing muted" rather than to an error. The column is written by the API as a marshalled []string and can only be corrupt if somebody edited the row by hand. Failing open there shows a user one notification they did not want; failing closed would silently hide every notification they have — the wrong way round for a display preference.

func (*NotificationSettings) UrgentOverrides added in v0.53.0

func (s *NotificationSettings) UrgentOverrides() map[string]bool

UrgentOverrides decodes UrgentOverridesRaw. Malformed JSON reads as "no choice made" — the administrator's defaults then apply, which is the fail-open direction for a preference that only ever delays a notice.

type NotificationTarget

type NotificationTarget struct {
	Kind    NotificationTargetKind `json:"kind"`
	Storage string                 `json:"storage,omitempty"`
	Path    string                 `json:"path,omitempty"`
	// ID is the opaque id of a target that is not a path — the share token
	// for TargetShare.
	ID string `json:"id,omitempty"`
	// Open, when set, says what to do once the file is reached: start an
	// app-plugin action or view on it (a "sign this" notification lands the
	// reader in the signing screen, not merely on the file).
	Open *NotificationOpen `json:"open,omitempty"`
}

NotificationTarget is the typed "where does this go" of one notification.

⚠ Storage is the storage NAME, not its numeric id: the explorer addresses storages by name (`<storage>://<path>`), and the id is meaningless to a caller who cannot read /api/admin/storages — which is every non-admin. It is resolved once, centrally, when the event is sent (notify.Service.Send), so no emitter has to look it up and no two emitters can resolve it differently.

⚠ Path is relative to that storage's root and never carries the `<storage>://` prefix; Kind decides how it is read — the file itself for TargetFile, the folder for TargetDir, the path the item was deleted FROM for TargetTrash.

func TargetFromMeta

func TargetFromMeta(meta json.RawMessage) *NotificationTarget

TargetFromMeta pulls the target back out of a stored meta blob.

The target rides inside meta_json (the same fold notify.marshalMeta does for node/share/actor) rather than in a column of its own, so reading it back is a parse and not a migration. A blob that has no target — every row written before this field existed — reads back nil, which every client treats as TargetNone.

type NotificationTargetKind

type NotificationTargetKind string

NotificationTargetKind says WHAT a notification is about, so a click on it can go somewhere. It is a closed set on purpose: every click surface (bell row, browser notification, desktop notification) switches on these five and nothing else, which is what keeps the three from disagreeing.

const (
	TargetNone  NotificationTargetKind = "none"
	TargetFile  NotificationTargetKind = "file"
	TargetDir   NotificationTargetKind = "dir"
	TargetShare NotificationTargetKind = "share"
	// TargetTrash is the Trash view, with the item that came from Path
	// (its ORIGINAL path in Storage) selected. What a soft delete — or an
	// antivirus quarantine — addresses.
	//
	// ⚠⚠ It exists because the only address a trashed file used to have was
	// its trash KEY: `file.trashed` shipped `{kind: file, path:
	// ".filex-trash/<unix>-<rand>__<name>"}`, and a click opened the bin's
	// raw folder — breadcrumb `docs › .filex-trash`, an empty listing, no way
	// to restore (the owner's report, 2026-09-21). The Trash view lists items
	// by where they CAME FROM, so the original path is the one address in it
	// that means anything; `.filex-trash/…` is never a target of any kind.
	TargetTrash NotificationTargetKind = "trash"
	// TargetApp opens one of an app plugin's HOME pages (Open.Plugin +
	// Open.View, optionally at Open.Section) — a notice about a list rather
	// than about a file. No storage, no path. A click surface that has no
	// such page (the desktop app) brings its window forward and stops.
	TargetApp NotificationTargetKind = "app"
)

The five target kinds. "none" is a real answer, not a missing one — an event about the whole instance (a replica failure, an available update) has nothing to open, and saying so is better than inventing a path.

type NotificationTextPart added in v0.54.0

type NotificationTextPart struct {
	Text string `json:"text,omitempty"`
	Name *int   `json:"name,omitempty"`
}

NotificationTextPart is one piece of a sentence: either words (Text) or the name at Names[Name].

type OfficeSession added in v0.53.0

type OfficeSession struct {
	// DocKey is the session's document.key.
	DocKey string `json:"doc_key"`
	NodeID int64  `json:"node_id"`
	// Size, MtimeNs (Unix nanoseconds, 0 = none) and Etag ("" = none) are the
	// version the storage driver reported.
	Size    int64  `json:"size"`
	MtimeNs int64  `json:"mtime_ns"`
	Etag    string `json:"etag,omitempty"`
	// Unknown: the session is on an older version, which one is not known.
	Unknown bool `json:"unknown,omitempty"`
	// Dropped: the person chose the outside version; the save is not written.
	Dropped bool `json:"dropped,omitempty"`
	// ExpiresUnix is when the row may be removed (Unix seconds).
	ExpiresUnix int64 `json:"expires_unix"`
}

OfficeSession is the version of a document an ONLYOFFICE editing session opened (table office_sessions, migration 00092; internal/onlyoffice session_base.go owns what it means, #184). A save of the session is written over the file only while the file is still this version.

type PermRuleConditions added in v0.49.0

type PermRuleConditions struct {
	// StorageIDs limits the rule to these storages; empty is every storage.
	StorageIDs []int64 `json:"storage_ids,omitempty"`
	// Paths limits it to storage-relative paths matching any of these
	// patterns: "Archive" or "Archive/**" (that folder and everything in
	// it), "*" within a segment ("*.psd", "Clients/*/Contracts/**").
	Paths []string `json:"paths,omitempty"`
}

PermRuleConditions narrow WHERE a rule's effects apply (migration 00069). Empty is everywhere. A rule with conditions may only change file permissions checked against a path (perm.Conditionable) and carries no settings — perm.NormalizeRule refuses anything else.

func (PermRuleConditions) Empty added in v0.49.0

func (c PermRuleConditions) Empty() bool

Empty reports whether c applies everywhere.

type PermRuleSettings added in v0.49.0

type PermRuleSettings struct {
	// ShareLinkMaxDays caps a public link's lifetime; also forbids links
	// with no expiry.
	ShareLinkMaxDays *int `json:"share_link_max_days,omitempty"`
	// ShareLinkPasswordRequired refuses a public link without a password.
	ShareLinkPasswordRequired bool `json:"share_link_password_required,omitempty"`
	// BlockedExtensions refuses uploads by file extension, lowercase and
	// without the dot ("exe", "bat").
	BlockedExtensions []string `json:"blocked_extensions,omitempty"`
	// MaxUploadBytes caps a single file's size.
	MaxUploadBytes *int64 `json:"max_upload_bytes,omitempty"`
	// Require2FA refuses a session to an account without TOTP enabled.
	Require2FA bool `json:"require_2fa,omitempty"`
	// Apps are the role's decisions about app permissions (perm/app.go):
	// "app.sign.request" → allow | deny. A key it does not name follows the
	// built-in role's decision, then the app's own default.
	Apps map[string]string `json:"apps,omitempty"`
}

PermRuleSettings are the valued limits a rule carries (as opposed to the yes/no permissions). A nil / zero field is "not set by this rule". When several rules apply, the most restrictive value wins (perm.Resolve).

type PermRuleTarget added in v0.49.0

type PermRuleTarget struct {
	Kind  string `json:"kind"`
	Value string `json:"value,omitempty"`
}

PermRuleTarget is one "who" of a permission rule.

type PermissionRule added in v0.49.0

type PermissionRule struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// Names and Descriptions are the role in other interface languages
	// (migration 00071): language code → text, {"tr": "Muhasebe"}. Name and
	// Description stay the role's own — the answer for a language without an
	// entry, and what the audit log records. Pick for a reader with NameFor
	// and DescriptionFor, never by indexing these.
	Names        map[string]string `json:"names,omitempty"`
	Descriptions map[string]string `json:"descriptions,omitempty"`
	Enabled      bool              `json:"enabled"`
	// Permissions is the role's own list (migration 00069): what its people
	// may do everywhere.
	Permissions []string           `json:"permissions"`
	ProviderID  *int64             `json:"provider_id,omitempty"`
	Targets     []PermRuleTarget   `json:"targets"`
	Effects     map[string]string  `json:"effects"`
	Settings    PermRuleSettings   `json:"settings"`
	Conditions  PermRuleConditions `json:"conditions"`
	CreatedBy   *int64             `json:"created_by,omitempty"`
	CreatedAt   time.Time          `json:"created_at"`
	UpdatedAt   time.Time          `json:"updated_at"`
}

PermissionRule is one row of permission_rules (migration 00069): a custom role. Permissions is what it allows everywhere; Effects with Conditions is its "different in some folders" part — Allow or Deny for file actions in the places Conditions names.

ProviderID scopes the rule to one tenant; nil is install-wide. Targets and Effects are validated by package perm before they are stored — the store persists what it is handed.

func (*PermissionRule) DescriptionFor added in v0.49.0

func (r *PermissionRule) DescriptionFor(lang string) string

DescriptionFor is NameFor for the description.

func (*PermissionRule) NameFor added in v0.49.0

func (r *PermissionRule) NameFor(lang string) string

NameFor is the role's name for a reader whose interface language is lang (LocalizedText): the one place that choice is made — the refusal sentences reach it through perm.Source.

type Plugin

type Plugin struct {
	ID int64 `json:"id"`
	// Name is the operator-chosen identifier, unique, [a-z0-9][a-z0-9_-]{0,31}.
	// It names the plugin's directory and appears in logs; the DRIVER name is
	// what the plugin itself describes, and may differ.
	Name string `json:"name"`
	Kind string `json:"kind"`
	// Binary is the file name inside <data-dir>/plugins/<name>/ (kind binary).
	Binary string `json:"binary,omitempty"`
	// SHA256 of the binary as installed. Checked again on every start: a file
	// that changed under filex is refused, not run.
	SHA256 string `json:"sha256,omitempty"`
	// Address is the remote base URL (kind remote), http(s)://host:port[/path].
	Address string `json:"address,omitempty"`
	// TokenSealed is the bearer token, sealed with the instance secret key
	// (secretbox). For a binary plugin filex minted it and hands it to the
	// process at start; for a remote one the admin supplied it. Never on the
	// wire.
	TokenSealed string `json:"-"`
	Enabled     bool   `json:"enabled"`

	// Version and Driver are the last values the plugin described, kept so
	// the list can show them while a plugin is stopped.
	Version string `json:"version,omitempty"`
	Driver  string `json:"driver,omitempty"`
	// LastError is the last start/validation failure, cleared on success.
	LastError string `json:"last_error,omitempty"`

	// Source is where newer versions of a binary plugin are published:
	// `owner/name` (the filex-storage.json attached to the repository's
	// latest release) or the https address of a filex-storage.json feed.
	// Empty = none (plugin/updates.go).
	Source string `json:"source,omitempty"`
	// UpdateJSON is what the last update check found (plugin.UpdateInfo);
	// the admin reads it as Status.Update.
	UpdateJSON string `json:"-"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Plugin is one registered storage plugin (migration 00029) — see internal/plugin for what a plugin is and how it is spoken to.

The row is the ADMIN's intent (which plugin, from where, on or off); what the plugin is doing right now (running, crashed, refused, which driver it registered) is runtime state the manager reports next to it and never persists — a restart re-derives it by starting the plugin again.

type PluginRequest added in v0.49.0

type PluginRequest struct {
	ID int64
	// Key is random hex, unique: what the store reads a new row back by.
	Key string
	// Kind is PluginRequestKindApp or PluginRequestKindStorage.
	Kind string
	// Op is PluginRequestOpInstall or PluginRequestOpUpgrade.
	Op string
	// Name is the plugin's name: an app's manifest name, or the name a
	// storage plugin is (to be) installed under.
	Name string
	// PluginID is the installed plugin an upgrade replaces (app_plugins.id or
	// plugins.id, per Kind); nil for an install.
	PluginID *int64
	// SourceKind is github | url | source | from_source; SourceJSON the source
	// as given (pluginreq.Source), SourceKey the sha256 of its normalized form.
	SourceKind string
	SourceJSON string
	SourceKey  string
	// Version is what the source answered; FromVersion the version an upgrade
	// replaces.
	Version     string
	FromVersion string
	// ManifestJSON is the resolved snapshot (an app's filex-app.json, a
	// storage plugin's filex-storage.json feed); ReviewJSON the dry run's
	// review, for the approval screen.
	ManifestJSON string
	ReviewJSON   string
	// SHA256 is what approval holds the bytes to (an app's module, or its
	// manifest when it has none; a storage plugin's binary). ManifestSHA256
	// pins an app's manifest, which the module's hash does not.
	SHA256         string
	ManifestSHA256 string
	// PermissionsJSON is the JSON array of permission ids the request asks to
	// grant, frozen at request time.
	PermissionsJSON string
	// RequestedBy is the account that asked (nil once deleted); Requester its
	// name as shown when it asked; TokenID/TokenLabel the API key it came
	// through (nil/"" from a session).
	RequestedBy *int64
	Requester   string
	TokenID     *int64
	TokenLabel  string
	// Reason is the requester's own words.
	Reason string
	// Status is one of the PluginRequestStatus* values.
	Status string
	// DecidedBy/Decider/DecidedAt: who closed the request and when (an
	// expired one: nobody, at its expiry).
	DecidedBy *int64
	Decider   string
	DecidedAt *time.Time
	// DecisionNote is a rejection's reason, or why the request was superseded.
	DecisionNote string
	// ResultJSON is what approval did — the installed plugin — or, while the
	// request stays pending, the last attempt's refusal.
	ResultJSON string
	ExpiresAt  time.Time
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

PluginRequest is a request to install or upgrade a plugin, left by somebody who may not do it themselves — an API key (an agent, a script, the CLI) — for an administrator signed in to the admin panel to approve or reject (migration 00070, internal/pluginreq, docs/APP-PLUGINS.md → Install requests).

What the source answered when the request was made is FROZEN here: the manifest, the hash approval holds the bytes to, and the permissions it asks to grant. An approval installs exactly that; a source that answers other bytes by then makes the request `superseded`.

type Provider

type Provider struct {
	ID   int64  `json:"id"`
	Slug string `json:"slug"`
	// Realm is the tenant's sign-in name (migration 00073, package tenant):
	// the sign-in form's Realm field and `realm/name` over SFTP. Set when the
	// tenant is created and never changed — UpdateProvider does not write it.
	// Empty for the supertenant, which is signed in to with an empty realm.
	Realm    string `json:"realm"`
	Name     string `json:"name"`
	Host     string `json:"host,omitempty"`
	AuthType string `json:"auth_type"`

	OIDCIssuer       string `json:"oidc_issuer,omitempty"`
	OIDCClientID     string `json:"oidc_client_id,omitempty"`
	OIDCClientSecret string `json:"-"` // never serialized to the client
	OIDCRedirectURL  string `json:"oidc_redirect_url,omitempty"`
	RoleClaim        string `json:"role_claim,omitempty"`
	AdminGroup       string `json:"admin_group,omitempty"`

	// CookieDomain, when set (e.g. ".example.com"), is stamped as the Domain
	// attribute on this tenant's session cookie so the tenant's subdomains
	// share the session. Empty = derive from Host by dropping its first
	// label (files.example.com → .example.com), falling back to the global
	// FILEX_COOKIE_DOMAIN. Only consulted in multi-tenant mode.
	CookieDomain string `json:"cookie_domain,omitempty"`

	IsSupertenant bool `json:"is_supertenant"`
	Enabled       bool `json:"enabled"`

	// AllowInsecureAuth lets this tenant's own sign-in providers reach an
	// internal address and plain ldap:// (migration 00076,
	// docs/TENANT-ADMIN.md). Off by default; only a platform super
	// administrator sets it (db.Store.SetProviderAllowInsecureAuth), and
	// UpdateProvider never writes it.
	AllowInsecureAuth bool `json:"allow_insecure_auth"`

	// OIDCTrustEmail is the tenant's own OIDC's "trust this provider's email
	// addresses" (migration 00079, docs/SSO.md): every address it sends counts
	// as verified, `email_verified` or not. Off for a tenant made from 0.50 on;
	// ON for the ones an upgrade found (OIDCTrustEmailByUpgrade). Written by
	// db.Store.SetProviderOIDCTrustEmail only; UpdateProvider never writes it.
	OIDCTrustEmail bool `json:"oidc_trust_email"`
	// OIDCTrustEmailByUpgrade: the value above is the one the upgrade to 0.50
	// set, to keep the old behaviour, and nobody has saved it since. Filled
	// by the API from the upgrade's record, not a column.
	OIDCTrustEmailByUpgrade bool `json:"oidc_trust_email_by_upgrade,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Provider is a tenant: an auth realm (OIDC or local) bound to a host and linked to one or more storages via provider_storages. Users carry a provider_id tag assigned at (JIT) login.

Isolation is enforced in TWO independent layers (docs/MULTI-TENANCY.md):

  1. file data — storage confinement (a provider only reaches its linked storages); a bug here is the only way file data could cross a tenant.
  2. directory — user/share/grant scoping by provider_id; a bug here leaks at most a name, never file data.

A provider with IsSupertenant is confine-EXEMPT and platform-scoped: its admins see every tenant. There must be at most one, it should be a hardened realm, and a local bootstrap admin remains the break-glass path.

func (*Provider) LoginRealm added in v0.50.0

func (p *Provider) LoginRealm() string

LoginRealm is the realm this tenant's accounts are addressed by: its Realm, or "" for the supertenant — the platform's own tenant is signed in to with an empty realm, and a tenant that became the supertenant (the flag moved to it) keeps its old realm only as a second name.

type ProviderDomain added in v0.50.0

type ProviderDomain struct {
	ID         int64  `json:"id"`
	ProviderID int64  `json:"provider_id"`
	Domain     string `json:"domain"`
	Status     string `json:"status"`
	// LastError is what the last check found when it did not find the CNAME
	// ("points at other.example", "no CNAME record").
	LastError string `json:"last_error,omitempty"`
	// LastErrorCode and LastErrorParams say the same as LastError for a
	// screen to put in the reader's language: one of the DomainWhy* codes and
	// its names (domain, found, target, tenant_domain, detail). LastError
	// stays the English sentence, for a log, a tool and a code a screen does
	// not know.
	LastErrorCode   string            `json:"last_error_code,omitempty"`
	LastErrorParams map[string]string `json:"last_error_params,omitempty"`
	CheckedAt       *time.Time        `json:"checked_at,omitempty"`
	ActiveSince     *time.Time        `json:"active_since,omitempty"`
	// TLSCertPEM is a certificate the tenant brought (the chain, PEM); its
	// key is sealed in TLSKeySealed and never sent to a client.
	TLSCertPEM   string     `json:"-"`
	TLSKeySealed string     `json:"-"`
	TLSNotAfter  *time.Time `json:"tls_not_after,omitempty"`
	CreatedBy    *int64     `json:"created_by,omitempty"`
	CreatedAt    time.Time  `json:"created_at"`
}

ProviderDomain is a tenant's own domain.

func (*ProviderDomain) HasOwnCertificate added in v0.50.0

func (d *ProviderDomain) HasOwnCertificate() bool

HasOwnCertificate reports whether the tenant brought a certificate for it.

type ProviderE2E added in v0.51.0

type ProviderE2E struct {
	// Allowed is the platform's ceiling, which only the supertenant changes.
	// Off: nobody in the tenant encrypts anything new, whatever the policy.
	Allowed bool
	// Policy is the tenant's own choice, one of the E2EPolicy* values.
	Policy string
}

ProviderE2E is one tenant's two encryption columns, read and written apart from the rest of the provider row (Store.GetProviderE2E/SetProviderE2E).

type ProviderStorage

type ProviderStorage struct {
	ProviderID int64 `json:"provider_id"`
	StorageID  int64 `json:"storage_id"`
}

ProviderStorage links a tenant to a storage (M:N; 1:1 in the current UI). A storage reachable through no provider link is only visible to a supertenant / to single-tenant mode.

type PushSubscription added in v0.54.0

type PushSubscription struct {
	ID           int64  `json:"id"`
	UserID       int64  `json:"-"`
	Endpoint     string `json:"-"`
	EndpointHash string `json:"endpoint_hash"`
	P256dh       string `json:"-"`
	Auth         string `json:"-"`
	// Label is what the device was called when it subscribed ("Chrome on
	// Android"), for the person's list.
	Label string `json:"label"`
	// Service is the push service's host (fcm.googleapis.com,
	// web.push.apple.com ...), filled when answered.
	Service string `json:"service,omitempty"`
	// ThroughID is the device's mark: every notification at or below it has
	// been pushed to it or passed over. A compare-and-set moves it, so two
	// servers never push one row twice.
	ThroughID int64 `json:"-"`
	// Failures counts the push service's refusals since the last push it
	// took; past a few the device is dropped.
	Failures  int        `json:"-"`
	CreatedAt time.Time  `json:"created_at"`
	LastOKAt  *time.Time `json:"last_ok_at,omitempty"`
}

PushSubscription is one device a person receives Web Push on (task #191, internal/notify push.go): a browser profile on a phone or a computer, or the web app added to an iPhone's Home Screen.

⚠ Endpoint, P256dh and Auth never go out in an answer. The endpoint is an address anybody holding it (and the instance's VAPID key) may push to; the client finds itself in the list by EndpointHash (webpush.EndpointHash, the hex SHA-256 it computes the same way).

type PushVAPIDKey added in v0.54.0

type PushVAPIDKey struct {
	PublicKey     string
	PrivateSealed string
	CreatedAt     time.Time
}

PushVAPIDKey is the instance's application server key (RFC 8292): made at the first start, replaced only by a rotation.

⚠ PrivateSealed is the private key sealed with FILEX_SECRET_KEY (internal/secretbox, bound to its row); it never leaves the server.

type RecentSearch added in v0.53.0

type RecentSearch struct {
	ID     int64 `json:"id"`
	UserID int64 `json:"-"`
	// Surface is which search box kept it (RecentSearchSurfaceAdmin).
	Surface string `json:"-"`
	// Query is the words as typed, a prefix such as `file:` included.
	Query      string    `json:"query"`
	SearchedAt time.Time `json:"searched_at"`
}

RecentSearch is one thing a person searched for in a search box that keeps its history on the server (migration 00090, task #168): the admin panel's search today. The list is per person and per surface, newest first, and the same words searched again are one row that moved to the top.

type ReplicaFailure

type ReplicaFailure struct {
	ID            int64      `json:"id"`
	StorageID     int64      `json:"storage_id"`
	Path          string     `json:"path"`
	Op            string     `json:"op"`
	ErrorCode     string     `json:"error_code"`
	ErrorMsg      string     `json:"error_msg"`
	Attempts      int        `json:"attempts"`
	LastAttemptAt time.Time  `json:"last_attempt_at"`
	ResolvedAt    *time.Time `json:"resolved_at,omitempty"`
}

ReplicaFailure is one row in replica_failures. Each (storage, path, op) has at most one row — repeated failures bump attempts via UPSERT.

StorageID names the storage the path belongs to (migration 00094): paths are relative to a storage, and several storages can replicate at once, so a repair has to know whose wrapper to replay it through. 0 is a row written before 00094, which no storage can claim.

type ReplicaInitialCopy added in v0.53.0

type ReplicaInitialCopy struct {
	StorageID int64  `json:"storage_id"`
	TargetID  int64  `json:"target_id"`
	Phase     string `json:"phase"`
	// Cursor is the last file the walk finished with, in the walk's own order
	// (internal/replica walk.go); the next slice starts after it. "" = from
	// the beginning of the phase.
	Cursor string `json:"-"`
	// Counted: Total is final (the counting walk finished).
	Counted bool `json:"counted"`
	// Total is the number of files on the storage (so far, while counting).
	Total int64 `json:"total"`
	// Copied, Present, Excluded and Failed add up to the files looked at:
	// written to the target, already there (same size, not older), left out
	// by a `skip` rule, failed (in replica_failures, Fix all replays them).
	Copied      int64  `json:"copied"`
	Present     int64  `json:"present"`
	Excluded    int64  `json:"excluded"`
	Failed      int64  `json:"failed"`
	CopiedBytes int64  `json:"copied_bytes"`
	LastError   string `json:"last_error,omitempty"`
	// StartedUnix, UpdatedUnix, FinishedUnix: Unix seconds; FinishedUnix is
	// 0 until the copy is done.
	StartedUnix  int64 `json:"started_unix"`
	UpdatedUnix  int64 `json:"updated_unix"`
	FinishedUnix int64 `json:"finished_unix"`
	// LeaseOwner/LeaseUntil: the worker running a slice of this copy now and
	// until when its claim holds (Unix seconds). Revision moves on every write
	// so an UPDATE always changes the row (MySQL counts changed rows).
	LeaseOwner string `json:"-"`
	LeaseUntil int64  `json:"-"`
	Revision   int64  `json:"-"`
}

ReplicaInitialCopy is one row in replica_initial_copies (migration 00094): the copy of the files a storage ALREADY held when it was linked to a replication target. The live fan-out only sees writes made after the link; this is what brings the backup up to the storage's state at that moment.

One row per storage, for the target it is linked to now. Times are Unix seconds in integer columns so every engine returns them unchanged.

func (*ReplicaInitialCopy) Done added in v0.53.0

func (c *ReplicaInitialCopy) Done() int64

Done is the number of files the copy has looked at so far.

type ReplicaLink struct {
	StorageID   int64  `json:"storage_id"`
	TargetID    int64  `json:"target_id"`
	Folder      string `json:"folder"`
	FolderKey   string `json:"-"`
	CreatedUnix int64  `json:"created_unix"`
}

ReplicaLink is one row in replica_links (migration 00095): the folder a storage writes into on the replication target it is linked to. Chosen once, when the storage is first linked there (internal/replica folder.go), and kept through a rename of the storage or a target switched off and on. FolderKey is the folder lowercased: no two storages on one target share it.

type ReplicaRule

type ReplicaRule struct {
	ID          int64     `json:"id"`
	PathPattern string    `json:"path_pattern"`
	Mode        string    `json:"mode"`
	Priority    int       `json:"priority"`
	Enabled     bool      `json:"enabled"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

ReplicaRule is one path-glob → mode entry. Lower priority wins (priority asc, first match returns).

type ReplicaRuleInput

type ReplicaRuleInput struct {
	PathPattern string `json:"path_pattern"`
	Mode        string `json:"mode"`
	Priority    int    `json:"priority"`
	Enabled     bool   `json:"enabled"`
	Description string `json:"description"`
}

ReplicaRuleInput is the upsert payload — id is filled in by the store on INSERT.

type ReplicaSettings

type ReplicaSettings struct {
	ReportCron    string `json:"report_cron"`
	ReportEnabled bool   `json:"report_enabled"`
	DefaultMode   string `json:"default_mode"`
}

ReplicaSettings is the singleton config row (id=1).

type ReplicaStatusReport

type ReplicaStatusReport struct {
	GeneratedAt   time.Time       `json:"generated_at"`
	TotalFiles    int64           `json:"total_files"`
	FailedCount   int64           `json:"failed_count"`
	RepairedCount int64           `json:"repaired_count"`
	SummaryJSON   json.RawMessage `json:"summary"`
}

ReplicaStatusReport is the singleton row produced by the cron job. Only one row exists at a time (id=1, CHECK constraint).

type ReplicationTarget

type ReplicationTarget struct {
	ID         int64           `json:"id"`
	Name       string          `json:"name"`
	Driver     string          `json:"driver"`
	ConfigJSON json.RawMessage `json:"config"`
	Mode       string          `json:"mode"` // "async" | "sync"
	Enabled    bool            `json:"enabled"`
	CreatedAt  time.Time       `json:"created_at"`
	UpdatedAt  time.Time       `json:"updated_at"`
}

ReplicationTarget is a backup-only sink that the replica engine fans writes out to. It is NOT a regular storage — operators never write to it directly, and it never appears in the Depolar list or the file explorer. Defined in v0.1.18 (migration 00009).

type Role

type Role struct {
	ID          int64    `json:"id"`
	Name        string   `json:"name"`
	Permissions []string `json:"permissions"`
}

Role definition (DB row).

type S3AccessKey

type S3AccessKey struct {
	ID int64 `json:"id"`
	// AccessKeyID is the public half — what the client puts in the
	// Authorization header, and what an incoming signature is looked up by.
	AccessKeyID string `json:"access_key_id"`
	// SecretEnc is the sealed secret. It is `json:"-"` for the same reason a
	// password hash is: the plaintext is shown exactly once, at creation, and
	// no listing endpoint may ever hand it back.
	SecretEnc string `json:"-"`
	UserID    int64  `json:"user_id"`
	// APITokenID ties the key to the token it inherits from, or nil when it was
	// minted straight from the account. The FK cascades, so revoking the token
	// revokes this key too — a credential must not outlive its parent.
	APITokenID *int64 `json:"api_token_id,omitempty"`
	Label      string `json:"label"`
	// Bucket/Prefix are the optional confinement, in the shape S3 clients
	// already understand (and IAM already models). Empty Bucket means every
	// storage the caller can see.
	Bucket string `json:"bucket,omitempty"`
	Prefix string `json:"prefix,omitempty"`

	CreatedAt  time.Time  `json:"created_at"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"`
	// DisabledAt turns the key off without deleting it, so an operator can stop
	// a leaked credential and still see it in the audit trail.
	DisabledAt *time.Time `json:"disabled_at,omitempty"`
}

S3AccessKey is the credential an S3 client signs requests with (migration 00026).

Why it is not an APIToken with a different label

An API token is sha256-hashed because a bearer protocol sends the secret and the server only has to compare. SigV4 sends no secret — it sends an HMAC chain derived from one — so the server must be able to recompute that chain, which means holding the secret in recoverable form (SecretEnc, sealed by internal/secretbox). Different storage contract, different table.

What it may and may not do

A key minted FROM an API token (APITokenID non-nil) is that token projected into a protocol that cannot carry it. Its effective permission is the intersection of everything upstream — token scopes ∩ ACL grants ∩ confinement ∩ tenant ∩ storage read_only ∩ role ceiling. It may narrow; it may never widen. A key with no token carries the account's own permissions.

func (*S3AccessKey) Usable

func (k *S3AccessKey) Usable(now time.Time) bool

Usable reports whether the key may authenticate right now. Expiry and the disabled flag are checked in one place so a caller cannot honour one and forget the other.

type SSHPublicKey

type SSHPublicKey struct {
	ID     int64 `json:"id"`
	UserID int64 `json:"user_id"`
	// Name is the human label — the comment from the pasted key by default,
	// which is usually `user@machine` and is exactly what somebody needs to
	// decide which row to revoke.
	Name string `json:"name"`
	// Fingerprint is the SHA256 fingerprint OpenSSH prints, base64, without the
	// `SHA256:` prefix. It is what a login is looked up by.
	Fingerprint string `json:"fingerprint"`
	// PublicKey is the normalised wire form (`<type> <base64>`), so the key can
	// be shown back and exported.
	PublicKey string `json:"public_key"`

	CreatedAt  time.Time  `json:"created_at"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
	// DisabledAt switches a key off without deleting it, so a machine that is
	// away for a while can be stopped and restored rather than re-registered.
	DisabledAt *time.Time `json:"disabled_at,omitempty"`
}

SSHPublicKey is a key an account registered for the SFTP endpoint (migration 00027).

What it decides, and what it does not

It does NOT decide whether the client holds the private key — x/crypto/ssh has already verified the signature by the time filex is consulted. It decides WHICH ACCOUNT that key belongs to, which is why the fingerprint is unique across the install: two accounts sharing a key would make the login ambiguous, and resolving it by taking the first row is how a key ends up authenticating as somebody else.

func (*SSHPublicKey) Usable

func (k *SSHPublicKey) Usable() bool

Usable reports whether this key may authenticate right now.

type Session

type Session struct {
	ID        int64     `json:"id"`
	UserID    int64     `json:"user_id"`
	Token     string    `json:"-"`
	ExpiresAt time.Time `json:"expires_at"`
	IP        string    `json:"ip,omitempty"`
	UserAgent string    `json:"user_agent,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

Session is a server-side login session.

type Share

type Share struct {
	ID      int64  `json:"id"`
	NodeID  int64  `json:"node_id"`
	Token   string `json:"token"`
	PinHash string `json:"-"`       // never serialized
	HasPin  bool   `json:"has_pin"` // computed
	// PinEnc is the SEALED PIN (internal/secretbox, AES-GCM under
	// FILEX_SECRET_KEY) — migration 00049. It exists so the link's owner and an
	// administrator can be told the PIN again; nothing ever verifies against
	// it, because PinHash above is still the gate (share/pin.go CheckPIN).
	//
	// ⚠ `json:"-"`, like the hash and for a stronger reason: a hash handed out
	// is a hash to crack offline, but a ciphertext handed out is one guess away
	// from the PIN for anyone who also gets the key. It leaves this process
	// only as PLAINTEXT, only through GET /api/shares/{id}/pin, only to the
	// creator or an admin, and only with an audit row written.
	PinEnc string `json:"-"`
	// PinRecoverable is computed: can this link's PIN still be shown? False
	// for a link minted before 00049, for one minted while the instance had no
	// FILEX_SECRET_KEY, and for one with no PIN at all. It carries no secret —
	// it is what lets a row say "this one cannot be shown" instead of offering
	// a copy button with nothing behind it.
	PinRecoverable bool       `json:"pin_recoverable"`
	ExpiresAt      *time.Time `json:"expires_at,omitempty"`
	MaxDownloads   *int       `json:"max_downloads,omitempty"`
	DownloadCount  int        `json:"download_count"`
	// VisitCount is how many times an APP page was opened (00052). Kept
	// apart from DownloadCount because a page view is not a download: a
	// signing link somebody had only looked at twice said "İndirme 2" in
	// My shares (2026-09-21). Always 0 for an ordinary share.
	VisitCount int    `json:"visit_count"`
	CreatedBy  *int64 `json:"created_by,omitempty"`
	// CreatedVia is the token username the creating API call acted under
	// ("work", "fishapp"…). Empty for browser-session shares. Display-only —
	// ownership/authorization stay on CreatedBy.
	CreatedVia string    `json:"created_via,omitempty"`
	CreatedAt  time.Time `json:"created_at"`

	// Drop-link fields (Kind == ShareKindDrop). Empty/zero for a normal
	// download share.
	Kind         string  `json:"kind"`                    // "download" | "drop"
	MaxUploads   *int    `json:"max_uploads,omitempty"`   // cap on total files received
	UploadCount  int     `json:"upload_count"`            // files received so far
	DropSettings *string `json:"drop_settings,omitempty"` // JSON limits blob (max_files, max_file_size_mb, allowed_ext, ask_name)

	// ── App-plugin page fields (migration 00046) ──
	//
	// An app plugin's public page IS a share: the plugin opens one through the
	// `share_create` host function and the visitor's link is /s/<token> like
	// every other, so the administrator sees and revokes it in Shares, the
	// expiry ceiling is the instance's, and there is ONE PIN implementation to
	// get right. Zero here means an ordinary share and every path behaves
	// exactly as it did before.
	PluginID int64  `json:"plugin_id,omitempty"`
	PageID   string `json:"page_id,omitempty"`
	// Subject is the line the public shell puts in its header ("Please sign
	// contract.pdf"). Host-owned; the plugin supplies it at creation.
	Subject string `json:"subject,omitempty"`
	// StateJSON is the PLUGIN's durable record for this link, read and
	// replaced by the share_state host function. Never serialized: it is the
	// app's own bookkeeping, not the visitor's business.
	StateJSON string `json:"-"`
	// FilesJSON lists the copies the plugin exposed to the visitor —
	// [{ref,name,file,size,mime}] — where `file` is the BASENAME inside the
	// share's directory under <data-dir>/app-plugins/public/<id>/. Never
	// serialized: the public API answers with refs, not with the layout on
	// disk.
	FilesJSON string `json:"-"`
	// PurposeJSON is what the app said this link IS when it opened it
	// (wire.PagePurpose, migration 00052): the only way a page-less link —
	// a plain share of a finished document — can be named for what it is in
	// a list of links. Not serialized: lists carry the resolved `app`
	// (db.AppLink) instead.
	PurposeJSON string `json:"-"`
	// AppRoot is the `root:` of the token whose app job opened this link
	// (`<adapter>://<rel>`, migration 00082), "" for a link opened with no
	// root. A visitor's job on the link runs as its creator with no token
	// behind it, so this is the root it is held to: the door stamps it on the
	// job (wasmplugin.StampJobRoot) as the token's own door would have. Never
	// serialized: it is the host's bookkeeping.
	AppRoot string `json:"-"`

	// PinFails / LockedUntil are the five-strikes-then-ten-minutes PIN lock,
	// which from 00046 covers EVERY public link rather than only an app page.
	// Never serialized: a strike counter a client can read is one it can pace
	// itself against.
	PinFails    int        `json:"-"`
	LockedUntil *time.Time `json:"-"`

	// RevokedAt is when a person (or the app that opened it) ENDED the link
	// (00053). Revoking also sets ExpiresAt to the same moment, which is what
	// stops the link working; this is only the word for it, so a screen can
	// say "revoked" rather than "expired" (QA, 2026-09-21: My shares said
	// "Süresi doldu" for a link revoked a minute earlier).
	//
	// ⚠ Filled by the LISTINGS (shareMetaCols), not by the single-row reads
	// (shareCols): those decide whether a link works, and that is the expiry.
	RevokedAt *time.Time `json:"revoked_at,omitempty"`

	// State is where the link stands - ShareStateActive, ShareStateExpired,
	// ShareStateExhausted or ShareStateRevoked - said by the server
	// (StateAt) on every LIST of links, so a screen shows the word instead of
	// working it out from the expiry on the browser's clock. Empty on a
	// single-row read, which decides whether the link works (IsExpired).
	State string `json:"state,omitempty"`
}

Share is a public token granting limited access to a node. For a download share this is read (download); for a drop share it is blind upload into the node (a directory) — see Kind.

func (*Share) CappedCount

func (s *Share) CappedCount() int

CappedCount is the counter `max_downloads` is measured against: the visits of an app page (its `max_visits`), the downloads of anything else.

func (*Share) IsApp

func (s *Share) IsApp() bool

IsApp reports whether an app plugin answers this link — the visitor gets that app's surface instead of a download or a folder listing.

func (*Share) IsDrop

func (s *Share) IsDrop() bool

IsDrop reports whether this is a public upload (file-drop) share.

func (*Share) IsExpired

func (s *Share) IsExpired(now time.Time) bool

IsExpired reports whether the share has lapsed. Covers time expiry, the download cap (download shares) and the upload cap (drop shares).

func (*Share) PinLocked

func (s *Share) PinLocked(now time.Time) bool

PinLocked reports whether the PIN gate is shut because of wrong guesses.

func (*Share) StateAt added in v0.54.0

func (s *Share) StateAt(now time.Time) string

StateAt is where the link stands at now.

⚠ The same facts IsExpired judges a visit by, plus the one it cannot see (RevokedAt, filled by the listings only): "My shares" used to compare expires_at with the browser's clock, so a link whose downloads were used up still read as active and a skewed clock turned the label over early or late. Revoked wins over expired because revoking also sets the expiry to the same moment; a link revoked before 00053 has no RevokedAt and reads as expired, which is all that can be known about it.

type StagedUpload

type StagedUpload struct {
	ID            string    `json:"id"`
	StorageID     int64     `json:"storage_id"`
	StorageKey    string    `json:"storage_key"`
	UserID        int64     `json:"user_id"`
	TotalSize     int64     `json:"total_size"`
	ChunkSize     int64     `json:"chunk_size"`
	Mime          string    `json:"mime,omitempty"`
	Hash          string    `json:"hash,omitempty"`
	ReceivedBytes int64     `json:"received_bytes"`
	State         string    `json:"state"`
	Error         string    `json:"error,omitempty"`
	NodeID        *int64    `json:"node_id,omitempty"`
	OpID          *int64    `json:"op_id,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	ExpiresAt     time.Time `json:"expires_at"`
}

StagedUpload is one in-flight staged (resumable, driver-agnostic) upload.

It is the session record; the authority for what is physically staged is the manifest inside the staging directory (internal/staging). ReceivedBytes here mirrors the manifest's contiguous offset so listings and the sweeper do not have to open every manifest.

type Storage

type Storage struct {
	ID int64 `json:"id"`
	// UID is the address that does not move. The Name is what people read and
	// what the file protocols accept as the first path segment, which means
	// renaming a storage re-addresses it and every mount written against the
	// old name answers 404 (issue #21). The uid is assigned once, at creation,
	// and the protocols resolve it too — so a mount written against it
	// survives every rename. Empty only on a row from before the column
	// existed that the migration somehow did not reach.
	UID           string          `json:"uid,omitempty"`
	Name          string          `json:"name"`
	Driver        string          `json:"driver"`
	MountPath     string          `json:"mount_path"`
	ConfigJSON    json.RawMessage `json:"config"`
	SyncMode      SyncMode        `json:"sync_mode"`
	SyncIntervalS int             `json:"sync_interval_s"`
	LastSyncAt    *time.Time      `json:"last_sync_at,omitempty"`
	LastSyncToken string          `json:"last_sync_token,omitempty"`
	Enabled       bool            `json:"enabled"`
	ReadOnly      bool            `json:"read_only"`
	// RBACEnabled turns on per-user/per-item access control for this storage.
	// When false (default) the storage is visible to every authenticated user
	// and capability is governed purely by account role (RBAC-off passthrough,
	// preserving pre-00012 behavior). When true the storage is hidden by
	// default and only paths explicitly granted (directly or via an ancestor
	// folder) are visible; see internal/acl. Added in migration 00012.
	RBACEnabled bool      `json:"rbac_enabled"`
	CreatedAt   time.Time `json:"created_at"`
	// Replica pairing — `role` and `replica_of_id` are LEGACY columns
	// retained for backwards compatibility with v0.1.16 deployments
	// (SQLite can't DROP COLUMN cleanly). The current model lives in
	// `replica_target_id` — a foreign key into the new
	// `replication_targets` table. Set it via the Replikasyon page;
	// the wrapper Driver fan-outs writes to the linked target.
	Role            string `json:"role,omitempty"`
	ReplicaOfID     *int64 `json:"replica_of_id,omitempty"`
	ReplicaMode     string `json:"replica_mode,omitempty"`
	ReplicaTargetID *int64 `json:"replica_target_id,omitempty"`

	// SortOrder is the position the admin gave this storage (1 = first;
	// migration 00060, issue #57). NULL = not placed: listed after every
	// placed storage, in creation order. Written only by
	// Store.SetStorageOrder — CreateStorage leaves it NULL and UpdateStorage
	// never touches it. No omitempty: the admin list says null explicitly.
	SortOrder *int64 `json:"sort_order"`
}

Storage is a configured backend (local FS / S3 / SFTP / WebDAV / …).

type StorageCapabilities

type StorageCapabilities struct {
	Read bool `json:"read"`
	// Range reports ranged reads — the download endpoint answers 206 /
	// Content-Range for this storage instead of whole-object only.
	Range     bool `json:"range"`
	Write     bool `json:"write"`
	Move      bool `json:"move"`
	Copy      bool `json:"copy"`
	Delete    bool `json:"delete"`
	Mkdir     bool `json:"mkdir"`
	Presign   bool `json:"presign"`
	Multipart bool `json:"multipart"`
	Events    bool `json:"events"`
}

StorageCapabilities describes a single backend's optional features. Used by the frontend to decide whether to show the chunked upload UI, the browser-direct upload button, or the live event indicator.

type SyncConflict

type SyncConflict struct {
	ID           int64      `json:"id"`
	NodeID       *int64     `json:"node_id,omitempty"`
	StorageID    int64      `json:"storage_id"`
	StorageKey   string     `json:"storage_key,omitempty"`
	DBEtag       string     `json:"db_etag,omitempty"`
	BackendEtag  string     `json:"backend_etag,omitempty"`
	DBMtime      *time.Time `json:"db_mtime,omitempty"`
	BackendMtime *time.Time `json:"backend_mtime,omitempty"`
	DetectedAt   time.Time  `json:"detected_at"`
	ResolvedAt   *time.Time `json:"resolved_at,omitempty"`
	Resolution   string     `json:"resolution,omitempty"` // backend_wins, db_wins, manual
}

SyncConflict captures backend/DB drift requiring human attention.

type SyncMode

type SyncMode string

SyncMode describes how a Storage is synced into the DB cache.

const (
	SyncModePoll     SyncMode = "poll"     // periodic remote scan
	SyncModeFSNotify SyncMode = "fsnotify" // local FS event-driven
	SyncModeOnDemand SyncMode = "ondemand" // explicit user-triggered
	// SyncModeLazy catalogues a LOCAL storage folder by folder: the folder
	// somebody opens is listed from disk at once and catalogued first, the
	// rest in the background (or only as people visit it). Issue #45; see
	// docs/LAZY-CATALOGUE.md and internal/sync/lazy.go.
	SyncModeLazy SyncMode = "lazy"

	// SyncModePush was declared for "the backend pushes changes at us"
	// (a webhook receiver) and NOTHING WAS EVER BUILT BEHIND IT. The sync
	// worker has no `push` branch, so such a storage falls through to the
	// poll loop: it works, but it does not do what its own setting says.
	//
	// The constant stays so that legacy rows can still be recognised and
	// named in a log line. It is rejected on write (ValidateSyncMode) and
	// the worker warns about any row that still carries it.
	SyncModePush SyncMode = "push"
)

func SyncModes

func SyncModes() []SyncMode

SyncModes lists the modes the sync worker actually implements — the set an operator may choose from. Order is the order they appear in the docs.

func (SyncMode) Implemented

func (m SyncMode) Implemented() bool

Implemented reports whether the sync worker has a branch for this mode. An unimplemented mode is not a harmless label: the storage silently polls, so the operator reads their own configuration and believes something else is happening.

type SyncRun

type SyncRun struct {
	ID           int64      `json:"id"`
	StorageID    int64      `json:"storage_id"`
	StartedAt    time.Time  `json:"started_at"`
	FinishedAt   *time.Time `json:"finished_at,omitempty"`
	CursorBefore string     `json:"cursor_before,omitempty"`
	CursorAfter  string     `json:"cursor_after,omitempty"`
	SeenCount    int        `json:"seen_count"`
	Added        int        `json:"added"`
	Updated      int        `json:"updated"`
	Deleted      int        `json:"deleted"`
	Status       string     `json:"status"` // running, ok, partial, failed, aborted
	Error        string     `json:"error,omitempty"`
}

SyncRun is a row in sync_runs.

type SyncState

type SyncState string

SyncState describes per-node sync lifecycle.

const (
	SyncStateSynced  SyncState = "synced"
	SyncStateDirty   SyncState = "dirty"
	SyncStatePending SyncState = "pending"
	SyncStateError   SyncState = "error"
)

type Tag

type Tag struct {
	ID       int64  `json:"id"`
	Kind     string `json:"kind"`
	TenantID *int64 `json:"-"`
	OwnerID  *int64 `json:"-"`
	// Name is the display form, case preserved (tagname.Clean).
	Name string `json:"name"`
	// Key is the identity (tagname.Key) — never shown.
	Key       string    `json:"-"`
	CreatedAt time.Time `json:"-"`
}

Tag is one entry of a tag VOCABULARY — a person's (personal) or a tenant's (team). The files carrying it are linked through node_tags, so a tag is named once and renaming or re-casing it is one row, not one per file.

Exactly one of OwnerID / TenantID is set, and that is what makes it personal or team:

  • personal: OwnerID = the person, TenantID = nil.
  • team: OwnerID = nil, TenantID = the tenant (provider id), or 0 for "the instance" — a single-tenant install, or a storage linked to no tenant. A tenant-0 team tag follows the file: every tenant that can see the file sees it (see handlers/tags.go, tagVisible).

⚠ The two nullable columns are how uniqueness is expressed on all three engines: UNIQUE(owner_id, name_key) and UNIQUE(tenant_id, name_key), and a NULL never collides in a UNIQUE index — so a personal row is only unique among that person's tags and a team row only among that tenant's. MySQL has no partial index to say it any other way.

type TagPlacement

type TagPlacement struct {
	TagID     int64
	NodeID    int64
	StorageID int64
	Path      string
}

TagPlacement is one (tag, live file) pair with what visibility needs to judge it: the storage and the path. The tag listings are built from these so that a tag is only named to someone who can see at least one file carrying it — a tag NAME is information too.

type TagQuery

type TagQuery struct {
	// OwnerID > 0 includes that person's personal tags.
	OwnerID int64
	// Team includes team tags; TeamTenants, when non-nil, restricts them to
	// those tenant ids (0 included explicitly when wanted). nil = every tenant
	// — only ever for an unconfined caller (single-tenant mode, supertenant).
	Team        bool
	TeamTenants []int64
}

TagQuery selects part of the vocabulary. Both halves are optional and are ORed: a caller's visible vocabulary is "my personal tags + my tenant's team tags".

type ThumbCapabilities

type ThumbCapabilities struct {
	Image       bool `json:"image"`
	ImageMagick bool `json:"imagemagick"` // `magick`/`convert` binary present
	// HEIC: that ImageMagick DECODES a HEIC photo, measured by drawing a
	// sample (enginebin.HEIC), not inferred from ImageMagick being there:
	// libheif can be present without its HEVC decoder plugin (Ubuntu 24.04).
	HEIC   bool `json:"heic"`
	Video  bool `json:"video"`
	Audio  bool `json:"audio"`
	PDF    bool `json:"pdf"`
	Office bool `json:"office"`
	SVG    bool `json:"svg"`
}

ThumbCapabilities indicates which media types can be thumbnailed.

type Thumbnail

type Thumbnail struct {
	NodeID      int64      `json:"node_id"`
	State       string     `json:"state"` // pending, ready, failed, skipped
	StorageKey  string     `json:"storage_key,omitempty"`
	Width       int        `json:"width,omitempty"`
	Height      int        `json:"height,omitempty"`
	Error       string     `json:"error,omitempty"`
	GeneratedAt *time.Time `json:"generated_at,omitempty"`
	// SourceSig is the ContentFingerprint of the bytes the latest render
	// read (migration 00075). A row whose signature is not the node's
	// current one was drawn from other content: thumb.Assess re-renders it.
	// Empty on rows drawn before 0.50.
	SourceSig string `json:"source_sig,omitempty"`
	// AttemptedAt is when the latest render started, whatever it ended in.
	AttemptedAt *time.Time `json:"attempted_at,omitempty"`
	// Generator is who drew the picture: "builtin", or "app:<name>@<version>"
	// (migration 00077). Empty when nobody did, and on rows from before 0.50.
	Generator string `json:"generator,omitempty"`
	// Attempts is who was asked, in order, and what each answered: a JSON
	// list of {"h": handler, "r": "ok" | reason} (thumb.Attempt). Empty on
	// rows drawn before 0.50; "[]" when every handler was switched off.
	Attempts string `json:"attempts,omitempty"`
}

Thumbnail references a generated thumbnail asset.

type ThumbnailProblem added in v0.50.0

type ThumbnailProblem struct {
	NodeID      int64      `json:"node_id"`
	StorageID   int64      `json:"storage_id"`
	Path        string     `json:"path"`
	Name        string     `json:"name"`
	Size        int64      `json:"size"`
	State       string     `json:"state"`
	Error       string     `json:"error,omitempty"`
	AttemptedAt *time.Time `json:"attempted_at,omitempty"`
	// Attempts: see Thumbnail.Attempts.
	Attempts string `json:"attempts,omitempty"`
}

ThumbnailProblem is one file whose thumbnail failed or was skipped, as the repair tool lists them (Store.ListThumbnailProblems).

type UploadPart

type UploadPart struct {
	PartNumber int    `json:"part_number"`
	Etag       string `json:"etag"`
	Size       int64  `json:"size"`
	URL        string `json:"url,omitempty"` // only on init response
}

UploadPart represents one chunk of a multipart upload.

type User

type User struct {
	ID    int64  `json:"id"`
	Email string `json:"email"`
	// Username is the short login name (migration 00025), unique across the
	// install. It exists because the connection protocols cannot carry an
	// e-mail comfortably — `sftp://ada@example.com@host` needs escaping, and
	// rclone/WinSCP config files split on `@`. Every login surface accepts
	// EITHER this or the e-mail; internal/identity owns that rule and is the
	// only place allowed to decide which one an identifier is.
	Username          string   `json:"username"`
	DisplayName       string   `json:"display_name"`
	PasswordHash      string   `json:"-"`
	Role              string   `json:"role"`
	TOTPSecret        string   `json:"-"`
	TOTPPendingSecret string   `json:"-"`
	TOTPEnabled       bool     `json:"totp_enabled"`
	TOTPRecoveryCodes []string `json:"-"`
	Locale            string   `json:"locale"`
	Timezone          string   `json:"timezone"`
	// AvatarURL is the profile picture: a small data: URI or an http(s) /
	// site-relative URL (migration 00023). It is the face the collaboration
	// presence strip draws instead of initials, for every client of this
	// account — browser session, desktop app, and any API key minted under it.
	AvatarURL   string     `json:"avatar_url,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at"`
	LastLoginAt *time.Time `json:"last_login_at,omitempty"`
	// Multi-tenancy (docs/MULTI-TENANCY.md): the tenant (provider) this user
	// belongs to. Nil only on rows predating the 00014 backfill; assigned
	// immutably at JIT login. OIDCSubject pins the OIDC identity per provider.
	ProviderID  *int64 `json:"provider_id,omitempty"`
	OIDCSubject string `json:"-"`
	// OIDCIssuer is the `iss` of the SSO identity the account is bound to,
	// with OIDCSubject its `sub` (migration 00079): once bound, an SSO sign-in
	// finds the account by that pair, and the same e-mail address arriving
	// with another identity is refused (docs/SSO.md).
	OIDCIssuer string `json:"-"`
	// SSOLinked: the account is bound to an SSO identity (it has a subject).
	// Filled by the store from OIDCIssuer/OIDCSubject, which are never sent:
	// the users page offers "Remove SSO bind" on such an account.
	SSOLinked bool `json:"sso_linked,omitempty"`
	// Storage accounting (migration 00003). QuotaBytes == 0 means unlimited.
	// Carried on the user so an admin table costs one call instead of one
	// /quota request per row.
	QuotaBytes int64 `json:"quota_bytes"`
	UsageBytes int64 `json:"used_bytes"`
	// Enabled (migration 00022) gates whether the account may start a
	// session — local login, OIDC and /dav alike. Disabling is not a soft
	// delete: files, quota and grants are untouched.
	Enabled bool `json:"enabled"`
	// DisabledReason says why the SERVER switched the account off (migration
	// 00079): DisabledPendingApproval for an account an SSO sign-in opened
	// whose address the identity provider did not confirm, DisabledByDirectory
	// for one LDAP directory sync switched off. "" for an account that is on,
	// or that an administrator switched off; switching it on or off clears it.
	// The users page shows it beside the disabled state.
	DisabledReason string `json:"disabled_reason,omitempty"`
	// AuthSource says where the account comes from (migration 00084): one of
	// the AuthSource* values, or "" for an account from before it with no
	// password here and no SSO identity (its next sign-in labels it; any
	// LDAP directory may take it until then - ldap.notMine). A label for the
	// Users page; it decides only which LDAP directory signs the account in.
	AuthSource string `json:"auth_source"`
	// AuthDirectory names the LDAP provider that made the account — its
	// slug, "ldap" or another instance's (migration 00084); "" for any other
	// account. Only that directory signs it in (DirectoryOwner).
	AuthDirectory string `json:"auth_directory,omitempty"`
	// DirectoryID is the person's permanent id in that directory —
	// "<directory>:<entryUUID or objectGUID>" (migration 00085); "" when the
	// directory has none, or for any other account. A sign-in or sync finds
	// the account by it before the e-mail.
	DirectoryID string `json:"-"`
	// AdminByGroup: an administrator because a group gives its members
	// Administrator (migration 00086); back to their earlier level when no
	// group does. Any other change of role clears it.
	AdminByGroup bool `json:"admin_by_group,omitempty"`
}

User represents an authenticated principal.

func (*User) DirectoryOwner added in v0.52.0

func (u *User) DirectoryOwner() string

DirectoryOwner is the LDAP directory an account belongs to: the one that made it, and the main directory for an LDAP account from before users.auth_directory. "" for an account no directory made.

func (*User) DisabledByDirectory added in v0.52.0

func (u *User) DisabledByDirectory() bool

DisabledByDirectory reports whether directory sync switched the account off (and may switch it back on).

func (*User) HasPermission

func (u *User) HasPermission(perm string, granted []string) bool

HasPermission checks whether the user's role permits an action. `*` is a wildcard. Tested in roles.permissions_json.

func (*User) IsAdmin

func (u *User) IsAdmin() bool

func (*User) IsViewer

func (u *User) IsViewer() bool

IsViewer returns true if the user has the read-only viewer role.

func (*User) Label

func (u *User) Label() string

Label is PersonLabel for this account ("" for a nil user).

type VaultLock added in v0.54.0

type VaultLock struct {
	TenantID int64
	// VaultID is the key file's vault.id as 32 lower-case hex digits.
	VaultID string
	// Rev is the compare-and-set counter: every write of the row names the
	// revision it read, and loses when another writer came first.
	Rev int64
	// TokenHash is the hex SHA-256 of the live lock's token; "" when no lock
	// is recorded as held.
	TokenHash string
	// Holder: the person, the kind of client (web, desktop, cli, mount) and
	// the client's own short label ("Firefox, ofis").
	HolderUserID int64
	HolderName   string
	HolderClient string
	HolderLabel  string
	// StorageID and Path are where the lock was taken: the vault folder the
	// holder named (a copy of a vault shares its id, and so its lock).
	StorageID int64
	Path      string
	// TakenMs: when the lock was taken; LeaseMs: when it runs out unless
	// renewed; ActiveMs: the last vault write or active renewal; IdleSeconds:
	// the holder's idle time, read when the lock was taken.
	TakenMs     int64
	LeaseMs     int64
	ActiveMs    int64
	IdleSeconds int64
	// IndexStartedMs: an index write of the holder is running since then
	// (0 = none). The lock is not free again until it ends or 60 seconds
	// pass.
	IndexStartedMs int64
	// FirstGen and LastGen are the first and last generation committed
	// under the current (or, once it ended, the last) lock.
	FirstGen int64
	LastGen  int64
	// EndedTokenHash, EndedReason and EndedMs record how the last lock
	// ended: released, expired, idle, broken or locked_idle. The holder of
	// that token is told the reason on its next call - and, for a break,
	// EndedBy: the name of who broke it.
	EndedTokenHash string
	EndedReason    string
	EndedBy        string
	EndedMs        int64
}

VaultLock is one row of vault_locks (migration 00096, internal/vaultlock, docs/E2E-VAULT-FORMAT.md → The write lock): the write lock of one vault, by tenant and vault id. The row outlives the locks taken on it - it says who held the vault last, and how that ended - so a lock is held exactly while TokenHash is set and the clock says so (vaultlock.Service decides).

Every time is Unix MILLISECONDS in an integer column: the lease is a minute long and is compared on every vault write, on three engines that store and compare timestamps three different ways.

type WebhookTarget

type WebhookTarget struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	URL       string    `json:"url"`
	Secret    string    `json:"-"`
	Events    string    `json:"events"`
	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`

	// Lang is the language the target's `title` and `body` are said in
	// (migration 00105): a webhook is a receiver no person stands behind, so
	// it has the language chosen for it here, and "" is the instance's
	// (FILEX_DEFAULT_LOCALE, else English). notify say.go.
	Lang string `json:"lang"`

	// Last-delivery persistence (migration 00019). All nil until the
	// first delivery attempt (real dispatch or admin test-fire).
	// LastStatus is the HTTP status code of the final attempt — 0 means
	// the request never got a response (DNS/connect/timeout). LastError
	// is nil after a success, the aggregated error message otherwise.
	LastStatus     *int       `json:"last_status,omitempty"`
	LastError      *string    `json:"last_error,omitempty"`
	LastDeliveryAt *time.Time `json:"last_delivery_at,omitempty"`
}

WebhookTarget is one row of webhook_targets (webhook v2, migration 00017) — an additional POST destination next to the legacy single global webhook. Events is a comma-separated allow-list of event names (mirrors APIToken.Usernames' CSV precedent); empty means "all events".

Secret is never serialized — admin API responses expose only a secret_set flag. It signs each delivery body as X-Filex-Signature: sha256=<hex hmac-sha256(body)>.

func (*WebhookTarget) EventList

func (t *WebhookTarget) EventList() []string

EventList splits the CSV allow-list into trimmed, non-empty names.

func (*WebhookTarget) MatchesEvent

func (t *WebhookTarget) MatchesEvent(event string) bool

MatchesEvent reports whether the target wants deliveries for the given event name. An empty allow-list matches everything.

Jump to

Keyboard shortcuts

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