Documentation
¶
Overview ¶
Package db defines a Driver interface that wraps a *sql.DB and exposes the typed Store interface used by the rest of the codebase.
Each concrete driver (sqlite, mysql, postgres) lives under db/drivers/ and adapts dialect-specific quirks (placeholder syntax, RETURNING support, JSON column type) into the same Store interface.
Migrations are embedded per-dialect via embed.FS and run by goose.
Index ¶
- Constants
- Variables
- func AuditActionPrefixes(action string) ([]string, bool)
- func BeginOwnTx(ctx context.Context, pool *sql.DB) (*sql.Tx, error)
- func CatalogueFolderDepth(p string) int
- func CatalogueFolderPath(dir string) string
- func CatalogueSubtreeRange(dir string) (lo, hi string, ok bool)
- func CatalogueTime(t *time.Time) any
- func CollectPermOverrides(rows *sql.Rows, err error) (map[int64]map[string]string, error)
- func CollectPermRules(rows *sql.Rows, err error) ([]*model.PermissionRule, error)
- func CollectStrings(rows *sql.Rows, err error) ([]string, error)
- func CollectUserCustomRoles(rows *sql.Rows, err error) (map[int64]int64, error)
- func CustomRoleOrNone(id int64, err error) (int64, error)
- func CustomThemeJSON(t *model.CustomTheme) (light string, dark string, err error)
- func DecodePermConditions(s string) (model.PermRuleConditions, error)
- func DecodePermList(v sql.NullString) ([]string, error)
- func DecodePermOverrides(s string) (map[string]string, error)
- func DecodePermRule(r *model.PermissionRule, targets, effects, settings string) error
- func DecodePermTexts(s string) (map[string]string, error)
- func DollarPlaceholders(q string) string
- func EncodePermConditions(c model.PermRuleConditions) (string, error)
- func EncodePermList(list []string) (any, error)
- func EncodePermOverrides(m map[string]string) (string, error)
- func EncodePermRule(r *model.PermissionRule) (targets, effects, settings string, err error)
- func EncodePermRuleAll(r *model.PermissionRule) (targets, effects, settings, conditions string, err error)
- func EncodePermRuleTexts(r *model.PermissionRule) (names, descriptions string, err error)
- func EncodePermTexts(m map[string]string) (string, error)
- func EnsureNotEmpty(dsn string, hint string) error
- func IDList(ids []int64, first int, ph func(n int) string) (string, []any)
- func InTx(ctx context.Context, pool *sql.DB) bool
- func Migrate(ctx context.Context, drv Driver, sqlDB *sql.DB) error
- func MigrateDown(ctx context.Context, drv Driver, sqlDB *sql.DB) error
- func MigrateStatus(ctx context.Context, drv Driver, sqlDB *sql.DB) error
- func Names() []string
- func NotInDraftsSQL(col string) string
- func OverridesOrEmpty(raw string, err error) (map[string]string, error)
- func PlainTime(t *time.Time) any
- func PlatformSubdomain(p *model.Provider) string
- func PrefixChars(p string) int
- func ProviderRealmOnCreate(p *model.Provider) any
- func RealmOfPlatformSubdomain(host string) string
- func Register(name string, f Factory)
- func ReplaceUserSSOGroups(ctx context.Context, pool *sql.DB, del, ins string, userID int64, ...) error
- func ResolveExtraHost(ctx context.Context, s extraHostStore, host string) (*model.Provider, error)
- func RunInTx(ctx context.Context, pool *sql.DB, fn func(ctx context.Context) error) error
- func ScanCatalogueFolder(r RowScanner) (*model.CatalogueFolder, error)
- func ScanCustomTheme(rows *sql.Rows) *model.CustomTheme
- func ScanCustomThemes(rows *sql.Rows) ([]*model.CustomTheme, error)
- func ScanPermRule(r RowScanner) (*model.PermissionRule, error)
- func SetTenantDomain(d string)
- func SubtreePrefixVariants(paths []string) []string
- func SubtreeSuffix(p string, prefixes []string) string
- func TagQueryWhere(q model.TagQuery, alias string, first int, ph func(n int) string) (string, []any)
- func TenantDomain() string
- type AppLink
- type AuditEntryWithUser
- type CatalogueDialect
- type CatalogueFolderSQL
- func (c *CatalogueFolderSQL) CountCatalogueFolders(ctx context.Context, storageID int64) (model.CatalogueCounts, error)
- func (c *CatalogueFolderSQL) DeleteCatalogueFoldersUnder(ctx context.Context, storageID int64, dir string) error
- func (c *CatalogueFolderSQL) DiscoverCatalogueFolders(ctx context.Context, storageID int64, folders []model.CatalogueFolder) error
- func (c *CatalogueFolderSQL) GetCatalogueFolder(ctx context.Context, storageID int64, pathHash string) (*model.CatalogueFolder, error)
- func (c *CatalogueFolderSQL) HasUncataloguedUnder(ctx context.Context, storageID int64, dir string) (bool, error)
- func (c *CatalogueFolderSQL) ListCatalogueFolders(ctx context.Context, storageID int64, f model.CatalogueFolderFilter) ([]*model.CatalogueFolder, error)
- func (c *CatalogueFolderSQL) RecordCatalogueFolder(ctx context.Context, f *model.CatalogueFolder) error
- func (c *CatalogueFolderSQL) ResetCatalogueWatches(ctx context.Context, storageID int64) (int64, error)
- func (c *CatalogueFolderSQL) SetCatalogueFolderWatch(ctx context.Context, storageID int64, pathHash string, watchedAt *time.Time, ...) error
- func (c *CatalogueFolderSQL) TouchCatalogueFolderVisit(ctx context.Context, storageID int64, pathHash string, at time.Time) error
- type DraftSQL
- func (d *DraftSQL) CountLiveDrafts(ctx context.Context, userID int64) (int, error)
- func (d *DraftSQL) CreateDraft(ctx context.Context, in *model.Draft) (*model.Draft, error)
- func (d *DraftSQL) DeleteDraft(ctx context.Context, id int64) error
- func (d *DraftSQL) GetDraftByKey(ctx context.Context, key string) (*model.Draft, error)
- func (d *DraftSQL) GetDraftByNode(ctx context.Context, nodeID int64) (*model.Draft, error)
- func (d *DraftSQL) ListLiveDrafts(ctx context.Context, userID int64) ([]*model.Draft, error)
- type Driver
- type DuplicateNode
- type ExternalService
- type Factory
- type FileAssocSQL
- func (f *FileAssocSQL) DeleteAppThumbLimits(ctx context.Context, pluginID int64) error
- func (f *FileAssocSQL) DeleteFileAssociation(ctx context.Context, capability, ext string) (bool, error)
- func (f *FileAssocSQL) GetAppThumbLimits(ctx context.Context, pluginID int64) (*model.AppThumbLimits, error)
- func (f *FileAssocSQL) ListFileAssociations(ctx context.Context) ([]*model.FileAssociation, error)
- func (f *FileAssocSQL) ListUserPrefs(ctx context.Context, userID int64) ([]UserPrefsDoc, error)
- func (f *FileAssocSQL) PutAppThumbLimits(ctx context.Context, l *model.AppThumbLimits) error
- func (f *FileAssocSQL) PutFileAssociation(ctx context.Context, a *model.FileAssociation) error
- func (f *FileAssocSQL) ThumbnailGenerators(ctx context.Context, storageIDs []int64) (map[string]int64, error)
- type GroupSQL
- func (g *GroupSQL) AddGroupMember(ctx context.Context, groupID, userID int64) error
- func (g *GroupSQL) CreateGroup(ctx context.Context, gr *model.Group) (*model.Group, error)
- func (g *GroupSQL) CreateGroupFileGrant(ctx context.Context, gr *model.FileGrant) (*model.FileGrant, error)
- func (g *GroupSQL) DeleteGroup(ctx context.Context, id int64) error
- func (g *GroupSQL) DeleteGroupFileGrant(ctx context.Context, id int64) error
- func (g *GroupSQL) DeleteUserGroupLevel(ctx context.Context, userID int64) error
- func (g *GroupSQL) DropForeignMemberships(ctx context.Context, userID, providerID int64) error
- func (g *GroupSQL) GetGroup(ctx context.Context, id int64) (*model.Group, error)
- func (g *GroupSQL) GetGroupFileGrant(ctx context.Context, id int64) (*model.FileGrant, error)
- func (g *GroupSQL) GetUserGroupLevel(ctx context.Context, userID int64) (string, bool, error)
- func (g *GroupSQL) ListAllGroupFileGrants(ctx context.Context) ([]*model.FileGrant, error)
- func (g *GroupSQL) ListAllGroupMembers(ctx context.Context) ([]*model.GroupMember, error)
- func (g *GroupSQL) ListGroupFileGrantsByGroup(ctx context.Context, groupID int64) ([]*model.FileGrant, error)
- func (g *GroupSQL) ListGroupFileGrantsByStorage(ctx context.Context, storageID int64) ([]*model.FileGrant, error)
- func (g *GroupSQL) ListGroupFileGrantsByStorageUser(ctx context.Context, storageID, userID int64) ([]*model.FileGrant, error)
- func (g *GroupSQL) ListGroupMembers(ctx context.Context, groupID int64) ([]*model.GroupMember, error)
- func (g *GroupSQL) ListGroups(ctx context.Context) ([]*model.Group, error)
- func (g *GroupSQL) ListUserGroupMemberships(ctx context.Context, userID int64) ([]*model.GroupMember, error)
- func (g *GroupSQL) ReassignGroupRole(ctx context.Context, from, to int64) error
- func (g *GroupSQL) RemoveGroupMember(ctx context.Context, groupID, userID int64) error
- func (g *GroupSQL) SetUserGroupLevel(ctx context.Context, userID int64, level string) error
- func (g *GroupSQL) SetUserLinkedGroups(ctx context.Context, userID int64, source string, groupIDs []int64) (added, removed []int64, err error)
- func (g *GroupSQL) UpdateGroup(ctx context.Context, gr *model.Group) error
- func (g *GroupSQL) UpdateGroupFileGrantLevel(ctx context.Context, id int64, level string) error
- type LoginThrottleSQL
- func (p *LoginThrottleSQL) DeleteLoginThrottle(ctx context.Context, scope, subject string) (bool, error)
- func (p *LoginThrottleSQL) GetLoginThrottle(ctx context.Context, scope, subject string) (*model.LoginThrottle, error)
- func (p *LoginThrottleSQL) ListLoginThrottles(ctx context.Context, scope string, lockedAt *time.Time, limit int) ([]*model.LoginThrottle, error)
- func (p *LoginThrottleSQL) LoadLoginThrottles(ctx context.Context, since time.Time, limit int) ([]*model.LoginThrottle, error)
- func (p *LoginThrottleSQL) PruneLoginThrottles(ctx context.Context, before time.Time) (int64, error)
- func (p *LoginThrottleSQL) SaveLoginThrottle(ctx context.Context, t *model.LoginThrottle) error
- type NodeAgg
- type NodeDeletedBySQL
- type NodeUnavailableSQL
- func (o *NodeUnavailableSQL) ClearNodeUnavailable(ctx context.Context, id int64) (bool, error)
- func (o *NodeUnavailableSQL) MarkNodeUnavailable(ctx context.Context, id int64, reason string) (bool, error)
- func (o *NodeUnavailableSQL) UnavailableAt(ctx context.Context, storageID int64, rel string) (*UnavailableEntry, error)
- type OIDCIdentitySQL
- func (o *OIDCIdentitySQL) ClearUserOIDCIdentity(ctx context.Context, userID int64) (bool, error)
- func (o *OIDCIdentitySQL) GetUserByOIDCIdentity(ctx context.Context, t UserTenant, issuer, subject string) (*model.User, error)
- func (o *OIDCIdentitySQL) GetUserInTenantByEmail(ctx context.Context, t UserTenant, email string) (*model.User, error)
- func (o *OIDCIdentitySQL) SetProviderOIDCTrustEmail(ctx context.Context, providerID int64, trust bool) error
- func (o *OIDCIdentitySQL) SetUserDisabledReason(ctx context.Context, userID int64, reason string) error
- func (o *OIDCIdentitySQL) SetUserOIDCIdentity(ctx context.Context, userID int64, issuer, subject string) error
- type PluginRequestSQL
- func (p *PluginRequestSQL) CreatePluginRequest(ctx context.Context, in *model.PluginRequest) (*model.PluginRequest, error)
- func (p *PluginRequestSQL) GetPluginRequest(ctx context.Context, id int64) (*model.PluginRequest, error)
- func (p *PluginRequestSQL) ListPluginRequests(ctx context.Context, status string, limit int) ([]*model.PluginRequest, error)
- func (p *PluginRequestSQL) PendingPluginRequestBySource(ctx context.Context, sourceKey string) (*model.PluginRequest, error)
- func (p *PluginRequestSQL) UpdatePluginRequest(ctx context.Context, r *model.PluginRequest, onlyIfPending bool) (bool, error)
- type Querier
- type RowScanner
- type ShareWithMeta
- type StorageOrderSQL
- type Store
- type TenantAuthSQL
- func (s *TenantAuthSQL) BindAuthInstance(ctx context.Context, providerID, instanceID int64, source string) error
- func (s *TenantAuthSQL) CreateAuthInstance(ctx context.Context, a *model.AuthInstance) (*model.AuthInstance, error)
- func (s *TenantAuthSQL) CreateProviderDomain(ctx context.Context, d *model.ProviderDomain) (*model.ProviderDomain, error)
- func (s *TenantAuthSQL) DeleteAuthInstance(ctx context.Context, id int64) error
- func (s *TenantAuthSQL) DeleteProviderDomain(ctx context.Context, id int64) error
- func (s *TenantAuthSQL) GetAuthInstance(ctx context.Context, id int64) (*model.AuthInstance, error)
- func (s *TenantAuthSQL) GetAuthInstanceBySlug(ctx context.Context, slug string) (*model.AuthInstance, error)
- func (s *TenantAuthSQL) GetProviderDomain(ctx context.Context, domain string) (*model.ProviderDomain, error)
- func (s *TenantAuthSQL) GetProviderDomainByID(ctx context.Context, id int64) (*model.ProviderDomain, error)
- func (s *TenantAuthSQL) ListAuthBindings(ctx context.Context) ([]*model.AuthBinding, error)
- func (s *TenantAuthSQL) ListAuthInstances(ctx context.Context) ([]*model.AuthInstance, error)
- func (s *TenantAuthSQL) ListProviderDomains(ctx context.Context, providerID int64) ([]*model.ProviderDomain, error)
- func (s *TenantAuthSQL) ProviderIDByActiveDomain(ctx context.Context, domain string) (int64, error)
- func (s *TenantAuthSQL) SetProviderAllowInsecureAuth(ctx context.Context, providerID int64, allow bool) error
- func (s *TenantAuthSQL) SetProviderDomainCert(ctx context.Context, id int64, certPEM, keySealed string, notAfter *time.Time) error
- func (s *TenantAuthSQL) SetProviderDomainStatus(ctx context.Context, id int64, status string, why model.DomainCheck, ...) error
- func (s *TenantAuthSQL) UnbindAuthInstance(ctx context.Context, providerID, instanceID int64) error
- func (s *TenantAuthSQL) UpdateAuthInstance(ctx context.Context, a *model.AuthInstance) error
- type TrashTally
- type UnavailableEntry
- type UserPrefsDoc
- type UserTenant
- type VanishedSQL
Constants ¶
const CatalogueFolderColumns = `storage_id, path_hash, path, depth, state, reconciled_at, visited_at, watched_at, reconcile_on_open, entries, held_back`
CatalogueFolderColumns is the select list both drivers use, in the order ScanCatalogueFolder reads them.
const CustomThemeColumns = `id, theme_key, name, tokens_light, tokens_dark, created_at, updated_at`
CustomThemeColumns is the select list both drivers use, in the order ScanCustomTheme reads them.
⚠ The order is a contract between this constant and the function below. They are next to each other so a column added to one is added to the other in the same edit; split across packages, a mismatch would scan the name into the light-token field and fail with a JSON error naming neither.
const PermRuleColumns = `` /* 188-byte string literal not displayed */
PermRuleColumns is the select list both drivers use for permission_rules, in the order ScanPermRule reads them. ⚠ The order is a contract with the function below — change them together.
UnavailableReasonMax bounds what is kept of a storage's answer: it is shown to a person and written to a log, and a backend error can be a page long.
Variables ¶
var ErrNestedTx = errors.New("db: this operation opens its own transaction and cannot run inside another")
ErrNestedTx refuses a store method that opens a transaction of its own while the context already carries one on the same database. On SQLite it would wait for the connection the outer transaction holds, for ever.
var ErrNoRows = sql.ErrNoRows
ErrNoRows is returned by Store methods when a single-row query has no result.
Functions ¶
func AuditActionPrefixes ¶
AuditActionPrefixes reads ListAuditFiltered's action filter: a value ending in "." ("user.") asks for every action of that resource, and a comma- separated list of them ("app_plugin.,app-plugins.") for every action of any of them — one resource can be written under two spellings (a handler's own row and the admin route's generic one). The answer is one LIKE pattern per prefix, with `\` as the escape character, so the underscore in "auth_provider." is a literal underscore. ok=false means an exact action (or no filter at all).
⚠ The Audit page's action filter was a free-text box matched EXACTLY against wire names ("user.create"): a person typing what the page shows them ("Kullanıcı") found nothing. The page now offers the resources by name and sends their prefixes.
func BeginOwnTx ¶ added in v0.45.0
BeginOwnTx is BeginTx for a store method that commits on its own: refused (ErrNestedTx) inside a transaction RunInTx opened on the same database.
func CatalogueFolderDepth ¶ added in v0.44.2
CatalogueFolderDepth is how many segments deep a canonical folder path is (the root is 0).
func CatalogueFolderPath ¶ added in v0.44.2
CatalogueFolderPath is the canonical spelling of a catalogue folder: "/" for the storage root, "/a/b" below it — the spelling the walk stores in nodes.
func CatalogueSubtreeRange ¶ added in v0.44.2
CatalogueSubtreeRange is the byte range holding every canonical path strictly below dir: (lo, hi) exclusive. The migration makes `path` a byte-ordered column on every engine, and '0' is the byte after '/', so `path > lo AND path < hi` is exactly "starts with dir/" — "/a/b.txt" and "/a/bc" fall outside, and the index answers it. ok is false for the root, where every other row is below.
func CatalogueTime ¶ added in v0.44.2
CatalogueTime is how the SQLite/MySQL driver binds a catalogue timestamp: UTC, second precision, in CURRENT_TIMESTAMP's own spelling — so a bound value and a stored one compare as the instants they are (see ListStaleNodes for what an RFC 3339 binding did to that comparison).
func CollectPermOverrides ¶ added in v0.49.0
CollectPermOverrides scans (user_id, overrides_json) rows into a map and closes them.
func CollectPermRules ¶ added in v0.49.0
CollectPermRules scans every row of a PermRuleColumns query and closes it.
func CollectStrings ¶ added in v0.49.0
CollectStrings scans a one-string-column result set and closes it; an empty set is an empty (not nil) slice.
func CollectUserCustomRoles ¶ added in v0.49.0
CollectUserCustomRoles scans (user_id, role_id) rows into a map and closes them.
func CustomRoleOrNone ¶ added in v0.49.0
CustomRoleOrNone turns a single role_id lookup into 0 for "no row".
func CustomThemeJSON ¶
func CustomThemeJSON(t *model.CustomTheme) (light string, dark string, err error)
CustomThemeJSON renders a theme's two token maps for storage.
func DecodePermConditions ¶ added in v0.49.0
func DecodePermConditions(s string) (model.PermRuleConditions, error)
DecodePermConditions parses a conditions column ("" and "{}" are none).
func DecodePermList ¶ added in v0.49.0
func DecodePermList(v sql.NullString) ([]string, error)
DecodePermList parses a permissions_json column into a non-nil list (possibly empty); NULL is empty.
func DecodePermOverrides ¶ added in v0.49.0
DecodePermOverrides parses an overrides / effects column. Empty text is an empty map (a MySQL JSON column never is, but a hand-edited row might be).
func DecodePermRule ¶ added in v0.49.0
func DecodePermRule(r *model.PermissionRule, targets, effects, settings string) error
DecodePermRule fills a rule's structured fields from its three columns.
func DecodePermTexts ¶ added in v0.49.0
DecodePermTexts parses a names / descriptions column. Empty text and {} are nil, so a rule without translations reads back exactly as one written before the column existed.
func DollarPlaceholders ¶ added in v0.44.2
DollarPlaceholders numbers every `?` of a statement as PostgreSQL's $N. ⚠ For statements with no `?` inside a string literal — true of every statement in this file.
func EncodePermConditions ¶ added in v0.49.0
func EncodePermConditions(c model.PermRuleConditions) (string, error)
EncodePermConditions renders a rule's conditions column.
func EncodePermList ¶ added in v0.49.0
EncodePermList renders a role's permission list; nil is an empty one.
func EncodePermOverrides ¶ added in v0.49.0
EncodePermOverrides renders an overrides / effects map. nil encodes as {}.
func EncodePermRule ¶ added in v0.49.0
func EncodePermRule(r *model.PermissionRule) (targets, effects, settings string, err error)
EncodePermRule renders a rule's three JSON columns.
func EncodePermRuleAll ¶ added in v0.49.0
func EncodePermRuleAll(r *model.PermissionRule) (targets, effects, settings, conditions string, err error)
EncodePermRuleAll renders all four of a rule's JSON columns.
func EncodePermRuleTexts ¶ added in v0.49.0
func EncodePermRuleTexts(r *model.PermissionRule) (names, descriptions string, err error)
EncodePermRuleTexts renders a rule's two translation columns.
func EncodePermTexts ¶ added in v0.49.0
EncodePermTexts renders a rule's names / descriptions column (migration 00071): language -> text. nil encodes as {}.
func EnsureNotEmpty ¶
EnsureNotEmpty surfaces the common "DSN missing" error consistently.
func MigrateDown ¶
MigrateDown rolls back one step.
func MigrateStatus ¶
MigrateStatus prints status to stdout.
func NotInDraftsSQL ¶ added in v0.48.0
NotInDraftsSQL is the WHERE condition "the node at column col is not inside the drafts area", in both path spellings the drivers store (`/a/b` and `a/b`). The storage's counts (StorageStats) leave drafts out the way they leave the trash out: a draft is somebody's unfinished document, not a file of the storage yet (issue #71).
⚠ Safe to splice: syspath.Drafts is plain — no `%`, `_`, `\` or quote — so the pattern matches only itself (syspath's tests hold every internal name to that, for notify's LIKE patterns).
func OverridesOrEmpty ¶ added in v0.49.0
OverridesOrEmpty turns the GetUserPermissionOverrides scan result into the Store contract: no row is an empty map, not an error.
func PlainTime ¶ added in v0.44.2
PlainTime binds a timestamp as the instant it is, in UTC (PostgreSQL).
func PlatformSubdomain ¶ added in v0.50.0
PlatformSubdomain is a tenant's address under the tenant domain, "" when there is none (no tenant domain, the platform's own tenant, no realm).
func PrefixChars ¶ added in v0.47.0
PrefixChars is the length SUBSTR needs for a path prefix: SQL counts CHARACTERS (SQLite, MySQL and PostgreSQL alike), Go's len counts bytes. Passing len() made every folder whose path is not plain ASCII match none of its own rows — "/Müşteri/" is 9 characters and 11 bytes — so the folder went to the trash and its contents stayed live, and a restore left them in the trash.
func ProviderRealmOnCreate ¶ added in v0.50.0
ProviderRealmOnCreate is the value a new tenant row's realm column gets (migration 00073), for every engine's CreateProvider: the realm the caller gave, else the slug — the same default the API offers and the migration used for the tenants that existed before it — lower-cased; NULL for a provider created as the supertenant, which has no realm.
It does not validate: the API checks a realm before it gets here (tenant.CheckRealm), and a caller that creates rows directly (the cloud signup, a test) passes a slug that is already realm-shaped.
⚠ There is deliberately no counterpart for UpdateProvider. A realm is set once and never rewritten (package tenant).
func RealmOfPlatformSubdomain ¶ added in v0.50.0
RealmOfPlatformSubdomain returns the realm a host names when it is a platform subdomain (`acme.tenants.files.example` → `acme`), "" otherwise. Only one label sits in front of the tenant domain.
func ReplaceUserSSOGroups ¶ added in v0.49.0
func ReplaceUserSSOGroups(ctx context.Context, pool *sql.DB, del, ins string, userID int64, groups []string) error
ReplaceUserSSOGroups deletes a user's groups and inserts the given ones in one transaction — blanks and duplicates dropped. del takes (user_id), ins takes (user_id, group_name), each in the caller's dialect. A context that already carries a transaction on pool runs in that one (RunInTx).
func ResolveExtraHost ¶ added in v0.50.0
ResolveExtraHost answers steps 2 and 3 above for a host no provider's `host` claims: the enabled tenant an active own domain or a platform subdomain names, or (nil, nil).
func RunInTx ¶ added in v0.45.0
RunInTx runs fn in one transaction on pool: every statement fn runs through a Store on pool with the context it is handed is part of it. It commits when fn returns nil and rolls back otherwise (fn's error is returned). A context already carrying a transaction on pool runs fn in that one.
⚠ Do not keep fn's context: once RunInTx returns, a statement run with it fails (sql.ErrTxDone).
func ScanCatalogueFolder ¶ added in v0.44.2
func ScanCatalogueFolder(r RowScanner) (*model.CatalogueFolder, error)
ScanCatalogueFolder reads one row in CatalogueFolderColumns order.
func ScanCustomTheme ¶
func ScanCustomTheme(rows *sql.Rows) *model.CustomTheme
ScanCustomTheme turns the current row into a model, or nil when the row cannot be read.
⚠ Unparseable is nil, NOT an error. This runs on the public appearance path that the login page and every share page read; a row hand-edited into the database, or written by a future version with a shape this one cannot read, must cost the operator that one theme rather than the whole instance.
func ScanCustomThemes ¶
func ScanCustomThemes(rows *sql.Rows) ([]*model.CustomTheme, error)
ScanCustomThemes drains a result set, dropping rows that cannot be read.
func ScanPermRule ¶ added in v0.49.0
func ScanPermRule(r RowScanner) (*model.PermissionRule, error)
ScanPermRule reads one permission_rules row selected with PermRuleColumns.
func SetTenantDomain ¶ added in v0.50.0
func SetTenantDomain(d string)
SetTenantDomain sets the installation's tenant domain (FILEX_TENANT_DOMAIN): `tenants.files.example` gives every tenant the address `<realm>.tenants.files.example`. "" switches platform subdomains off. Set once at boot (server.New); tests set and reset it.
⚠ A process-wide value and not a Store field: the store is wrapped and handed around (identitystore, quotastore, tenantstore) long before anyone could thread a setting through every wrapper, and every one of them must answer the same.
func SubtreePrefixVariants ¶ added in v0.47.0
SubtreePrefixVariants normalizes candidate folder paths into the two on-disk conventions the nodes.path column historically mixes (with and without a leading slash), each with a trailing "/" so only strict descendants match. The storage root is never a subtree.
func SubtreeSuffix ¶ added in v0.47.0
SubtreeSuffix strips the first of prefixes p starts with off p ("" when none matches).
func TagQueryWhere ¶
func TagQueryWhere(q model.TagQuery, alias string, first int, ph func(n int) string) (string, []any)
TagQueryWhere renders a model.TagQuery as a SQL predicate over the `tags` table aliased `alias`, with the placeholder syntax the caller's engine uses (`?` for SQLite/MySQL, `$n` for Postgres — `ph(n)` is given the 1-based position counting from `first`).
ONE builder for both store implementations. The predicate is the visibility boundary between one person's tags and another's, and between one tenant's and another's; two hand-written copies of it are two chances for the engines to disagree about who sees a tag.
An empty query (no owner, no team) renders "1=0": a caller that asked for nothing gets nothing, never "no filter".
func TenantDomain ¶ added in v0.50.0
func TenantDomain() string
TenantDomain returns the installation's tenant domain, "" for none.
Types ¶
type AppLink ¶
type AppLink struct {
Plugin string `json:"plugin"`
Page string `json:"page"`
// Label names the kind of link ({lang: …}): "Signing request".
Label map[string]string `json:"label"`
// Revoke is what revoking it does ({lang: …}); empty when the page says
// nothing, and the list then asks its ordinary question.
Revoke map[string]string `json:"revoke,omitempty"`
// View / Section: where in the app the row opens (`/app/<plugin>/<view>
// ?section=`). Empty View when the app has no home page to open.
View string `json:"view,omitempty"`
Section string `json:"section,omitempty"`
}
AppLink is what a list of links needs to show an app's link for what it is (wire.PagePurpose): the owner's decision of 2026-09-21 — a signing link in My shares is marked as a signing request, opens the request's page in the app, and its revoke says that it cancels the request.
type AuditEntryWithUser ¶
type AuditEntryWithUser struct {
Entry *model.AuditEntry `json:"entry"`
UserEmail string `json:"user_email,omitempty"`
// TargetName is the thing the row is about, in words (a user's e-mail,
// a storage's name, a file's path) — filled by the audit handler, not
// by the query (handlers/audit_targets.go).
TargetName string `json:"target_name,omitempty"`
// UserName is who acted, as every screen names a person
// (model.PersonLabel), filled by the audit handler — never by the store.
UserName string `json:"user_name,omitempty"`
}
AuditEntryWithUser is an audit row joined with the user.email column for nicer admin UI rendering.
type CatalogueDialect ¶ added in v0.44.2
type CatalogueDialect struct {
// Placeholders turns a statement written with `?` into the engine's
// (PostgreSQL: $1, $2 …; DollarPlaceholders).
Placeholders func(q string) string
// Upsert rewrites SQLite's `ON CONFLICT (…) DO UPDATE SET` tail for the
// engine (MySQL: ON DUPLICATE KEY UPDATE). PostgreSQL speaks SQLite's.
Upsert func(q string) string
// Time binds a timestamp (nil for NULL): CatalogueTime for SQLite and
// MySQL, which compare stored timestamps as text; PlainTime for
// PostgreSQL.
Time func(t *time.Time) any
}
CatalogueDialect is what the engines disagree on for these statements. A nil function is the identity (SQLite's own spelling).
type CatalogueFolderSQL ¶ added in v0.44.2
type CatalogueFolderSQL struct {
DB *sql.DB
Dialect CatalogueDialect
}
CatalogueFolderSQL implements the catalogue_folders half of db.Store.
func (*CatalogueFolderSQL) CountCatalogueFolders ¶ added in v0.44.2
func (c *CatalogueFolderSQL) CountCatalogueFolders(ctx context.Context, storageID int64) (model.CatalogueCounts, error)
func (*CatalogueFolderSQL) DeleteCatalogueFoldersUnder ¶ added in v0.44.2
func (*CatalogueFolderSQL) DiscoverCatalogueFolders ¶ added in v0.44.2
func (c *CatalogueFolderSQL) DiscoverCatalogueFolders(ctx context.Context, storageID int64, folders []model.CatalogueFolder) error
func (*CatalogueFolderSQL) GetCatalogueFolder ¶ added in v0.44.2
func (c *CatalogueFolderSQL) GetCatalogueFolder(ctx context.Context, storageID int64, pathHash string) (*model.CatalogueFolder, error)
func (*CatalogueFolderSQL) HasUncataloguedUnder ¶ added in v0.44.2
func (*CatalogueFolderSQL) ListCatalogueFolders ¶ added in v0.44.2
func (c *CatalogueFolderSQL) ListCatalogueFolders(ctx context.Context, storageID int64, f model.CatalogueFolderFilter) ([]*model.CatalogueFolder, error)
func (*CatalogueFolderSQL) RecordCatalogueFolder ¶ added in v0.44.2
func (c *CatalogueFolderSQL) RecordCatalogueFolder(ctx context.Context, f *model.CatalogueFolder) error
func (*CatalogueFolderSQL) ResetCatalogueWatches ¶ added in v0.44.2
func (*CatalogueFolderSQL) SetCatalogueFolderWatch ¶ added in v0.44.2
func (*CatalogueFolderSQL) TouchCatalogueFolderVisit ¶ added in v0.44.2
type DraftSQL ¶ added in v0.48.0
type DraftSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
}
DraftSQL implements the drafts half of Store (migration 00064, issue #71), written ONCE for every engine. Each driver embeds a *DraftSQL in its Store, so the methods are promoted and there is no per-driver copy to drift — the arrangement of StorageOrderSQL and NodeDeletedBySQL, and for the same reason (the duplication gate in web/tests/quality).
See db/migrations/sqlite/00064_drafts.sql for the model and internal/drafts for the rules; this only reads and writes the rows.
func (*DraftSQL) CountLiveDrafts ¶ added in v0.48.0
CountLiveDrafts counts a person's drafts whose file is live.
func (*DraftSQL) CreateDraft ¶ added in v0.48.0
CreateDraft inserts a draft row and returns it as the store reads it back.
func (*DraftSQL) DeleteDraft ¶ added in v0.48.0
DeleteDraft removes a draft row. Its file is the caller's business.
func (*DraftSQL) GetDraftByKey ¶ added in v0.48.0
GetDraftByKey returns the draft with that key, live or not.
func (*DraftSQL) GetDraftByNode ¶ added in v0.48.0
GetDraftByNode returns the draft whose file is that node, live or not.
type Driver ¶
type Driver interface {
Name() string
Open(ctx context.Context, dsn string) (*sql.DB, error)
MigrationsFS() embed.FS // embed.FS rooted at the dialect's migration directory
Dialect() string // goose-compatible: "sqlite3", "postgres", "mysql"
NewStore(db *sql.DB) Store
}
Driver describes a database engine integration.
type DuplicateNode ¶
type DuplicateNode struct {
ID int64
StorageID int64
Path string
Name string
Size int64
Etag string
}
DuplicateNode is one row of the duplicate-file report query — a slim projection of nodes (no timestamps/sync state) since the report only needs identity + grouping fields.
type ExternalService ¶
type ExternalService struct {
Name string
Enabled bool
URL string
SecretEnc string
OptionsJSON string
LastCheck *time.Time
LastState string
}
ExternalService is the DB row representation. Lives in the db package so model can stay pure-domain.
type FileAssocSQL ¶ added in v0.50.0
type FileAssocSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
// Time binds a timestamp: CatalogueTime for SQLite and MySQL, PlainTime
// for PostgreSQL. Nil = CatalogueTime.
Time func(t *time.Time) any
}
FileAssocSQL implements the file-association half of Store (migration 00077, internal/assoc): the administrator's rule per kind and capability, and the per-app thumbnail limits. Written ONCE for every engine and embedded in each driver's Store - the arrangement of PluginRequestSQL, for the same reason (the duplication gate in web/tests/quality).
⚠ An upsert is a delete and an insert in one transaction rather than each engine's own ON CONFLICT / ON DUPLICATE KEY: the three spellings differ, and a table this small does not need them.
func (*FileAssocSQL) DeleteAppThumbLimits ¶ added in v0.50.0
func (f *FileAssocSQL) DeleteAppThumbLimits(ctx context.Context, pluginID int64) error
DeleteAppThumbLimits removes one app's limits (the app is removed).
func (*FileAssocSQL) DeleteFileAssociation ¶ added in v0.50.0
func (f *FileAssocSQL) DeleteFileAssociation(ctx context.Context, capability, ext string) (bool, error)
DeleteFileAssociation removes the rule for (capability, ext): the kind keeps the default order again. ok = there was one.
func (*FileAssocSQL) GetAppThumbLimits ¶ added in v0.50.0
func (f *FileAssocSQL) GetAppThumbLimits(ctx context.Context, pluginID int64) (*model.AppThumbLimits, error)
GetAppThumbLimits answers (nil, nil) when the app has none stored.
func (*FileAssocSQL) ListFileAssociations ¶ added in v0.50.0
func (f *FileAssocSQL) ListFileAssociations(ctx context.Context) ([]*model.FileAssociation, error)
ListFileAssociations returns every rule, by capability then kind.
func (*FileAssocSQL) ListUserPrefs ¶ added in v0.50.0
func (f *FileAssocSQL) ListUserPrefs(ctx context.Context, userID int64) ([]UserPrefsDoc, error)
ListUserPrefs returns every preference document one person stored (00047), newest first, each with when it was last written.
It rides on FileAssocSQL, the shared statement set every driver embeds, because it serves the same 0.50 feature: the account-wide "open with" choices (handlers/openwith.go) are adopted once from the most recently written surface document, and only this listing knows which one that is. One statement for every engine: it only reads, and `?` is rebound for PostgreSQL.
func (*FileAssocSQL) PutAppThumbLimits ¶ added in v0.50.0
func (f *FileAssocSQL) PutAppThumbLimits(ctx context.Context, l *model.AppThumbLimits) error
PutAppThumbLimits creates or replaces one app's limits.
func (*FileAssocSQL) PutFileAssociation ¶ added in v0.50.0
func (f *FileAssocSQL) PutFileAssociation(ctx context.Context, a *model.FileAssociation) error
PutFileAssociation creates or replaces the rule for (Capability, Ext).
func (*FileAssocSQL) ThumbnailGenerators ¶ added in v0.50.0
func (f *FileAssocSQL) ThumbnailGenerators(ctx context.Context, storageIDs []int64) (map[string]int64, error)
ThumbnailGenerators counts the ready thumbnails of live files by who drew them, within storageIDs (nil: every storage, empty: none).
type GroupSQL ¶ added in v0.50.0
type GroupSQL struct {
// contains filtered or unexported fields
}
GroupSQL is the group store for one connection.
func NewGroupSQL ¶ added in v0.50.0
NewGroupSQL returns the group store for conn. dollar is true for PostgreSQL: `$n` placeholders, BOOLEAN columns and INSERT … RETURNING.
func (*GroupSQL) AddGroupMember ¶ added in v0.50.0
AddGroupMember puts a person in a group by hand. A membership a link made becomes a manual one — the person now stays whatever the directory says.
func (*GroupSQL) CreateGroup ¶ added in v0.50.0
CreateGroup stores a new group and returns it as stored.
func (*GroupSQL) CreateGroupFileGrant ¶ added in v0.50.0
func (g *GroupSQL) CreateGroupFileGrant(ctx context.Context, gr *model.FileGrant) (*model.FileGrant, error)
CreateGroupFileGrant upserts a group's grant on the (storage_id, path_prefix, group_id) key, like CreateFileGrant does a person's.
func (*GroupSQL) DeleteGroup ¶ added in v0.50.0
DeleteGroup deletes a group; its memberships and grants go with it (ON DELETE CASCADE — SQLite runs with foreign_keys on).
func (*GroupSQL) DeleteGroupFileGrant ¶ added in v0.50.0
DeleteGroupFileGrant revokes a group grant.
func (*GroupSQL) DeleteUserGroupLevel ¶ added in v0.50.0
DeleteUserGroupLevel forgets it — the level is the account's own again.
func (*GroupSQL) DropForeignMemberships ¶ added in v0.50.0
DropForeignMemberships takes a person out of every group of a tenant other than providerID — what moving an account to another tenant does. The drivers' SetUserProvider calls it, so every path that re-homes an account does. Install-wide groups (no tenant) keep them.
func (*GroupSQL) GetGroupFileGrant ¶ added in v0.50.0
GetGroupFileGrant returns one group grant, or sql.ErrNoRows.
func (*GroupSQL) GetUserGroupLevel ¶ added in v0.50.0
GetUserGroupLevel returns the built-in level an account had before a group's role moved it, and whether one is recorded.
func (*GroupSQL) ListAllGroupFileGrants ¶ added in v0.50.0
ListAllGroupFileGrants returns every group grant.
func (*GroupSQL) ListAllGroupMembers ¶ added in v0.50.0
ListAllGroupMembers returns every membership, in (group, user) order — what the permission snapshot reads once for the whole install.
func (*GroupSQL) ListGroupFileGrantsByGroup ¶ added in v0.50.0
func (g *GroupSQL) ListGroupFileGrantsByGroup(ctx context.Context, groupID int64) ([]*model.FileGrant, error)
ListGroupFileGrantsByGroup returns one group's grants, on every storage.
func (*GroupSQL) ListGroupFileGrantsByStorage ¶ added in v0.50.0
func (g *GroupSQL) ListGroupFileGrantsByStorage(ctx context.Context, storageID int64) ([]*model.FileGrant, error)
ListGroupFileGrantsByStorage returns every group grant on one storage.
func (*GroupSQL) ListGroupFileGrantsByStorageUser ¶ added in v0.50.0
func (g *GroupSQL) ListGroupFileGrantsByStorageUser(ctx context.Context, storageID, userID int64) ([]*model.FileGrant, error)
ListGroupFileGrantsByStorageUser returns the grants on one storage of every group the person is in — what they reach through their groups.
func (*GroupSQL) ListGroupMembers ¶ added in v0.50.0
func (g *GroupSQL) ListGroupMembers(ctx context.Context, groupID int64) ([]*model.GroupMember, error)
ListGroupMembers returns one group's members, in user id order.
func (*GroupSQL) ListGroups ¶ added in v0.50.0
ListGroups returns every group, in id order.
func (*GroupSQL) ListUserGroupMemberships ¶ added in v0.50.0
func (g *GroupSQL) ListUserGroupMemberships(ctx context.Context, userID int64) ([]*model.GroupMember, error)
ListUserGroupMemberships returns the groups one person is in, in group id order.
func (*GroupSQL) ReassignGroupRole ¶ added in v0.50.0
ReassignGroupRole gives every group holding role `from` the role `to` instead (0: no role) — what deleting a role does to its groups.
func (*GroupSQL) RemoveGroupMember ¶ added in v0.50.0
RemoveGroupMember takes a person out of a group, whatever put them there.
func (*GroupSQL) SetUserGroupLevel ¶ added in v0.50.0
SetUserGroupLevel records the level an account had before a group's role moved it. The first record stands: a second move is not "before".
func (*GroupSQL) SetUserLinkedGroups ¶ added in v0.50.0
func (g *GroupSQL) SetUserLinkedGroups(ctx context.Context, userID int64, source string, groupIDs []int64) (added, removed []int64, err error)
SetUserLinkedGroups makes groupIDs the groups a person is in through one kind of link (source): rows of that source not in groupIDs are removed, missing ones added. A membership of any other source — "manual" above all — is never touched, and is not duplicated. It reports the group ids that were added and removed.
func (*GroupSQL) UpdateGroup ¶ added in v0.50.0
UpdateGroup rewrites a group's name, description, role, priority and links — never its tenant, which is fixed at creation. sql.ErrNoRows for an unknown id; a name another group of the tenant has is the engine's unique error.
type LoginThrottleSQL ¶ added in v0.50.0
type LoginThrottleSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
// Time binds a timestamp: CatalogueTime for SQLite and MySQL (they compare
// stored timestamps as text), PlainTime for PostgreSQL. Nil = CatalogueTime.
Time func(t *time.Time) any
}
LoginThrottleSQL implements the login_throttle half of Store (migration 00072, internal/loginguard), written ONCE for every engine and embedded in each driver's Store — the arrangement of PluginRequestSQL and DraftSQL.
It only reads and writes counter rows; what a count means (the limits, the windows, the lock arithmetic) is internal/loginguard's.
func (*LoginThrottleSQL) DeleteLoginThrottle ¶ added in v0.50.0
func (p *LoginThrottleSQL) DeleteLoginThrottle(ctx context.Context, scope, subject string) (bool, error)
DeleteLoginThrottle removes one counter and reports whether there was one.
func (*LoginThrottleSQL) GetLoginThrottle ¶ added in v0.50.0
func (p *LoginThrottleSQL) GetLoginThrottle(ctx context.Context, scope, subject string) (*model.LoginThrottle, error)
GetLoginThrottle returns one counter, or (nil, nil) when nothing has been counted for that subject.
func (*LoginThrottleSQL) ListLoginThrottles ¶ added in v0.50.0
func (p *LoginThrottleSQL) ListLoginThrottles(ctx context.Context, scope string, lockedAt *time.Time, limit int) ([]*model.LoginThrottle, error)
ListLoginThrottles returns counters, the most recently active first, at most limit of them (<= 0: 200; never more than 1000). scope "" = every scope. With lockedAt non-nil only the rows locked at that instant.
func (*LoginThrottleSQL) LoadLoginThrottles ¶ added in v0.50.0
func (p *LoginThrottleSQL) LoadLoginThrottles(ctx context.Context, since time.Time, limit int) ([]*model.LoginThrottle, error)
LoadLoginThrottles returns the counters that still have something to say at since — a failure at or after it, or a lock that runs past it (exactly the rows PruneLoginThrottles(since) keeps) — the most recently active first, at most limit of them (<= 0: all). Unlike ListLoginThrottles it has no ceiling of its own: it is read once, at start, to fill the limiter's memory, and the limiter passes the size of that memory.
func (*LoginThrottleSQL) PruneLoginThrottles ¶ added in v0.50.0
func (p *LoginThrottleSQL) PruneLoginThrottles(ctx context.Context, before time.Time) (int64, error)
PruneLoginThrottles deletes counters that have had nothing to say since before: no failure after it and no lock still running. It answers how many.
func (*LoginThrottleSQL) SaveLoginThrottle ¶ added in v0.50.0
func (p *LoginThrottleSQL) SaveLoginThrottle(ctx context.Context, t *model.LoginThrottle) error
SaveLoginThrottle writes a counter, creating the row on first sight. The (scope, subject) pair is the key; ID is ignored.
⚠ UPDATE-then-INSERT and not an upsert, because the two upsert spellings differ per engine and this file is written once. Two writers racing on a brand-new subject can both reach the INSERT; the loser's unique-key error is answered by looking the row up — it exists, the other writer's count stands.
type NodeDeletedBySQL ¶ added in v0.47.0
type NodeDeletedBySQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
}
NodeDeletedBySQL implements Store.SetNodeDeletedBy (migration 00061), written ONCE for every engine. Each driver embeds a *NodeDeletedBySQL in its Store, so the method is promoted and there is no per-driver copy to drift — the arrangement of StorageOrderSQL and CatalogueFolderSQL (lesson #499). PR #64 wrote it twice, identical but for the placeholders, and only the duplication gate's driver-pair entry kept that green.
func (*NodeDeletedBySQL) SetNodeDeletedBy ¶ added in v0.47.0
SetNodeDeletedBy names who put a trashed row in the trash — the row, and for a folder in the trash every trashed row under its path that names nobody yet: its contents, which SoftDeleteAndRetag moved under the same trash key. A live row is left alone, and so is a row another delete already named.
⚠ The contents only when the folder's path IS a trash key (syspath.InTrash). A folder soft-deleted where it stood — a driver that could not trash it, so its bytes were deleted outright and its row kept its path — took none of its rows along: what is trashed under that path is older tombstones (the scanner found those files gone, nobody deleted them), and naming this person on them credited them with deletes they never made.
⚠ The subtree is matched with SUBSTR on the CHARACTER count, not LIKE: a `%` or `_` in a folder name is data, and see PrefixChars for why len() is wrong.
type NodeUnavailableSQL ¶ added in v0.50.0
type NodeUnavailableSQL struct {
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
}
NodeUnavailableSQL implements Store.MarkNodeUnavailable, Store.ClearNodeUnavailable and Store.UnavailableAt (migration 00078, issue #104), written ONCE for every engine. Each driver embeds a *NodeUnavailableSQL in its Store, so the methods are promoted and there is no per-driver copy to drift (the arrangement of NodeDeletedBySQL).
An "unavailable" row is a live row the storage could not answer for: the sync asked whether its object still exists and got neither "yes" nor "not found". It stays in the catalogue, is listed with a warning, and nothing may be done to it or below it until the storage answers again.
func (*NodeUnavailableSQL) ClearNodeUnavailable ¶ added in v0.50.0
ClearNodeUnavailable lifts the mark from row id and reports whether it carried one.
func (*NodeUnavailableSQL) MarkNodeUnavailable ¶ added in v0.50.0
func (o *NodeUnavailableSQL) MarkNodeUnavailable(ctx context.Context, id int64, reason string) (bool, error)
MarkNodeUnavailable records reason on the live row id and reports whether that changed anything: false when the row already carried the same reason (the sync asks on every pass, and only a change is worth a log line), or is not a live row.
func (*NodeUnavailableSQL) UnavailableAt ¶ added in v0.50.0
func (o *NodeUnavailableSQL) UnavailableAt(ctx context.Context, storageID int64, rel string) (*UnavailableEntry, error)
UnavailableAt answers whether rel, on storageID, is unavailable: whether its own live row, or the live row of any folder above it, carries a mark. The shallowest such row is returned, nil when there is none.
One indexed query, whatever the depth: every ancestor is looked up by its path hash ((storage_id, path_hash) is unique for live rows), which is also how both spellings a path column carries ("/a/b", "a/b") are matched.
type OIDCIdentitySQL ¶ added in v0.50.0
type OIDCIdentitySQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
// GetUser reads one account with the driver's own column list.
GetUser func(ctx context.Context, id int64) (*model.User, error)
}
OIDCIdentitySQL implements the account lookups and writes an OIDC sign-in makes (migration 00079, docs/SSO.md "Which account an SSO sign-in opens"), written ONCE for every engine. Each driver embeds a *OIDCIdentitySQL in its Store, so the methods are promoted and there is no per-driver copy to drift.
⚠⚠ Every lookup here stays inside ONE tenant (UserTenant). An OIDC sign-in used to find its account by e-mail across the whole platform (Store.GetUserByEmail), so an identity provider bound to one tenant could sign a person in to another tenant's account that had the same address. There is deliberately no unscoped variant.
func (*OIDCIdentitySQL) ClearUserOIDCIdentity ¶ added in v0.50.0
ClearUserOIDCIdentity removes an account's SSO bind (issuer and subject), and reports whether it had one: its next SSO sign-in is matched by its address again, as a first sign-in is (docs/SSO.md, "Removing an account's SSO bind"). An administrator's repair when the identity provider gave the person a new identity, or a new issuer address.
func (*OIDCIdentitySQL) GetUserByOIDCIdentity ¶ added in v0.50.0
func (o *OIDCIdentitySQL) GetUserByOIDCIdentity(ctx context.Context, t UserTenant, issuer, subject string) (*model.User, error)
GetUserByOIDCIdentity is the account of the tenant bound to the identity (issuer, subject), or (nil, nil) when none is. The comparison is exact on every engine: an identity provider's `sub` is case-sensitive (on MySQL the columns are utf8mb4_bin for it, migration 00079).
func (*OIDCIdentitySQL) GetUserInTenantByEmail ¶ added in v0.50.0
func (o *OIDCIdentitySQL) GetUserInTenantByEmail(ctx context.Context, t UserTenant, email string) (*model.User, error)
GetUserInTenantByEmail is the account of the tenant with this e-mail address, or (nil, nil) when the tenant has none: an account of another tenant with the address is not found.
func (*OIDCIdentitySQL) SetProviderOIDCTrustEmail ¶ added in v0.50.0
func (o *OIDCIdentitySQL) SetProviderOIDCTrustEmail(ctx context.Context, providerID int64, trust bool) error
SetProviderOIDCTrustEmail is a tenant's own OIDC's "trust this provider's email addresses" (providers.oidc_trust_email, migration 00079). Its own statement: UpdateProvider never writes it, so a caller that rebuilds a provider from a form cannot switch it by accident.
func (*OIDCIdentitySQL) SetUserDisabledReason ¶ added in v0.50.0
func (o *OIDCIdentitySQL) SetUserDisabledReason(ctx context.Context, userID int64, reason string) error
SetUserDisabledReason switches an account off and records why the server did (model.DisabledPendingApproval). An administrator switching the account on or off (Store.SetUserEnabled) clears the reason.
func (*OIDCIdentitySQL) SetUserOIDCIdentity ¶ added in v0.50.0
func (o *OIDCIdentitySQL) SetUserOIDCIdentity(ctx context.Context, userID int64, issuer, subject string) error
SetUserOIDCIdentity binds an account to the identity (issuer, subject). The unique index (provider_id, oidc_issuer, oidc_subject) refuses a second account of the same tenant bound to the same identity.
type PluginRequestSQL ¶ added in v0.49.0
type PluginRequestSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
// Time binds a timestamp: CatalogueTime for SQLite and MySQL (they compare
// stored timestamps as text), PlainTime for PostgreSQL. Nil = CatalogueTime.
Time func(t *time.Time) any
}
PluginRequestSQL implements the plugin_requests half of Store (migration 00070, internal/pluginreq), written ONCE for every engine and embedded in each driver's Store — the arrangement of DraftSQL, for the same reason (the duplication gate in web/tests/quality).
It only reads and writes rows; what a request means, and when one may move from one state to another, is internal/pluginreq's.
func (*PluginRequestSQL) CreatePluginRequest ¶ added in v0.49.0
func (p *PluginRequestSQL) CreatePluginRequest(ctx context.Context, in *model.PluginRequest) (*model.PluginRequest, error)
CreatePluginRequest inserts a request and returns it as the store reads it back. A request without a key is given one.
func (*PluginRequestSQL) GetPluginRequest ¶ added in v0.49.0
func (p *PluginRequestSQL) GetPluginRequest(ctx context.Context, id int64) (*model.PluginRequest, error)
GetPluginRequest returns one request, sql.ErrNoRows when there is none.
func (*PluginRequestSQL) ListPluginRequests ¶ added in v0.49.0
func (p *PluginRequestSQL) ListPluginRequests(ctx context.Context, status string, limit int) ([]*model.PluginRequest, error)
ListPluginRequests returns the requests in one state ("" = every state), the newest first, at most limit of them (<= 0: 500).
func (*PluginRequestSQL) PendingPluginRequestBySource ¶ added in v0.49.0
func (p *PluginRequestSQL) PendingPluginRequestBySource(ctx context.Context, sourceKey string) (*model.PluginRequest, error)
PendingPluginRequestBySource returns the pending request for a source key, sql.ErrNoRows when there is none. The newest wins if a race left two.
func (*PluginRequestSQL) UpdatePluginRequest ¶ added in v0.49.0
func (p *PluginRequestSQL) UpdatePluginRequest(ctx context.Context, r *model.PluginRequest, onlyIfPending bool) (bool, error)
UpdatePluginRequest writes a request's decision fields — status, who decided and when, the note and the result. With onlyIfPending the write happens only while the stored row is still pending, and ok reports whether it did: two administrators closing one request cannot both win.
type Querier ¶ added in v0.45.0
type Querier interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
Querier is what one statement needs. *sql.DB and *sql.Tx are both one.
type RowScanner ¶ added in v0.44.2
RowScanner is the one method ScanCatalogueFolder needs from *sql.Row or *sql.Rows.
type ShareWithMeta ¶
type ShareWithMeta struct {
// CreatorName is the creator as every screen names a person
// (model.PersonLabel), filled by the admin handler — never by the store.
// Empty for an ordinary share. It is joined in rather than derived by the
// caller so the admin Shares table can carry a "plugin / page" column
// without a lookup per row.
PluginName string `json:"plugin_name,omitempty"`
// admin handler from the configured public origin — never by the store.
// The admin Shares page used to build it from the browser's address, so an
// operator signed in on localhost copied a localhost link (issue #32).
URL string `json:"url,omitempty"`
// by the handlers from the app's manifest — never by the store. Nil for
// an ordinary share and for a page that declares no purpose.
App *AppLink `json:"app,omitempty"`
}
ShareWithMeta is the admin-list row that joins shares + creator email + node path + storage name so the admin UI doesn't have to issue N follow-up queries.
type StorageOrderSQL ¶ added in v0.46.0
type StorageOrderSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
}
StorageOrderSQL implements Store.SetStorageOrder (migration 00060, issue #57), written ONCE for every engine. Each driver embeds a *StorageOrderSQL in its Store, so the method is promoted and there is no per-driver copy to drift (the same arrangement as CatalogueFolderSQL, and for the same reason: the duplication gate in web/tests/quality).
func (*StorageOrderSQL) SetStorageOrder ¶ added in v0.46.0
func (o *StorageOrderSQL) SetStorageOrder(ctx context.Context, ordered []int64, cleared []int64) error
SetStorageOrder gives ordered[i] position i+1 and clears every id in cleared, in one transaction (or in the caller's, when ctx carries one on this database), so no reader ever sees half an order. An id that names no row changes nothing; the caller decides which ids are valid.
⚠ Clearing runs first, so an id in both lists ends up placed.
type Store ¶
type Store interface {
// Lifecycle
Ping(ctx context.Context) error
Close() error
// WithTx runs fn in ONE transaction: every statement a Store method runs
// with the context fn is handed is part of it — through the wrappers
// (quotastore, identitystore) too, since they pass the context on. It
// commits when fn returns nil and rolls back otherwise. See tx.go for
// what must NOT run inside it (anything reaching the database through
// another handle, the job queue above all).
WithTx(ctx context.Context, fn func(ctx context.Context) error) error
// Storages
CreateStorage(ctx context.Context, s *model.Storage) (*model.Storage, error)
GetStorage(ctx context.Context, id int64) (*model.Storage, error)
GetStorageByName(ctx context.Context, name string) (*model.Storage, error)
// GetStorageByUID resolves a storage by the identifier it keeps for life.
// The file protocols accept it in place of the name so a mount survives a
// rename; see model.Storage.UID.
GetStorageByUID(ctx context.Context, uid string) (*model.Storage, error)
// ListStorages and ListEnabledStorages list in the admin's order: placed
// storages by sort_order, then unplaced ones by id (model.Storage.SortOrder).
ListStorages(ctx context.Context) ([]*model.Storage, error)
ListEnabledStorages(ctx context.Context) ([]*model.Storage, error)
// UpdateStorage writes every column the edit form owns — not sort_order.
UpdateStorage(ctx context.Context, s *model.Storage) error
UpdateStorageSyncCursor(ctx context.Context, id int64, at time.Time, token string) error
DeleteStorage(ctx context.Context, id int64) error
// SetStorageOrder places ordered[i] at position i+1 and clears (NULL) the
// position of every id in cleared, in one transaction (issue #57;
// StorageOrderSQL). It does not check the ids: the caller decides which
// storages it may order.
SetStorageOrder(ctx context.Context, ordered []int64, cleared []int64) error
// Nodes
CreateNode(ctx context.Context, n *model.Node) (*model.Node, error)
GetNode(ctx context.Context, id int64) (*model.Node, error)
GetNodeByPath(ctx context.Context, storageID int64, pathHash string) (*model.Node, error)
// GetNodeByPathIncludingDeleted answers "is there ANY row at this path",
// live or trashed. The sync worker asks it so that an object appearing
// where a trashed row still sits is reported rather than silently
// conflated with it.
//
// Since migration 00032 the unique index over (storage_id, path_hash) is
// partial on live rows, so several trashed rows may share a path with at
// most one live one. Implementations MUST return the live row when there
// is one, and otherwise the most recently trashed row -- an arbitrary
// pick would make the sync's decision depend on row order.
GetNodeByPathIncludingDeleted(ctx context.Context, storageID int64, pathHash string) (*model.Node, error)
// ListLiveNodesInTrash returns every LIVE node of a storage whose path is
// the trash bucket or sits inside it. Nothing should ever be live in
// there: it is either a trashed item an older sync worker un-deleted, or
// a row that same worker minted for the trash's own bytes. The sync pass
// uses it to clear both up. trashPrefix is given without a leading slash
// (`trash.Prefix`); implementations match the stored path with and
// without one, because drivers differ on that.
ListLiveNodesInTrash(ctx context.Context, storageID int64, trashPrefix string) ([]*model.Node, error)
// ListNodesUnder returns the row at dir and every row below it, in both
// path spellings drivers produce ("/a/b" and "a/b"); includeDeleted adds
// soft-deleted rows. The match is EXACT: names compare byte for byte, and
// the prefix bound is counted in characters, so a folder whose name is not
// ASCII matches as reliably as one that is. The storage root ("" or "/")
// is never a subtree and returns nothing.
ListNodesUnder(ctx context.Context, storageID int64, dir string, includeDeleted bool) ([]*model.Node, error)
ListNodesByParent(ctx context.Context, storageID int64, parentID *int64) ([]*model.Node, error)
// AggNodes returns a lightweight {id, parent_id, is_dir, size} row for every
// live node of a storage — the input to folder-size aggregation.
AggNodes(ctx context.Context, storageID int64) ([]NodeAgg, error)
// SetNodeSize overwrites a node's cached size. Used to store recursive folder
// totals (see internal/sync.RecomputeFolderSizes).
SetNodeSize(ctx context.Context, id int64, size int64) error
// SetNodeMtime overwrites a node's cached backend_mtime. Used to give folders
// a "last activity" date (newest descendant mtime) so the explorer can show a
// date for directories whose driver reports none (e.g. synthetic S3 prefixes).
SetNodeMtime(ctx context.Context, id int64, mtime *time.Time) error
UpdateNodeMeta(ctx context.Context, id int64, size int64, mime, etag string, mtime time.Time) error
TouchNodeSeen(ctx context.Context, id int64) error
SoftDeleteNode(ctx context.Context, id int64) error
// SoftDeleteAndRetag flips deleted_at + rewrites path/path_hash to a
// trash key in one shot, while saving the original path in
// storage_key. Used by vfDelete after the on-disk rename so the
// original-path slot is freed (UNIQUE(storage_id, path_hash)).
SoftDeleteAndRetag(ctx context.Context, id int64, trashPath, trashHash, origPath string) error
HardDeleteNode(ctx context.Context, id int64) error
MoveNode(ctx context.Context, id int64, parentID *int64, name, path, pathHash string) error
ListStaleNodes(ctx context.Context, storageID int64, before time.Time) ([]*model.Node, error)
// ListVanishedNodeIDs returns, above afterID and in id order, the ids of a
// storage's rows soft-deleted WHERE THEY STOOD (deleted, path outside
// `.filex-trash/`): rows whose bytes were never parked in the trash, which
// the storage sync drops for good (issue #74). See VanishedSQL.
ListVanishedNodeIDs(ctx context.Context, storageID, afterID int64, limit int) ([]int64, error)
// CountChildRows counts the rows, live or deleted, whose parent_id is
// parentID: what the parent_id cascade would take with that row.
CountChildRows(ctx context.Context, parentID int64) (int, error)
// live row id (migration 00078, issue #104), and reports whether that
// changed the row. See NodeUnavailableSQL.
MarkNodeUnavailable(ctx context.Context, id int64, reason string) (bool, error)
ClearNodeUnavailable(ctx context.Context, id int64) (bool, error)
// a folder's above it - or nil.
UnavailableAt(ctx context.Context, storageID int64, rel string) (*UnavailableEntry, error)
// ListStaleNodesUnder is ListStaleNodes bounded to the rows strictly BELOW
// dir (the folder's own row excluded), matched exactly as ListNodesUnder
// matches — the tombstone candidates of a folder rescan.
ListStaleNodesUnder(ctx context.Context, storageID int64, dir string, before time.Time) ([]*model.Node, error)
// CountLiveNodesUnder counts the live rows strictly below dir — the
// baseline a folder rescan's 70% guard compares what it saw against.
CountLiveNodesUnder(ctx context.Context, storageID int64, dir string) (int64, error)
CountNodesByStorage(ctx context.Context, storageID int64) (int64, error)
// Replication targets — separate entity. Storages.replica_target_id
// is the FK linking a primary to one of these.
ListReplicationTargets(ctx context.Context) ([]*model.ReplicationTarget, error)
GetReplicationTarget(ctx context.Context, id int64) (*model.ReplicationTarget, error)
CreateReplicationTarget(ctx context.Context, rt *model.ReplicationTarget) (*model.ReplicationTarget, error)
UpdateReplicationTarget(ctx context.Context, rt *model.ReplicationTarget) error
DeleteReplicationTarget(ctx context.Context, id int64) error
// StorageStats returns (file_count, total_size_bytes) for a storage,
// excluding directories and soft-deleted nodes. Used by the admin
// storages list page so each row can show "N files, 1.2 GB" without
// the SPA looping every node row.
StorageStats(ctx context.Context, storageID int64) (fileCount int64, totalBytes int64, err error)
// ── Lazy catalogue: per-folder state (migration 00059) ──
//
// What a lazily catalogued storage knows about each of its folders
// (docs/LAZY-CATALOGUE.md). Paths are canonical ("/" for the root,
// "/a/b" below it) and keyed by pathkey.Hash like nodes.
//
// GetCatalogueFolder returns (nil, nil) when the folder has no row.
GetCatalogueFolder(ctx context.Context, storageID int64, pathHash string) (*model.CatalogueFolder, error)
// DiscoverCatalogueFolders records folders seen in a parent's listing as
// uncatalogued; a folder that already has a row keeps it untouched.
DiscoverCatalogueFolders(ctx context.Context, storageID int64, folders []model.CatalogueFolder) error
// RecordCatalogueFolder writes every column of one folder's row.
RecordCatalogueFolder(ctx context.Context, f *model.CatalogueFolder) error
// SetCatalogueFolderWatch records a watch placed (watchedAt set: state
// watched) or removed (nil: state catalogued) on a folder that has been
// catalogued. An uncatalogued or missing row is left alone.
SetCatalogueFolderWatch(ctx context.Context, storageID int64, pathHash string, watchedAt *time.Time, reconcileOnOpen bool) error
// TouchCatalogueFolderVisit stamps visited_at on an existing row.
TouchCatalogueFolderVisit(ctx context.Context, storageID int64, pathHash string, at time.Time) error
// ListCatalogueFolders returns the rows a filter selects: by default the
// shallowest first; with ReconciledBefore, catalogued (unwatched) rows
// oldest first.
ListCatalogueFolders(ctx context.Context, storageID int64, f model.CatalogueFolderFilter) ([]*model.CatalogueFolder, error)
// CountCatalogueFolders counts a storage's rows per state.
CountCatalogueFolders(ctx context.Context, storageID int64) (model.CatalogueCounts, error)
// ResetCatalogueWatches demotes every watched row to catalogued with
// reconcile_on_open set — the watches died with the previous process.
ResetCatalogueWatches(ctx context.Context, storageID int64) (int64, error)
// DeleteCatalogueFoldersUnder drops the folder's row and every row below
// it (the folder is gone from the storage).
DeleteCatalogueFoldersUnder(ctx context.Context, storageID int64, dir string) error
// HasUncataloguedUnder reports whether any folder strictly below dir is
// still uncatalogued ("/" asks about the whole storage).
HasUncataloguedUnder(ctx context.Context, storageID int64, dir string) (bool, error)
// SearchNodes returns up to `limit` live nodes of a storage whose NAME
// answers m — every word in it, compared through internal/namefold (see
// model.NameMatch) — ranked by m.Prefer before the LIMIT. It is the
// search an install without the index runs, and every caller reaches it
// through search.Fallback.Candidates. No words, no rows.
//
// ⚠ EVERY word is a condition, never one of them: the fallback used to
// send only the longest word and check the rest in Go over the first rows
// by name, and a word most files shared pushed the file past the LIMIT
// (GitHub PR #46, measured on a 169k-file catalogue: the file at row
// 33 623 of a 1 000-row window).
//
// ⚠ Rank BEFORE the limit, never after: a LIMIT on `ORDER BY name` keeps
// the alphabetically first rows, and a search for `report` among a
// thousand `a-report-…` files lost `report.txt` itself.
//
// ⚠ The stored name goes through the same normaliser as the words, spelt
// in each dialect: fx_match/fx_rank (Go, registered on SQLite),
// normalize+lower on PostgreSQL, and on MySQL, which can compose nothing,
// its accent- and case-insensitive collation over both the composed and
// the decomposed form of each word. TestSearchNodesOnEveryEngine holds
// the three to one answer.
SearchNodes(ctx context.Context, storageID int64, m model.NameMatch, limit int) ([]*model.Node, error)
// Users
CreateUser(ctx context.Context, email, passwordHash, role, locale, tz string) (*model.User, error)
GetUser(ctx context.Context, id int64) (*model.User, error)
GetUserByEmail(ctx context.Context, email string) (*model.User, error)
// Dual-side login (migration 00025): an account answers to its e-mail or
// its username. Reach these through identity.Resolve, not directly — the
// rule for deciding which one an identifier is belongs in one place.
GetUserByUsername(ctx context.Context, username string) (*model.User, error)
SetUserUsername(ctx context.Context, id int64, username string) error
// Multi-tenancy (docs/MULTI-TENANCY.md): look a user up within one provider
// (tenant); re-home a user to a provider + record its OIDC subject (JIT).
GetUserByProviderEmail(ctx context.Context, providerID int64, email string) (*model.User, error)
// SetUserProvider re-homes a user. An empty oidcSubject also drops the
// account's SSO identity (its issuer, migration 00079): an account moved
// to another tenant is bound to nobody there.
SetUserProvider(ctx context.Context, userID, providerID int64, oidcSubject string) error
// The SSO identity an account is bound to (migration 00079, OIDCIdentitySQL):
// lookups that never leave one tenant, the bind, and the server's own
// reason for an account it opened switched off.
GetUserByOIDCIdentity(ctx context.Context, t UserTenant, issuer, subject string) (*model.User, error)
GetUserInTenantByEmail(ctx context.Context, t UserTenant, email string) (*model.User, error)
SetUserOIDCIdentity(ctx context.Context, userID int64, issuer, subject string) error
ClearUserOIDCIdentity(ctx context.Context, userID int64) (bool, error)
SetProviderOIDCTrustEmail(ctx context.Context, providerID int64, trust bool) error
SetUserDisabledReason(ctx context.Context, userID int64, reason string) error
ListUsersByProvider(ctx context.Context, providerID int64) ([]*model.User, error)
ListUsers(ctx context.Context) ([]*model.User, error)
CountUsers(ctx context.Context) (int64, error)
UpdateUserPassword(ctx context.Context, id int64, hash string) error
UpdateUserEmail(ctx context.Context, id int64, email string) error
UpdateUserDisplayName(ctx context.Context, id int64, displayName string) error
// UpdateUserAvatar sets (or clears, with "") the profile picture — a small
// data: URI or an http(s)/site-relative URL. Validation lives at the API
// boundary; the store only persists what it is handed.
UpdateUserAvatar(ctx context.Context, id int64, avatarURL string) error
UpdateUserLocale(ctx context.Context, id int64, locale, tz string) error
UpdateUserRole(ctx context.Context, id int64, role string) error
TouchLastLogin(ctx context.Context, id int64) error
// DeleteUser removes the account AND, in the same transaction, the public
// links it opened (`shares.created_by` = the account): download links and
// file requests (the maintainer, 2026-09-28: "Kapansın"). Every door that deletes
// an account — an administrator, a tenant deletion, a rolled-back
// provisioning — goes through here, so none of them can leave a working
// link behind. It used to keep the rows and clear `created_by` (ON DELETE
// SET NULL), and a link with no creator is a link nobody answers for.
//
// ⚠ An app's own public page (a signer's page — plugin_id > 0 with a
// page_id, model.Share.IsApp) stays, as it stays when its requester loses
// sharing (handlers.linkCreatorAllows): the app opened it. Links made
// before shares recorded a creator have none to match and are untouched.
DeleteUser(ctx context.Context, id int64) error
// DeleteUserWithLinks is DeleteUser that also says how many of the
// account's links were still open (not expired, not revoked) when they
// went — the number the audit row records. Removed rows of links that had
// already ended are not counted.
DeleteUserWithLinks(ctx context.Context, id int64) (closedLinks int64, err error)
// TOTP / 2FA
SetTotpPendingSecret(ctx context.Context, id int64, secret string, recoveryCodes []string) error
ActivateTotp(ctx context.Context, id int64) error
ClearTotp(ctx context.Context, id int64) error
// Sessions
CreateSession(ctx context.Context, userID int64, token string, expiresAt time.Time, ip, ua string) (*model.Session, error)
GetSessionByToken(ctx context.Context, token string) (*model.Session, error)
DeleteSession(ctx context.Context, token string) error
// SetSessionIDToken / GetSessionIDToken keep the IdP's id_token beside an
// OIDC session, so sign-out can end the IdP's session too (id_token_hint).
// Get returns "" — never an error — for a session that has none or does
// not exist: sign-out falls back to local-only and must not fail on it.
SetSessionIDToken(ctx context.Context, token, idToken string) error
GetSessionIDToken(ctx context.Context, token string) (string, error)
DeleteSessionsForUser(ctx context.Context, userID int64, exceptToken string) error
CountActiveSessions(ctx context.Context) (int64, error)
DeleteExpiredSessions(ctx context.Context) error
// API tokens — long-lived bearer credentials for AI / MCP / FilexClient.
CreateAPIToken(ctx context.Context, t *model.APIToken) (*model.APIToken, error)
GetAPITokenByHash(ctx context.Context, tokenHash string) (*model.APIToken, error)
GetAPITokenByID(ctx context.Context, id int64) (*model.APIToken, error)
ListAPITokens(ctx context.Context) ([]*model.APIToken, error)
ListAPITokensByUser(ctx context.Context, userID int64) ([]*model.APIToken, error)
TouchAPIToken(ctx context.Context, id int64) error
// UpdateAPITokenMeta edits a token's display metadata (label / username
// allow-list) and its kind ("user" / "app", migration 00030); nil leaves a
// field unchanged. The credential itself is immutable.
//
// Kind is editable because migration 00030 defaults every pre-existing row
// to "app": a personal token minted before the split needs one admin edit
// to become a "user" token again.
UpdateAPITokenMeta(ctx context.Context, id int64, label, usernames, kind *string) error
DeleteAPIToken(ctx context.Context, id int64) error
// S3 access keys (migration 00026) — the credential an S3 client signs
// with. Unlike an API token these hold a RECOVERABLE secret, because SigV4
// verifies by recomputing an HMAC chain rather than by comparing a hash;
// internal/secretbox is what keeps that out of a database dump. A key
// minted from a token INHERITS that token's permissions and never widens
// them (see model.S3AccessKey).
CreateS3AccessKey(ctx context.Context, k *model.S3AccessKey) (*model.S3AccessKey, error)
GetS3AccessKey(ctx context.Context, accessKeyID string) (*model.S3AccessKey, error)
GetS3AccessKeyByID(ctx context.Context, id int64) (*model.S3AccessKey, error)
ListS3AccessKeys(ctx context.Context, userID int64) ([]*model.S3AccessKey, error)
TouchS3AccessKey(ctx context.Context, id int64) error
SetS3AccessKeyDisabled(ctx context.Context, id int64, disabled bool) error
DeleteS3AccessKey(ctx context.Context, id, userID int64) error
// SSH public keys (migration 00027) — how an account reaches the SFTP
// endpoint without sending a password. The signature is verified by
// x/crypto/ssh before any of these are called; what the lookup decides is
// which ACCOUNT the key belongs to, which is why GetSSHPublicKey takes a
// fingerprint and returns exactly one row.
CreateSSHPublicKey(ctx context.Context, k *model.SSHPublicKey) (*model.SSHPublicKey, error)
GetSSHPublicKey(ctx context.Context, fingerprint string) (*model.SSHPublicKey, error)
GetSSHPublicKeyByID(ctx context.Context, id int64) (*model.SSHPublicKey, error)
ListSSHPublicKeys(ctx context.Context, userID int64) ([]*model.SSHPublicKey, error)
TouchSSHPublicKey(ctx context.Context, id int64) error
SetSSHPublicKeyDisabled(ctx context.Context, id int64, disabled bool) error
DeleteSSHPublicKey(ctx context.Context, id, userID int64) error
// NFS exports (migration 00028) — an NFSv3 mount bound to one account by a
// high-entropy export PATH, because NFSv3 has no authentication filex can
// use. GetNFSExport takes the sha256 of that path: the server only ever
// compares, so the plaintext is never stored.
CreateNFSExport(ctx context.Context, e *model.NFSExport) (*model.NFSExport, error)
GetNFSExport(ctx context.Context, tokenHash string) (*model.NFSExport, error)
GetNFSExportByID(ctx context.Context, id int64) (*model.NFSExport, error)
ListNFSExports(ctx context.Context, userID int64) ([]*model.NFSExport, error)
TouchNFSExport(ctx context.Context, id int64) error
SetNFSExportDisabled(ctx context.Context, id int64, disabled bool) error
DeleteNFSExport(ctx context.Context, id, userID int64) error
// Storage plugins (migration 00029) — the admin's registration of an
// out-of-process storage driver; see internal/plugin. Runtime state is
// NOT here (the manager re-derives it by starting the plugin).
CreatePlugin(ctx context.Context, p *model.Plugin) (*model.Plugin, error)
GetPlugin(ctx context.Context, id int64) (*model.Plugin, error)
GetPluginByName(ctx context.Context, name string) (*model.Plugin, error)
ListPlugins(ctx context.Context) ([]*model.Plugin, error)
UpdatePlugin(ctx context.Context, p *model.Plugin) error
DeletePlugin(ctx context.Context, id int64) error
// App plugins (migration 00042) — in-process WebAssembly plugins; see
// internal/wasmplugin. The row is the admin's intent + the approved grant;
// runtime state is derived by loading the module.
CreateAppPlugin(ctx context.Context, p *model.AppPlugin) (*model.AppPlugin, error)
GetAppPlugin(ctx context.Context, id int64) (*model.AppPlugin, error)
GetAppPluginByName(ctx context.Context, name string) (*model.AppPlugin, error)
ListAppPlugins(ctx context.Context) ([]*model.AppPlugin, error)
UpdateAppPlugin(ctx context.Context, p *model.AppPlugin) error
// DeleteAppPlugin removes the row and everything keyed on it (settings,
// overrides, state, jobs).
DeleteAppPlugin(ctx context.Context, id int64) error
// App versions an upgrade replaced (migration 00066), newest first.
CreateAppPluginVersion(ctx context.Context, v *model.AppPluginVersion) (*model.AppPluginVersion, error)
ListAppPluginVersions(ctx context.Context, pluginID int64) ([]*model.AppPluginVersion, error)
DeleteAppPluginVersion(ctx context.Context, id int64) error
GetAppPluginSettings(ctx context.Context, pluginID int64) (map[string]string, error)
// PutAppPluginSettings replaces the whole set.
PutAppPluginSettings(ctx context.Context, pluginID int64, values map[string]string) error
ListAppPluginOverrides(ctx context.Context, pluginID int64) ([]*model.AppPluginOverride, error)
// PutAppPluginOverrides replaces the whole set.
PutAppPluginOverrides(ctx context.Context, pluginID int64, rows []*model.AppPluginOverride) error
GetAppPluginState(ctx context.Context, pluginID, storageID int64, pathHash, key string) (string, bool, error)
// SetAppPluginState records one key. rel is the file's storage-relative
// path, stored beside the hash so a listing can name the file.
SetAppPluginState(ctx context.Context, pluginID, storageID int64, pathHash, rel, key, value string) error
DeleteAppPluginState(ctx context.Context, pluginID, storageID int64, pathHash, key string) error
CreateAppPluginJob(ctx context.Context, j *model.AppPluginJob) error
GetAppPluginJob(ctx context.Context, id string) (*model.AppPluginJob, error)
// ListAppPluginJobsByOp returns the jobs behind the given ops rows, keyed
// by op id — what the ops tray uses to decorate plugin rows.
ListAppPluginJobsByOp(ctx context.Context, opIDs []int64) (map[int64]*model.AppPluginJob, error)
UpdateAppPluginJob(ctx context.Context, j *model.AppPluginJob) error
// SetAppPluginJobOp writes ONLY the ops-row id onto a job.
//
// The whole-row update cannot be used for this: submitting the job wakes
// the worker immediately, a short action can finish before the handler
// gets its answer back, and writing the handler's copy of the row then
// erases the status, the message and the outputs the worker just wrote.
// The symptom is silent and awful — the ops row says "ok" and the
// message the plugin produced (a public link and its PIN, say) is gone.
SetAppPluginJobOp(ctx context.Context, jobID string, opID int64) error
CreateAppPluginSigningKey(ctx context.Context, k *model.AppPluginSigningKey) error
GetAppPluginSigningKey(ctx context.Context, id string) (*model.AppPluginSigningKey, error)
// GetAppPluginSigningCA returns the tenant's live CA row, or nil.
GetAppPluginSigningCA(ctx context.Context, tenantID int64) (*model.AppPluginSigningKey, error)
// GetAppPluginSealKey returns the live platform seal of one app for one
// tenant (purpose "platform", not destroyed), or nil.
GetAppPluginSealKey(ctx context.Context, tenantID, pluginID int64) (*model.AppPluginSigningKey, error)
RetireAppPluginSigningCA(ctx context.Context, tenantID int64) error
// ListAppPluginSigningCAs returns every CA a tenant has ever signed
// with, retired ones included, newest first. Nothing deletes these: a
// signature made years ago is only verifiable while the certificate
// that issued it is still here.
ListAppPluginSigningCAs(ctx context.Context, tenantID int64) ([]*model.AppPluginSigningKey, error)
// DestroyAppPluginSigningKey empties the sealed key and stamps destroyed_at.
DestroyAppPluginSigningKey(ctx context.Context, id string) error
// ListAppPluginStateFiles returns the files this plugin keeps the given
// state key on — the key joined back to the node rows, so a file that
// was deleted is simply not in the answer. An empty key lists every key.
ListAppPluginStateFiles(ctx context.Context, pluginID int64, key string, limit int) ([]*model.AppPluginStateFile, error)
// ListAppPluginStateKeys returns, per path hash, the "<plugin name>:<key>"
// pairs kept on those files — what a listing shows so the menu can offer
// state-aware actions (applies.state). Values are never returned.
ListAppPluginStateKeys(ctx context.Context, storageID int64, pathHashes []string) (map[string][]string, error)
// PutAppPluginLock inserts or replaces the lock on (storage, path).
PutAppPluginLock(ctx context.Context, l *model.AppPluginLock) error
GetAppPluginLock(ctx context.Context, storageID int64, pathHash string) (*model.AppPluginLock, error)
// ListAppPluginLocks returns every lock on a storage (0 = all storages),
// expired ones included; callers filter with Live.
ListAppPluginLocks(ctx context.Context, storageID int64) ([]*model.AppPluginLock, error)
DeleteAppPluginLock(ctx context.Context, storageID int64, pathHash string) error
// DeleteExpiredAppPluginLocks drops locks whose until passed before now.
DeleteExpiredAppPluginLocks(ctx context.Context, now time.Time) (int64, error)
// App-plugin schedule (migration 00050) — the hourly wake-up of an app
// that holds the `schedule` permission, and the work that wake-up asked
// for. One row per (plugin, key); the empty key is the wake-up itself.
//
// PutAppPluginScheduleItem inserts the item or moves the existing one.
// A row another process is RUNNING is left alone and no error is
// returned: the running process owns it and will finish it.
PutAppPluginScheduleItem(ctx context.Context, it *model.AppPluginScheduleItem) error
// ClaimAppPluginScheduleItem takes the lease on a due row: one
// conditional UPDATE from due to running, true only for the process
// whose UPDATE matched. This is what stops two filex processes on one
// database from running the same item twice.
ClaimAppPluginScheduleItem(ctx context.Context, pluginID int64, key, owner string, now time.Time) (bool, error)
// FinishAppPluginScheduleItem ends a claimed row. A non-nil rearmAt puts
// it back to `due` at that time instead (the wake-up re-arming itself
// for the next hour); status is then ignored and errMsg becomes the
// note the row carries until the next wake-up.
FinishAppPluginScheduleItem(ctx context.Context, pluginID int64, key, status, jobID, errMsg string, rearmAt *time.Time) error
// DueAppPluginScheduleItems returns rows that are `due` at or before
// now, oldest first, capped at limit.
DueAppPluginScheduleItems(ctx context.Context, now time.Time, limit int) ([]*model.AppPluginScheduleItem, error)
// NextAppPluginScheduleDue is when the earliest `due` row comes due, so
// the scheduler can sleep exactly that long instead of polling.
NextAppPluginScheduleDue(ctx context.Context) (*time.Time, error)
// ListAppPluginScheduleItems returns every row of one app, soonest first.
ListAppPluginScheduleItems(ctx context.Context, pluginID int64) ([]*model.AppPluginScheduleItem, error)
// ReapAppPluginScheduleItems drops finished rows older than before.
ReapAppPluginScheduleItems(ctx context.Context, before time.Time) (int64, error)
// File grants — per-user/per-folder ACL (RBAC feature, migration 00012).
ListFileGrantsByStorageUser(ctx context.Context, storageID, userID int64) ([]*model.FileGrant, error)
ListFileGrantsByStorage(ctx context.Context, storageID int64) ([]*model.FileGrant, error)
ListAllFileGrants(ctx context.Context) ([]*model.FileGrant, error)
GetFileGrant(ctx context.Context, id int64) (*model.FileGrant, error)
CreateFileGrant(ctx context.Context, g *model.FileGrant) (*model.FileGrant, error)
UpdateFileGrantLevel(ctx context.Context, id int64, level string) error
DeleteFileGrant(ctx context.Context, id int64) error
// Per-user permissions (migration 00069, see internal/perm). Overrides
// are keyed by permission name; values are model.PermAllow/PermDeny. The
// store persists what it is handed — perm.ValidateEffects runs first.
// GetUserPermissionOverrides returns an empty map (never an error) for a
// user with no row. SetUserPermissionOverrides with an empty map removes
// the row. ListUserPermissionOverrides returns every user that has one.
GetUserPermissionOverrides(ctx context.Context, userID int64) (map[string]string, error)
SetUserPermissionOverrides(ctx context.Context, userID int64, overrides map[string]string, updatedBy *int64) error
ListUserPermissionOverrides(ctx context.Context) (map[int64]map[string]string, error)
// Permission rules (migration 00069). ListPermissionRules is id order —
// the order perm.Resolve reports "the first rule that denied it" in.
// GetPermissionRule returns sql.ErrNoRows for an unknown id.
ListPermissionRules(ctx context.Context) ([]*model.PermissionRule, error)
GetPermissionRule(ctx context.Context, id int64) (*model.PermissionRule, error)
CreatePermissionRule(ctx context.Context, r *model.PermissionRule) (*model.PermissionRule, error)
UpdatePermissionRule(ctx context.Context, r *model.PermissionRule) error
DeletePermissionRule(ctx context.Context, id int64) error
// SSO groups from a user's most recent SSO login (migration 00069).
// SetUserSSOGroups replaces the whole set.
ListUserSSOGroups(ctx context.Context, userID int64) ([]string, error)
SetUserSSOGroups(ctx context.Context, userID int64, groups []string) error
// A person's one custom role (migration 00069). GetUserCustomRole is 0
// for none; SetUserCustomRole with 0 takes it away. Deleting the role
// takes it from everyone who held it (ON DELETE CASCADE).
GetUserCustomRole(ctx context.Context, userID int64) (int64, error)
SetUserCustomRole(ctx context.Context, userID, roleID int64) error
ListUserCustomRoles(ctx context.Context) (map[int64]int64, error)
// Groups (migration 00074, internal/group). One implementation for every
// engine: db.GroupSQL, embedded in each driver's Store. GetGroup and
// GetGroupFileGrant return sql.ErrNoRows for an unknown id. Deleting a
// group takes its memberships and grants along; deleting a role leaves
// its groups with none (ReassignGroupRole first to move them).
ListGroups(ctx context.Context) ([]*model.Group, error)
GetGroup(ctx context.Context, id int64) (*model.Group, error)
CreateGroup(ctx context.Context, g *model.Group) (*model.Group, error)
UpdateGroup(ctx context.Context, g *model.Group) error
DeleteGroup(ctx context.Context, id int64) error
ReassignGroupRole(ctx context.Context, from, to int64) error
ListGroupMembers(ctx context.Context, groupID int64) ([]*model.GroupMember, error)
ListUserGroupMemberships(ctx context.Context, userID int64) ([]*model.GroupMember, error)
ListAllGroupMembers(ctx context.Context) ([]*model.GroupMember, error)
AddGroupMember(ctx context.Context, groupID, userID int64) error
RemoveGroupMember(ctx context.Context, groupID, userID int64) error
SetUserLinkedGroups(ctx context.Context, userID int64, source string, groupIDs []int64) (added, removed []int64, err error)
DropForeignMemberships(ctx context.Context, userID, providerID int64) error
// The built-in level an account had before a group's role moved it
// (user_group_levels), so leaving the group can put it back.
GetUserGroupLevel(ctx context.Context, userID int64) (string, bool, error)
SetUserGroupLevel(ctx context.Context, userID int64, level string) error
DeleteUserGroupLevel(ctx context.Context, userID int64) error
// A group's folder grants (group_file_grants). Rows come back as
// model.FileGrant with GroupID set and UserID zero.
ListGroupFileGrantsByStorage(ctx context.Context, storageID int64) ([]*model.FileGrant, error)
ListGroupFileGrantsByStorageUser(ctx context.Context, storageID, userID int64) ([]*model.FileGrant, error)
ListGroupFileGrantsByGroup(ctx context.Context, groupID int64) ([]*model.FileGrant, error)
ListAllGroupFileGrants(ctx context.Context) ([]*model.FileGrant, error)
GetGroupFileGrant(ctx context.Context, id int64) (*model.FileGrant, error)
CreateGroupFileGrant(ctx context.Context, g *model.FileGrant) (*model.FileGrant, error)
UpdateGroupFileGrantLevel(ctx context.Context, id int64, level string) error
DeleteGroupFileGrant(ctx context.Context, id int64) error
// Shares
// counter an app page's visit ceiling is measured against. Page views
// are not downloads (the owner's ruling, 2026-09-21).
IncrementShareVisit(ctx context.Context, id int64) error
// reports whether it got one. This is the cap's only real enforcement
// point: a check that reads the counter and a serve that bumps it
// afterwards are two separate steps, and every request that starts inside
// that gap passes the check (measured on fm.example.com: a 1-download link
// handed three full files to three overlapping clients). Claim, then serve.
ReserveShareDownload(ctx context.Context, id int64) (bool, error)
// ReleaseShareDownload hands a reserved slot back. Used only when the serve
// fails before a single byte reaches the client, so a storage error does
// not silently eat one of the downloads the owner granted.
// (the share_state host function). Bounded by the caller at 64 KiB.
UpdateShareAppState(ctx context.Context, id int64, stateJSON string) error
// Separate from every other update so a failed guess cannot rewrite the
// link's expiry, caps or exposed files by riding along in a whole-row save.
UpdateSharePinLock(ctx context.Context, id int64, fails int, until *time.Time) error
// first. pluginID 0 means every app (never the ordinary shares);
// activeOnly drops the expired and revoked ones.
ListAppPluginShares(ctx context.Context, pluginID int64, activeOnly bool, limit, offset int) ([]*ShareWithMeta, int64, error)
// Chunked uploads
CreateChunkedUpload(ctx context.Context, u *model.ChunkedUpload) error
GetChunkedUpload(ctx context.Context, id string) (*model.ChunkedUpload, error)
UpdateChunkedUploadParts(ctx context.Context, id string, parts []model.UploadPart) error
DeleteChunkedUpload(ctx context.Context, id string) error
DeleteExpiredChunkedUploads(ctx context.Context) error
// Staged uploads — the driver-agnostic resumable path (docs/UPLOADS.md).
// A separate table from chunked_uploads on purpose: see the comment in
// db/migrations/sqlite/00024_staged_uploads.sql.
CreateStagedUpload(ctx context.Context, u *model.StagedUpload) error
GetStagedUpload(ctx context.Context, id string) (*model.StagedUpload, error)
// GetStagedUploadByNode is the reverse index chunk 5 (read-during-transfer)
// uses to find the staging directory holding a node's bytes.
GetStagedUploadByNode(ctx context.Context, nodeID int64) (*model.StagedUpload, error)
UpdateStagedUploadProgress(ctx context.Context, id string, receivedBytes int64) error
UpdateStagedUploadState(ctx context.Context, id, state, errMsg string) error
AttachStagedUploadTarget(ctx context.Context, id string, nodeID, opID int64) error
DeleteStagedUpload(ctx context.Context, id string) error
ListStagedUploads(ctx context.Context, state string, limit int) ([]*model.StagedUpload, error)
// ListIdleStagedUploads returns rows whose last activity is older than
// `before` — the staging sweeper's input.
ListIdleStagedUploads(ctx context.Context, before time.Time, limit int) ([]*model.StagedUpload, error)
// SumOpenStagedUploadBytes is the quota RESERVATION: the declared size of
// every not-yet-committed upload the user owns. Derived rather than stored,
// so it can never drift from the rows it describes and a row leaving the
// open set releases its reservation by construction.
SumOpenStagedUploadBytes(ctx context.Context, userID int64) (int64, error)
// SetNodeTransferState sets nodes.transfer_state: "staged" while the bytes
// are in filex's staging area, "stored" once they are on the driver, and
// "failed" when the transfer to the driver did not succeed.
SetNodeTransferState(ctx context.Context, nodeID int64, state string) error
// ListUnstoredNodes pages through every LIVE row whose transfer_state is
// "staged" or "failed" — bytes a staged upload committed that were never
// confirmed on the storage — in id order, after afterID. The staged-upload
// boot pass settles the ones whose bytes did land.
ListUnstoredNodes(ctx context.Context, afterID int64, limit int) ([]*model.Node, error)
// Sync runs / conflicts
CreateSyncRun(ctx context.Context, storageID int64, cursorBefore string) (*model.SyncRun, error)
FinishSyncRun(ctx context.Context, id int64, cursorAfter string, seen, added, updated, deleted int, status, errMsg string) error
GetSyncRun(ctx context.Context, id int64) (*model.SyncRun, error)
GetLastSyncRun(ctx context.Context, storageID int64) (*model.SyncRun, error)
// GetLastSyncRunByStatus is the most recent FINISHED run of a storage with
// the given status (sql.ErrNoRows when there is none) — the tombstone
// guard's baseline is the last run that finished "ok", never one that
// was cut short with whatever it had counted by then.
GetLastSyncRunByStatus(ctx context.Context, storageID int64, status string) (*model.SyncRun, error)
// AbortUnfinishedSyncRuns closes every run with no finished_at as
// "aborted", with errMsg as its error, and reports how many it closed.
// Called once when the sync worker starts, before any run of its own: a
// row still open then belongs to a process that is gone.
AbortUnfinishedSyncRuns(ctx context.Context, errMsg string) (int64, error)
ListSyncRuns(ctx context.Context, storageID int64, limit int) ([]*model.SyncRun, error)
ListSyncRunsAcrossAll(ctx context.Context, storageID int64, status string, limit, offset int) ([]*model.SyncRun, int64, error)
CreateSyncConflict(ctx context.Context, c *model.SyncConflict) error
ListUnresolvedConflicts(ctx context.Context) ([]*model.SyncConflict, error)
ListConflictsByStorage(ctx context.Context, storageID int64, limit int) ([]*model.SyncConflict, error)
CountSyncConflictsByRun(ctx context.Context, runID int64) (int64, error)
ResolveConflict(ctx context.Context, id int64, resolution string) error
CountQueueDepth(ctx context.Context) (int64, error)
// Audit
InsertAuditEntry(ctx context.Context, e *model.AuditEntry) error
ListAuditRecent(ctx context.Context, limit int) ([]*model.AuditEntry, error)
ListAuditFiltered(ctx context.Context, userID *int64, action string, from, to *time.Time, limit, offset int) ([]*AuditEntryWithUser, int64, error)
// Settings
GetSetting(ctx context.Context, key string) (string, error)
UpsertSetting(ctx context.Context, key, value string) error
ListSettings(ctx context.Context) (map[string]string, error)
// External services
UpsertExternalService(ctx context.Context, name string, enabled bool, url, secretEnc, optionsJSON string, lastCheck time.Time, lastState string) error
GetExternalService(ctx context.Context, name string) (*ExternalService, error)
ListExternalServices(ctx context.Context) ([]*ExternalService, error)
UpdateExternalServiceState(ctx context.Context, name string, lastCheck time.Time, state string) error
// Duplicate report — every live file node whose (size, non-empty etag)
// pair occurs more than once, minSize filtering applied in SQL. Rows come
// back ordered (size DESC, etag, id) so the handler can group them
// contiguously. Powers GET /api/admin/duplicates (v0.2 "Bul").
ListDuplicateNodes(ctx context.Context, minSize int64) ([]DuplicateNode, error)
// Cross-storage analytics for the dashboard
SumNodesBytesByStorage(ctx context.Context, storageID int64) (int64, error)
CountNodesAddedSince(ctx context.Context, storageID int64, since time.Time) (int64, error)
CountNodesDeletedSince(ctx context.Context, storageID int64, since time.Time) (int64, error)
// Thumbnails
GetThumbnail(ctx context.Context, nodeID int64) (*model.Thumbnail, error)
// GetThumbnails is GetThumbnail for many nodes in batched queries: what a
// listing asks once instead of once per file. Nodes without a row are
// absent from the map.
GetThumbnails(ctx context.Context, ids []int64) (map[int64]*model.Thumbnail, error)
// ListThumbnailProblems lists the files whose thumbnail failed or was
// skipped - and those drawn only after an earlier handler failed (a ready
// row with more than one attempt, 0.50) - most recently attempted first,
// at most limit of them. nil storageIDs is every storage; an empty slice
// is none. Trashed files and end-to-end encrypted skips (nothing to
// repair there) are left out.
ListThumbnailProblems(ctx context.Context, storageIDs []int64, limit int) ([]*model.ThumbnailProblem, error)
// ListPreviewCandidates answers, for each folder in parentIDs, the
// perFolder live FILES directly in it that came in last: newest first by
// the later of the row's creation (when the file entered the catalogue)
// and the file's own modification time, then by name. Subfolders are
// never looked into, and a folder that holds only folders is absent. What
// a folder's card is drawn from (docs/thumbnails.md, Folder previews).
// ONE query for a whole listing's folders.
ListPreviewCandidates(ctx context.Context, storageID int64, parentIDs []int64, perFolder int) (map[int64][]*model.Node, error)
UpsertThumbnail(ctx context.Context, t *model.Thumbnail) error
SetThumbnailState(ctx context.Context, nodeID int64, state, errMsg string) error
// DeleteThumbnail drops the row for one node. The cached JPEG on disk is
// the pipeline's business (internal/thumb); this is only the catalogue
// half, and it exists so a purge does not have to wait for the FK cascade
// to be the only thing that ever removed it.
DeleteThumbnail(ctx context.Context, nodeID int64) error
// ExistingNodeIDs reports which of the given ids still have a `nodes` row —
// TRASHED ROWS INCLUDED, because a trashed file is restorable and must keep
// its thumbnail. It is the safety interlock of the thumbnail-cache sweeper:
// a cached file is deleted only when this positively says its node is gone.
ExistingNodeIDs(ctx context.Context, ids []int64) (map[int64]bool, error)
// Node versions
CreateNodeVersion(ctx context.Context, v *model.NodeVersion) (*model.NodeVersion, error)
ListNodeVersions(ctx context.Context, nodeID int64) ([]*model.NodeVersion, error)
GetNodeVersion(ctx context.Context, id int64) (*model.NodeVersion, error)
NextNodeVersionNumber(ctx context.Context, nodeID int64) (int, error)
DeleteNodeVersion(ctx context.Context, id int64) error
DeleteOldNodeVersions(ctx context.Context, nodeID int64, keep int) ([]*model.NodeVersion, error)
// ListNodeIDsWithVersions returns the distinct node ids that have at
// least one node_versions row — the work list for the daily version
// retention job (v0.4 "Koru").
ListNodeIDsWithVersions(ctx context.Context) ([]int64, error)
// Sync conflicts (admin views)
ListSyncConflictsByRun(ctx context.Context, runID int64) ([]*model.SyncConflict, error)
ListSyncConflictsByStorage(ctx context.Context, storageID int64, limit int) ([]*model.SyncConflict, error)
// Search rebuild
AllNodesForIndex(ctx context.Context) ([]*model.Node, error)
// Quota
GetUserUsage(ctx context.Context, userID int64) (used, limit int64, err error)
IncrementUserUsage(ctx context.Context, userID int64, delta int64) error
SetUserQuota(ctx context.Context, userID int64, bytes int64) error
// SetUserEnabled flips the account on/off (migration 00022). A disabled
// user cannot start a session; nothing they own is touched. It clears the
// server's disabled reason (migration 00079): the administrator decided.
SetUserEnabled(ctx context.Context, userID int64, enabled bool) error
RecomputeUserUsage(ctx context.Context, userID int64) (int64, error)
// Node owner / attribution (migration 00004 + 00038)
//
// owner_id is who PUT THE THING HERE; last_actor_id is who touched it
// last; external_upload says it arrived through an anonymous drop link.
// NULL means SYSTEM in both id columns — see model.Node.
//
// A CREATE does not use these: internal/quotastore stamps the model and
// the INSERT carries the values, so a bulk write costs no extra queries.
// They exist for the mutations that change attribution on a row that
// already exists (overwrite, move, restore).
SetNodeOwner(ctx context.Context, nodeID int64, ownerID *int64) error
GetNodeOwner(ctx context.Context, nodeID int64) (*int64, error)
SetNodeActor(ctx context.Context, nodeID int64, actorID *int64) error
SetNodeExternalUpload(ctx context.Context, nodeID int64, external bool) error
// SetNodeDeletedBy names who put a trashed row in the trash: the row and
// every trashed row under its path that names nobody yet (a folder's
// contents, trashed with it). A live row is left alone. Every soft delete
// clears the name first; internal/quotastore writes it after the delete,
// from the acting identity.
SetNodeDeletedBy(ctx context.Context, nodeID int64, by *int64) error
// GetUserDisplayNames resolves a batch of user ids to the label an Owner
// column shows. One query for a whole listing page, names only — never
// full user rows. See the sqlite driver for why.
GetUserDisplayNames(ctx context.Context, ids []int64) (map[int64]string, error)
// Trash retention
//
// ListTrashedExpired returns up to `limit` soft-deleted nodes whose
// deleted_at is older than `before`, in id order, strictly after `afterID`
// (0 starts at the first), narrowed to storageIDs: nil means every
// storage, and an EMPTY, non-nil slice matches nothing (a scope that
// reaches no storage must not read as "no restriction"). A sweep passes
// the last id it saw back in, so every row is met once per run whatever
// the caller did with it.
//
// ⚠ The narrowing is in the SQL on purpose. The purge used to read every
// storage's rows and skip the foreign ones in Go; a skipped row is never
// removed, so behind a full batch of somebody else's rows it re-read the
// same batch forever.
//
// ⚠ The cursor is the id, never deleted_at. SQLite keeps CURRENT_TIMESTAMP
// as `YYYY-MM-DD HH:MM:SS` and the driver writes a time.Time parameter in
// another spelling, so a timestamp cursor compared unequal to the rows that
// share its second — and a trash emptied in one burst shares very few
// seconds between tens of thousands of rows.
ListTrashedExpired(ctx context.Context, before time.Time, storageIDs []int64, afterID int64, limit int) ([]*model.Node, error)
// CountTrashedExpired tallies, per storage, the rows ListTrashedExpired
// walks for the same `before`: how many, and the bytes their files hold
// (a folder's size is a cached total of its files, so it is not added).
CountTrashedExpired(ctx context.Context, before time.Time) (map[int64]TrashTally, error)
// ListTrashed returns soft-deleted nodes (paginated). storage filter optional.
ListTrashed(ctx context.Context, storageID *int64, limit, offset int) ([]*model.Node, int, error)
RestoreNode(ctx context.Context, id int64) error
// RestoreNodeAt restores a soft-deleted node, simultaneously reverting its
// path/path_hash to the supplied original-path values and re-attaching it
// to the resolved parent_id (nil = root). Used by trash.Service.Restore
// to undo the `.filex-trash/` rename.
RestoreNodeAt(ctx context.Context, id int64, parentID *int64, origPath string) error
// LookupParentByPath returns the parent_id (nil at root) for a path's
// parent dir, or an error if the parent dir doesn't exist in the cache.
LookupParentByPath(ctx context.Context, storageID int64, fullPath string) (*int64, error)
// Per-user metadata (starred, last_opened). Tags have their own tables
// since 00055 — see the Tags block below.
SetUserNodeMeta(ctx context.Context, userID, nodeID int64, key, value string) error
DeleteUserNodeMeta(ctx context.Context, userID, nodeID int64, key string) error
GetUserNodeMeta(ctx context.Context, userID, nodeID int64, key string) (string, error)
ListUserNodeMetaForNode(ctx context.Context, userID, nodeID int64, prefix string) (map[string]string, error)
ListNodesByUserMeta(ctx context.Context, userID int64, key string, limit int) ([]*model.Node, error)
// Per-user VIEW preferences (00039): how this person left each folder, as
// one JSON document read whole and written whole. Not per-node and not
// queried by folder, so it is deliberately not user_node_meta — see
// db/migrations/sqlite/00039_user_view_prefs.sql. "" = nothing stored yet,
// which is not an error.
GetUserViewPrefs(ctx context.Context, userID int64) (string, error)
SetUserViewPrefs(ctx context.Context, userID int64, doc string) error
// ── Surface preferences (00047) ──
//
// What a person chose about the interface itself — theme, palette,
// density, language — one JSON document per person PER SURFACE ("web" or
// "desktop"). Distinct from the view prefs above, which are how each
// FOLDER was left. "" and "no row" are the same answer and neither is an
// error: nothing chosen yet is every account's first day.
GetUserPrefs(ctx context.Context, userID int64, surface string) (string, error)
SetUserPrefs(ctx context.Context, userID int64, surface, doc string) error
// ListUserPrefs is every document one person stored, each with when it
// was last written (0.50: an account's "open with" choices are adopted
// once from the most recently written surface document).
ListUserPrefs(ctx context.Context, userID int64) ([]UserPrefsDoc, error)
// Operator-defined themes (00051): a name plus two `--fe-*` token maps,
// served to every browser beside the built-in palettes. Instance-wide —
// the table has no tenant column, which is exactly why the admin routes
// that write them are behind requireSupertenant.
//
// ⚠ ListCustomThemes is on the PUBLIC appearance path (an anonymous
// visitor's share page resolves the instance default through it), so a row
// whose token JSON cannot be parsed is DROPPED from the list rather than
// failing the call: one bad row must not take the login page down with it.
ListCustomThemes(ctx context.Context) ([]*model.CustomTheme, error)
GetCustomTheme(ctx context.Context, key string) (*model.CustomTheme, error)
UpsertCustomTheme(ctx context.Context, t *model.CustomTheme) error
// DeleteCustomTheme reports whether a row was actually removed, so the
// handler can answer 404 instead of a cheerful 200 for a key that was
// never there.
DeleteCustomTheme(ctx context.Context, key string) (bool, error)
// ── Tags (00055): personal and team vocabularies + the files they are on ──
//
// ⚠ The store does not decide who may SEE a tag. Visibility needs the
// caller, the tenant scope, the token's `root:` confinement and the ACL,
// none of which a store knows — handlers/tags.go owns it, and every read
// below returns rows for that code to judge. What the store does own is
// the vocabulary boundary: TagQuery (rendered once, by TagQueryWhere) says
// WHOSE tags a query may even consider.
//
// ListTags returns the vocabulary rows a query selects, oldest first.
ListTags(ctx context.Context, q model.TagQuery) ([]*model.Tag, error)
// CreateTag inserts a vocabulary row. A UNIQUE violation (a concurrent
// create of the same name) is returned as an error; the caller re-reads.
CreateTag(ctx context.Context, t *model.Tag) (*model.Tag, error)
// ListNodeTags returns EVERY tag on a node, of every owner and tenant —
// the caller filters to what the person may see.
ListNodeTags(ctx context.Context, nodeID int64) ([]*model.Tag, error)
// LinkNodeTags adds and removes tag links on one node in one transaction,
// and deletes a removed tag that no file carries any more (a tag exists
// while it is on something, as it always has).
LinkNodeTags(ctx context.Context, nodeID int64, add, remove []int64) error
// ListNodesByTagIDs returns live nodes carrying ANY of the tags,
// newest-first (by node updated_at), capped at limit.
ListNodesByTagIDs(ctx context.Context, tagIDs []int64, limit int) ([]*model.Node, error)
// ListTagPlacements returns every (tag, live file) pair for the tags a
// query selects — what the tag listings judge visibility on, file by file.
ListTagPlacements(ctx context.Context, q model.TagQuery) ([]model.TagPlacement, error)
// Notifications (in-app bell + webhook delivery audit)
InsertNotification(ctx context.Context, n *model.NotificationInput) (int64, error)
GetNotification(ctx context.Context, id int64) (*model.Notification, error)
// ListNotifications and UnreadNotificationCount take mutedEvents: the
// caller's already-resolved per-user mute list (see
// model.NotificationSettings.MutedList). It is applied IN SQL rather than
// by filtering the returned page, because a page filtered afterwards comes
// back short and its total still counts the rows the caller then hides.
// Nil/empty means "no mute filter", which is what the admin/global view
// (userID == nil) always passes.
//
// hiddenBodies are LIKE patterns a row's `body` must NOT match — the
// notify service's read-time filter for rows recorded about filex's own
// directories before those stopped being announced (notify.hiddenBodies).
// SQL for the same reason as the mute list: the unread badge is a COUNT,
// and a row the list hides but the count still counts is a badge that says
// "3" over an empty bell. Nil/empty filters nothing. The rows are not
// changed or deleted — the filter is on the read, never on the history.
//
// broadcasts decides how the read treats rows with no user: which of them
// a bell takes at all (see notify.Bell), and whose read state they carry.
// It is in SQL for the same reason as the mute list.
ListNotifications(ctx context.Context, userID *int64, onlyUnread bool, mutedEvents, hiddenBodies []string, broadcasts model.BroadcastFilter, limit, offset int) ([]*model.Notification, int64, error)
// MarkNotificationRead and MarkAllNotificationsRead stamp rows ADDRESSED
// to userID and nothing else: a broadcast is many readers' row, and is
// marked per reader through MarkBroadcastsRead. nil userID stamps the
// rows' own column regardless of owner (an instance-wide sweep).
MarkNotificationRead(ctx context.Context, id int64, userID *int64) error
MarkAllNotificationsRead(ctx context.Context, userID *int64) error
// MarkBroadcastsRead records that readerID has read these broadcasts
// (migration 00056). Ids that are not broadcasts are ignored, and marking
// one twice is not an error. Whether the reader may see them is the
// caller's question — the store cannot answer it.
MarkBroadcastsRead(ctx context.Context, readerID int64, ids []int64) error
// MarkAllBroadcastsRead moves readerID's "mark all read" point to the
// newest notification: every broadcast up to it is read for that reader,
// and nobody else. One write however many broadcasts there are.
MarkAllBroadcastsRead(ctx context.Context, readerID int64) error
UnreadNotificationCount(ctx context.Context, userID *int64, mutedEvents, hiddenBodies []string, broadcasts model.BroadcastFilter) (int64, error)
UpdateWebhookStatus(ctx context.Context, id int64, status, errMsg string) error
GetNotificationSettings(ctx context.Context, userID int64) (*model.NotificationSettings, error)
UpsertNotificationSettings(ctx context.Context, s *model.NotificationSettings) error
// Webhook targets (webhook v2, migration 00017) — additional POST
// destinations next to the legacy single global webhook. Update
// replaces the full mutable row (name/url/secret/events/enabled);
// partial-PATCH merging happens in the handler.
CreateWebhookTarget(ctx context.Context, t *model.WebhookTarget) (*model.WebhookTarget, error)
GetWebhookTarget(ctx context.Context, id int64) (*model.WebhookTarget, error)
ListWebhookTargets(ctx context.Context) ([]*model.WebhookTarget, error)
UpdateWebhookTarget(ctx context.Context, t *model.WebhookTarget) error
DeleteWebhookTarget(ctx context.Context, id int64) error
// UpdateWebhookTargetDelivery records the outcome of the most recent
// delivery attempt to one target (migration 00019): the final
// attempt's HTTP status (0 = no response at all), the aggregated
// error message ("" on success → stored NULL) and the attempt time.
UpdateWebhookTargetDelivery(ctx context.Context, id int64, httpStatus int, errMsg string, at time.Time) error
// Replica rules + failures + report + settings
ListReplicaRules(ctx context.Context) ([]*model.ReplicaRule, error)
GetReplicaRule(ctx context.Context, id int64) (*model.ReplicaRule, error)
CreateReplicaRule(ctx context.Context, in *model.ReplicaRuleInput) (*model.ReplicaRule, error)
UpdateReplicaRule(ctx context.Context, id int64, in *model.ReplicaRuleInput) (*model.ReplicaRule, error)
DeleteReplicaRule(ctx context.Context, id int64) error
UpsertReplicaFailure(ctx context.Context, path, op, errCode, errMsg string) error
ResolveReplicaFailure(ctx context.Context, path, op string) error
ListReplicaFailures(ctx context.Context, onlyUnresolved bool, limit, offset int) ([]*model.ReplicaFailure, int64, error)
CountUnresolvedReplicaFailures(ctx context.Context) (int64, error)
CountRecentlyResolvedReplicaFailures(ctx context.Context, since time.Time) (int64, error)
UpsertReplicaStatusReport(ctx context.Context, total, failed, repaired int64, summaryJSON []byte) error
GetReplicaStatusReport(ctx context.Context) (*model.ReplicaStatusReport, error)
GetReplicaSettings(ctx context.Context) (*model.ReplicaSettings, error)
UpsertReplicaSettings(ctx context.Context, s *model.ReplicaSettings) error
/* calisma:d3 comments */
// Node comments (migration 00020) — flat chronological threads on
// file/folder nodes. List excludes soft-deleted rows and joins the
// author display name; hard removal happens via the nodes FK CASCADE
// plus DeleteNodeCommentsByNode in the trash purge hook.
CreateNodeComment(ctx context.Context, c *model.NodeComment) (*model.NodeComment, error)
GetNodeComment(ctx context.Context, id int64) (*model.NodeComment, error)
ListNodeComments(ctx context.Context, nodeID int64) ([]*model.NodeComment, error)
SoftDeleteNodeComment(ctx context.Context, id int64) error
DeleteNodeCommentsByNode(ctx context.Context, nodeID int64) error
// Drafts (migration 00064, issue #71) — a new document before its first
// save. The rows are read joined to the draft file's node row: Path, Size,
// Mime, Mtime and NodeLive come from there. A draft whose node is trashed
// is kept (a restore brings it back); a draft whose node is purged goes
// with it (FK CASCADE).
CreateDraft(ctx context.Context, d *model.Draft) (*model.Draft, error)
// GetDraftByKey returns the draft with that key whatever its node's state,
// sql.ErrNoRows when there is none.
GetDraftByKey(ctx context.Context, key string) (*model.Draft, error)
// GetDraftByNode returns the draft whose file is that node, live or
// trashed, sql.ErrNoRows when there is none.
GetDraftByNode(ctx context.Context, nodeID int64) (*model.Draft, error)
// ListLiveDrafts returns a person's drafts whose file is live, the most
// recently made first.
ListLiveDrafts(ctx context.Context, userID int64) ([]*model.Draft, error)
// CountLiveDrafts is len(ListLiveDrafts) without reading the rows — the
// limit check and the navigation panel's badge.
CountLiveDrafts(ctx context.Context, userID int64) (int, error)
DeleteDraft(ctx context.Context, id int64) error
// Plugin install requests (migration 00070, internal/pluginreq) — what an
// API key leaves instead of installing a plugin, for an administrator to
// approve or reject. Written once for every engine (PluginRequestSQL).
CreatePluginRequest(ctx context.Context, r *model.PluginRequest) (*model.PluginRequest, error)
// GetPluginRequest answers sql.ErrNoRows when there is no such request.
GetPluginRequest(ctx context.Context, id int64) (*model.PluginRequest, error)
// ListPluginRequests: one state ("" = all), newest first, at most limit.
ListPluginRequests(ctx context.Context, status string, limit int) ([]*model.PluginRequest, error)
// PendingPluginRequestBySource answers sql.ErrNoRows when no request for
// that source is pending.
PendingPluginRequestBySource(ctx context.Context, sourceKey string) (*model.PluginRequest, error)
// UpdatePluginRequest writes the decision fields; with onlyIfPending only
// while the stored row is still pending (ok = it was written).
UpdatePluginRequest(ctx context.Context, r *model.PluginRequest, onlyIfPending bool) (bool, error)
// File associations (migration 00077, internal/assoc): the administrator's
// rule per kind of file and capability, and each app's thumbnail limits.
// Written once for every engine (FileAssocSQL).
ListFileAssociations(ctx context.Context) ([]*model.FileAssociation, error)
PutFileAssociation(ctx context.Context, a *model.FileAssociation) error
// DeleteFileAssociation: ok = there was a rule.
DeleteFileAssociation(ctx context.Context, capability, ext string) (bool, error)
// GetAppThumbLimits answers (nil, nil) when the app has none stored.
GetAppThumbLimits(ctx context.Context, pluginID int64) (*model.AppThumbLimits, error)
PutAppThumbLimits(ctx context.Context, l *model.AppThumbLimits) error
DeleteAppThumbLimits(ctx context.Context, pluginID int64) error
// ThumbnailGenerators counts the ready thumbnails by who drew them
// (thumbnails.generator; "" for rows from before 0.50 and placeholder
// cards). nil storageIDs is every storage; an empty slice is none.
ThumbnailGenerators(ctx context.Context, storageIDs []int64) (map[string]int64, error)
// Sign-in attempt counters (migration 00072, internal/loginguard): how many
// wrong attempts an account identifier or an address has made, and until
// when it is locked. Written once for every engine (LoginThrottleSQL).
// GetLoginThrottle answers (nil, nil) when nothing was counted.
GetLoginThrottle(ctx context.Context, scope, subject string) (*model.LoginThrottle, error)
// SaveLoginThrottle creates or overwrites the counter for (Scope, Subject).
SaveLoginThrottle(ctx context.Context, t *model.LoginThrottle) error
// DeleteLoginThrottle removes one counter; ok = there was one.
DeleteLoginThrottle(ctx context.Context, scope, subject string) (bool, error)
// ListLoginThrottles: one scope ("" = all), only rows locked at lockedAt
// when it is non-nil, most recently active first, at most limit.
ListLoginThrottles(ctx context.Context, scope string, lockedAt *time.Time, limit int) ([]*model.LoginThrottle, error)
// LoadLoginThrottles returns every counter PruneLoginThrottles(since)
// would keep (a failure at or after since, or a lock running past it),
// most recently active first, at most limit (<= 0: no limit). It is how
// the limiter fills its memory at start.
LoadLoginThrottles(ctx context.Context, since time.Time, limit int) ([]*model.LoginThrottle, error)
// PruneLoginThrottles drops counters idle since before; n = rows removed.
PruneLoginThrottles(ctx context.Context, before time.Time) (int64, error)
// Providers (tenants). See docs/MULTI-TENANCY.md. Inert while multi-tenant
// mode is off; a single "default" provider always exists (migration 00014).
CreateProvider(ctx context.Context, p *model.Provider) (*model.Provider, error)
GetProvider(ctx context.Context, id int64) (*model.Provider, error)
GetProviderBySlug(ctx context.Context, slug string) (*model.Provider, error)
// GetProviderByHost resolves a request Host to its tenant; returns nil if no
// enabled provider claims that host.
GetProviderByHost(ctx context.Context, host string) (*model.Provider, error)
// GetProviderByRealm resolves a realm (migration 00073) to its tenant, or
// nil. The realm is compared normalised (package tenant); "" is nobody's.
GetProviderByRealm(ctx context.Context, realm string) (*model.Provider, error)
ListProviders(ctx context.Context) ([]*model.Provider, error)
// UpdateProvider writes every column EXCEPT realm: a realm is set when the
// tenant is created (CreateProvider) and never changes (package tenant).
UpdateProvider(ctx context.Context, p *model.Provider) error
DeleteProvider(ctx context.Context, id int64) error
// GetSupertenant returns the single is_supertenant provider, or nil.
GetSupertenant(ctx context.Context) (*model.Provider, error)
// Provider ↔ storage links (M:N; 1:1 in the first UI).
LinkProviderStorage(ctx context.Context, providerID, storageID int64) error
UnlinkProviderStorage(ctx context.Context, providerID, storageID int64) error
ListProviderStorageIDs(ctx context.Context, providerID int64) ([]int64, error)
// GetProviderIDForStorage returns the (first) provider a storage is linked
// to — used by background workers to derive tenancy from a storage.
GetProviderIDForStorage(ctx context.Context, storageID int64) (int64, bool, error)
/* kimlik:e3 cloud */
// Provider plan metadata (cloud preparation, migration 00021 — see
// docs/CLOUD.md). The columns are nullable and PASSIVE: only the
// FILEX_CLOUD signup skeleton reads/writes them; with the flag off
// (default) nothing touches these methods.
SetProviderPlan(ctx context.Context, providerID int64, plan, limitsJSON, billingRef string) error
GetProviderPlan(ctx context.Context, providerID int64) (plan, limitsJSON, billingRef string, err error)
// Tenant self-service (migration 00076, docs/TENANT-ADMIN.md; TenantAuthSQL,
// one implementation for every engine).
//
// Sign-in provider instances and which tenant signs in through which.
ListAuthInstances(ctx context.Context) ([]*model.AuthInstance, error)
GetAuthInstance(ctx context.Context, id int64) (*model.AuthInstance, error)
GetAuthInstanceBySlug(ctx context.Context, slug string) (*model.AuthInstance, error)
CreateAuthInstance(ctx context.Context, a *model.AuthInstance) (*model.AuthInstance, error)
// UpdateAuthInstance writes label, enabled, legacy, configuration and
// promoted scopes; never the slug, driver, origin or owner.
UpdateAuthInstance(ctx context.Context, a *model.AuthInstance) error
DeleteAuthInstance(ctx context.Context, id int64) error
ListAuthBindings(ctx context.Context) ([]*model.AuthBinding, error)
BindAuthInstance(ctx context.Context, providerID, instanceID int64, source string) error
UnbindAuthInstance(ctx context.Context, providerID, instanceID int64) error
// SetProviderAllowInsecureAuth is the operator's per-tenant switch;
// UpdateProvider never writes the column.
SetProviderAllowInsecureAuth(ctx context.Context, providerID int64, allow bool) error
// A tenant's own domains (one tenant per domain).
ListProviderDomains(ctx context.Context, providerID int64) ([]*model.ProviderDomain, error)
GetProviderDomain(ctx context.Context, domain string) (*model.ProviderDomain, error)
GetProviderDomainByID(ctx context.Context, id int64) (*model.ProviderDomain, error)
CreateProviderDomain(ctx context.Context, d *model.ProviderDomain) (*model.ProviderDomain, error)
SetProviderDomainStatus(ctx context.Context, id int64, status string, why model.DomainCheck, checkedAt time.Time) error
SetProviderDomainCert(ctx context.Context, id int64, certPEM, keySealed string, notAfter *time.Time) error
DeleteProviderDomain(ctx context.Context, id int64) error
ProviderIDByActiveDomain(ctx context.Context, domain string) (int64, error)
}
Store is the interface implemented by every dialect-specific query adapter. Methods are intentionally tiny domain operations — handlers should never reach into *sql.DB directly.
In a fully-generated setup this surface would be sqlc's Querier interface; here we hand-roll it so the skeleton compiles without a `sqlc generate` step.
type TenantAuthSQL ¶ added in v0.50.0
type TenantAuthSQL struct {
// contains filtered or unexported fields
}
TenantAuthSQL implements the tenant self-service half of Store (migration 00076, docs/TENANT-ADMIN.md): sign-in provider instances, which tenant signs in through which, a tenant's own domains, and the operator's per-tenant switch for insecure providers. Written ONCE for every engine and embedded in each driver's Store, the arrangement of GroupSQL.
Every statement runs on Conn(ctx, pool), so a caller's RunInTx holds it too; nothing here opens a transaction of its own except where two statements must agree (BindAuthInstance), and that one joins the caller's.
func NewTenantAuthSQL ¶ added in v0.50.0
func NewTenantAuthSQL(conn *sql.DB, dollar bool) *TenantAuthSQL
NewTenantAuthSQL returns the store for conn; dollar for PostgreSQL.
func (*TenantAuthSQL) BindAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) BindAuthInstance(ctx context.Context, providerID, instanceID int64, source string) error
BindAuthInstance binds a tenant to an instance. Binding twice is not an error, and the first binding's source is kept.
Check, then insert, inside one transaction (the caller's when it has one): no upsert clause, so nothing differs per engine.
func (*TenantAuthSQL) CreateAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) CreateAuthInstance(ctx context.Context, a *model.AuthInstance) (*model.AuthInstance, error)
CreateAuthInstance inserts an instance. A slug another instance has is the unique index's refusal.
func (*TenantAuthSQL) CreateProviderDomain ¶ added in v0.50.0
func (s *TenantAuthSQL) CreateProviderDomain(ctx context.Context, d *model.ProviderDomain) (*model.ProviderDomain, error)
CreateProviderDomain adds a domain to a tenant, pending. A domain any tenant already has is the unique index's refusal (one tenant at a time).
func (*TenantAuthSQL) DeleteAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) DeleteAuthInstance(ctx context.Context, id int64) error
DeleteAuthInstance removes an instance; its bindings go with it.
func (*TenantAuthSQL) DeleteProviderDomain ¶ added in v0.50.0
func (s *TenantAuthSQL) DeleteProviderDomain(ctx context.Context, id int64) error
DeleteProviderDomain removes a domain from its tenant.
func (*TenantAuthSQL) GetAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) GetAuthInstance(ctx context.Context, id int64) (*model.AuthInstance, error)
GetAuthInstance returns one instance, or (nil, nil).
func (*TenantAuthSQL) GetAuthInstanceBySlug ¶ added in v0.50.0
func (s *TenantAuthSQL) GetAuthInstanceBySlug(ctx context.Context, slug string) (*model.AuthInstance, error)
GetAuthInstanceBySlug returns one instance by its slug, or (nil, nil).
func (*TenantAuthSQL) GetProviderDomain ¶ added in v0.50.0
func (s *TenantAuthSQL) GetProviderDomain(ctx context.Context, domain string) (*model.ProviderDomain, error)
GetProviderDomain returns the row of a domain (lower case), or (nil, nil).
func (*TenantAuthSQL) GetProviderDomainByID ¶ added in v0.50.0
func (s *TenantAuthSQL) GetProviderDomainByID(ctx context.Context, id int64) (*model.ProviderDomain, error)
GetProviderDomainByID returns one row, or (nil, nil).
func (*TenantAuthSQL) ListAuthBindings ¶ added in v0.50.0
func (s *TenantAuthSQL) ListAuthBindings(ctx context.Context) ([]*model.AuthBinding, error)
ListAuthBindings returns every binding, by tenant then instance.
func (*TenantAuthSQL) ListAuthInstances ¶ added in v0.50.0
func (s *TenantAuthSQL) ListAuthInstances(ctx context.Context) ([]*model.AuthInstance, error)
ListAuthInstances returns every instance, in id order.
func (*TenantAuthSQL) ListProviderDomains ¶ added in v0.50.0
func (s *TenantAuthSQL) ListProviderDomains(ctx context.Context, providerID int64) ([]*model.ProviderDomain, error)
ListProviderDomains returns a tenant's domains (every tenant's for 0), by domain.
func (*TenantAuthSQL) ProviderIDByActiveDomain ¶ added in v0.50.0
ProviderIDByActiveDomain returns the tenant an ACTIVE own domain routes to, 0 for none. It does not read the providers row itself: the engine's GetProviderByHost does, so the row is scanned the engine's way and its `enabled` is judged there.
func (*TenantAuthSQL) SetProviderAllowInsecureAuth ¶ added in v0.50.0
func (s *TenantAuthSQL) SetProviderAllowInsecureAuth(ctx context.Context, providerID int64, allow bool) error
SetProviderAllowInsecureAuth is the platform operator's per-tenant switch (providers.allow_insecure_auth). Its own statement: UpdateProvider never writes the column, so a tenant edit cannot carry it along.
func (*TenantAuthSQL) SetProviderDomainCert ¶ added in v0.50.0
func (s *TenantAuthSQL) SetProviderDomainCert(ctx context.Context, id int64, certPEM, keySealed string, notAfter *time.Time) error
SetProviderDomainCert stores (or, with empty values, removes) the certificate a tenant brought for its domain. The key arrives sealed.
func (*TenantAuthSQL) SetProviderDomainStatus ¶ added in v0.50.0
func (s *TenantAuthSQL) SetProviderDomainStatus(ctx context.Context, id int64, status string, why model.DomainCheck, checkedAt time.Time) error
SetProviderDomainStatus records a check: the state it left the domain in, what it found when that was not the CNAME (the sentence, and its code and names for a screen to say), and when. active_since is set when the domain becomes active and kept while it stays so; it is cleared when it stops being active.
func (*TenantAuthSQL) UnbindAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) UnbindAuthInstance(ctx context.Context, providerID, instanceID int64) error
UnbindAuthInstance removes one binding (nothing when there is none).
func (*TenantAuthSQL) UpdateAuthInstance ¶ added in v0.50.0
func (s *TenantAuthSQL) UpdateAuthInstance(ctx context.Context, a *model.AuthInstance) error
UpdateAuthInstance writes what may change about an instance: its label, switch, legacy mark, configuration and promoted scopes. ⚠ Never the slug, the driver, the origin or the owner: an instance is what it was made as, and a tenant's own instance never becomes another tenant's.
type TrashTally ¶
TrashTally is one storage's share of the trash a purge sweep will walk.
type UnavailableEntry ¶ added in v0.50.0
type UnavailableEntry struct {
}
UnavailableEntry is the row that makes a path unavailable: the path itself, or the folder above it that the storage could not answer for.
type UserPrefsDoc ¶ added in v0.50.0
NodeAgg is a lightweight node row used for folder-size aggregation (internal/sync.RecomputeFolderSizes): just enough to walk the tree and sum descendant file sizes into each folder's cached size. UserPrefsDoc is one stored preference document (00047): the surface it belongs to, the JSON and when it was last written.
type UserTenant ¶ added in v0.50.0
UserTenant is the tenant an account lookup stays inside: the tenant's row, and Main for the platform's own tenant, which also owns an account with no tenant at all (provider_id NULL, a bootstrap or legacy row; the same rule as identity.Tenant.Owns).
type VanishedSQL ¶ added in v0.48.0
type VanishedSQL struct {
Pool *sql.DB
// Placeholders turns `?` into the engine's own (PostgreSQL:
// DollarPlaceholders). Nil for SQLite and MySQL.
Placeholders func(q string) string
}
VanishedSQL implements Store.ListVanishedNodeIDs and Store.CountChildRows (issue #74), written ONCE for every engine. Each driver embeds a *VanishedSQL in its Store, so the methods are promoted and there is no per-driver copy to drift (the arrangement of NodeDeletedBySQL).
A "vanished" row is one soft-deleted WHERE IT STOOD: deleted_at is set and its path is still where the file lived, not a key inside `.filex-trash/`. Its bytes were never parked in the trash — the storage sync found the file gone (every version up to 0.47), or the bytes were deleted outright — so it is not a trash entry and nothing can restore it. See trash.Vanished.
func (*VanishedSQL) CountChildRows ¶ added in v0.48.0
CountChildRows counts the rows, live or deleted, whose parent_id is parentID — everything the parent_id cascade would take with the row.
func (*VanishedSQL) ListVanishedNodeIDs ¶ added in v0.48.0
func (o *VanishedSQL) ListVanishedNodeIDs(ctx context.Context, storageID, afterID int64, limit int) ([]int64, error)
ListVanishedNodeIDs returns up to limit ids, above afterID and in id order, of storageID's vanished rows: deleted, and with a path that is neither the trash directory nor inside it, in either spelling the path column carries.
⚠ The trash prefix is matched with SUBSTR on the CHARACTER count (see PrefixChars), never LIKE, like every other subtree match in this package.
Source Files
¶
- catalogue_folders_sql.go
- catalogue_scan.go
- customtheme_scan.go
- drafts_sql.go
- driver.go
- file_assoc_sql.go
- group_sql.go
- login_throttle_sql.go
- migrate.go
- node_deleted_by_sql.go
- node_unavailable_sql.go
- oidc_identity_sql.go
- permcodec.go
- permission_scan.go
- plugin_requests_sql.go
- provider_realm.go
- registry.go
- storage_order_sql.go
- store.go
- tagsql.go
- tenant_auth_sql.go
- tenant_host.go
- tx.go
- userprefs_list_sql.go
- vanished_sql.go
Directories
¶
| Path | Synopsis |
|---|---|
|
drivers
|
|
|
mysql
Package mysql is the MySQL/MariaDB DB driver.
|
Package mysql is the MySQL/MariaDB DB driver. |
|
postgres
Package postgres is the PostgreSQL DB driver.
|
Package postgres is the PostgreSQL DB driver. |
|
sqlite
Package sqlite is the SQLite DB driver.
|
Package sqlite is the SQLite DB driver. |