Documentation
¶
Index ¶
- Constants
- Variables
- func LocalizedText(base string, texts map[string]string, lang string) string
- func NewStorageUID() string
- func NormalizeTokenKind(s string) string
- func PersonLabel(displayName, username, email string) string
- func TransferLanded(n *Node, size int64, mtime time.Time) bool
- func ValidE2EPolicy(s string) bool
- func ValidE2ERequestKind(s string) bool
- func ValidGrantLevel(s string) bool
- func ValidPermEffect(s string) bool
- func ValidRole(name string) bool
- func ValidTagKind(k string) bool
- func ValidateSyncMode(m SyncMode) error
- func ValidateSyncModeFor(m SyncMode, driver string) error
- type APIToken
- func (t *APIToken) DefaultUsername() string
- func (t *APIToken) HasScope(want string) bool
- func (t *APIToken) IsApp() bool
- func (t *APIToken) PermLevel(key string) tokenperm.Level
- func (t *APIToken) ResolveUsername(requested string) (string, bool)
- func (t *APIToken) UsernameList() []string
- func (t *APIToken) WithPermissions() *APIToken
- type AppPlugin
- type AppPluginJob
- type AppPluginLock
- type AppPluginOverride
- type AppPluginScheduleItem
- type AppPluginSigningKey
- type AppPluginStateFile
- type AppPluginVersion
- type AppPluginsCapabilities
- type AppThumbLimits
- type AuditEntry
- type AuthBinding
- type AuthInstance
- type BroadcastFilter
- type Capabilities
- type CatalogueCounts
- type CatalogueFolder
- type CatalogueFolderFilter
- type CatalogueFolderState
- type ChunkedUpload
- type CustomTheme
- type DigestFilter
- type DigestPolicy
- type DomainCheck
- type Draft
- type E2ERequest
- type E2ERequestFilter
- type ExternalServiceState
- type FileAssociation
- type FileGrant
- type Group
- type GroupLink
- type GroupMember
- type LoginThrottle
- type NFSExport
- type NameMatch
- type Node
- type NodeComment
- type NodeType
- type NodeVersion
- type Notification
- type NotificationInput
- type NotificationOpen
- type NotificationSettings
- type NotificationTarget
- type NotificationTargetKind
- type OfficeSession
- type PermRuleConditions
- type PermRuleSettings
- type PermRuleTarget
- type PermissionRule
- type Plugin
- type PluginRequest
- type Provider
- type ProviderDomain
- type ProviderE2E
- type ProviderStorage
- type RecentSearch
- type ReplicaFailure
- type ReplicaInitialCopy
- type ReplicaLink
- type ReplicaRule
- type ReplicaRuleInput
- type ReplicaSettings
- type ReplicaStatusReport
- type ReplicationTarget
- type Role
- type S3AccessKey
- type SSHPublicKey
- type Session
- type Share
- type StagedUpload
- type Storage
- type StorageCapabilities
- type SyncConflict
- type SyncMode
- type SyncRun
- type SyncState
- type Tag
- type TagPlacement
- type TagQuery
- type ThumbCapabilities
- type Thumbnail
- type ThumbnailProblem
- type UploadPart
- type User
- type WebhookTarget
Constants ¶
const ( AppPluginSourceUpload = "upload" AppPluginSourceURL = "url" AppPluginSourceGitHub = "github" AppPluginSourceBundle = "bundle" )
App plugin sources — where the module came from (migration 00042).
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).
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.
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).
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).
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).
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).
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).
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.
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.
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.
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.
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).
const ( AuthTypeOIDC = "oidc" AuthTypeLocal = "local" )
Auth types a provider (tenant) can use to authenticate its users.
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).
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).
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).
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).
const ( )
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).
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
const GroupDirectoryRemoved = "removed"
GroupDirectoryRemoved is a synced group whose directory group is gone.
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).
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.
const MainDirectory = "ldap"
MainDirectory is the provider name of the first LDAP directory.
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.
const RecentSearchKeep = 20
RecentSearchKeep is how many searches a person's list keeps per surface; writing one more drops the oldest.
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.
const RecentSearchSurfaceAdmin = "admin"
RecentSearchSurfaceAdmin is the admin panel's search (docs/ADMIN-PANEL.md → Search).
const SettingE2EPolicy = "e2e.policy"
SettingE2EPolicy holds a single-tenant install's policy. A multi-tenant install reads each tenant's providers.e2e_policy instead.
const SettingPermissionAppDefaults = "permissions.app_defaults"
SettingPermissionAppDefaults holds the built-in roles' decisions about app permissions (perm/app.go): {"user": {"app.sign.request": "deny"}, …}.
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.
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.
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.
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.
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.
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.
const TokenSourceDesktop = "desktop"
TokenSourceDesktop marks a token minted by the desktop app's pairing (handlers/desktop_auth.go).
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 ¶
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
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 ¶
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 ¶
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 ¶
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
ValidE2EPolicy reports whether s is one of the four policies.
func ValidE2ERequestKind ¶ added in v0.51.0
ValidE2ERequestKind reports whether s is one of the three request kinds.
func ValidGrantLevel ¶
ValidGrantLevel reports whether s is a known grant level.
func ValidPermEffect ¶ added in v0.49.0
ValidPermEffect reports whether s is a storable effect.
func ValidRole ¶
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 ¶
ValidTagKind reports whether k is one of the two kinds.
func ValidateSyncMode ¶
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
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 ¶
DefaultUsername is the identity used when the caller doesn't pick one: the first configured username, else the token's label.
func (*APIToken) HasScope ¶
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 ¶
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
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 ¶
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 ¶
UsernameList returns the parsed, trimmed username allow-list (may be empty).
func (*APIToken) WithPermissions ¶ added in v0.53.0
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.
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.
type GroupLink ¶ added in v0.50.0
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).
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.
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"`
// 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 *time.Time `json:"unavailable_at,omitempty"`
// the row, so a JSON reader (the AI/MCP answers) has a flag to test.
Unavailable bool `json:"unavailable,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
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 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"`
// 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"`
}
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 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" // 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 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 {
// with no expiry.
ShareLinkMaxDays *int `json:"share_link_max_days,omitempty"`
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
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):
- file data — storage confinement (a provider only reaches its linked storages); a bug here is the only way file data could cross a tenant.
- 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
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 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 ¶ added in v0.53.0
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.
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 {
// 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.
// 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.
// 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.
// Drop-link fields (Kind == ShareKindDrop). Empty/zero for a normal
// download share.
// ── 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.
// contract.pdf"). Host-owned; the plugin supplies it at creation.
Subject string `json:"subject,omitempty"`
// replaced by the share_state host function. Never serialized: it is the
// app's own bookkeeping, not the visitor's business.
StateJSON string `json:"-"`
// [{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:"-"`
// (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:"-"`
// (`<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.
// (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"`
}
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 ¶
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 ¶
IsApp reports whether an app plugin answers this link — the visitor gets that app's surface instead of a download or a folder listing.
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 ¶
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 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 ¶
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
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
DisabledByDirectory reports whether directory sync switched the account off (and may switch it back on).
func (*User) HasPermission ¶
HasPermission checks whether the user's role permits an action. `*` is a wildcard. Tested in roles.permissions_json.
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"`
// 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.
Source Files
¶
- app_plugin.go
- auth_instance.go
- capability.go
- catalogue.go
- comment.go
- customtheme.go
- draft.go
- e2e_policy.go
- file_association.go
- grant.go
- group.go
- login_throttle.go
- nfsexport.go
- node.go
- notification.go
- office_session.go
- permission.go
- plugin.go
- plugin_request.go
- provider.go
- provider_domain.go
- recent_search.go
- replica.go
- s3key.go
- share.go
- sshkey.go
- staged_upload.go
- storage.go
- tag.go
- user.go