Documentation
¶
Overview ¶
Package handlers — account_rules.go
What an account's e-mail and username may be, and how a refusal is said. Used by the self-service profile (auth_self.go) and by an administrator adding a user (users.go), so the two forms refuse the same things in the same words.
⚠⚠ Why this file exists (release-candidate sweep, 2026-09-21):
- the profile form saved "bu-bir-eposta-degil" as an e-mail address and answered "Profil kaydedildi"; logging in with an e-mail then failed with 401, because the account no longer had one;
- changing to another account's address answered 200 with the success message and changed nothing: UpdateUserEmail's unique-constraint error was thrown away (`_ = h.Store.UpdateUserEmail(...)`);
- a username with "ş" in it came back as raw English — `invalid username: 'ş' is not allowed (use a-z, 0-9, dot, dash, underscore)` — in the Turkish panel.
A refusal is `{"error": <code>, "message": <sentence in the reader's language>, "field": "email"|"username"}`: the code for code, the sentence for the person (extractError prefers `message`), the field so a form can put the sentence under the box it is about.
Package handlers - ai_doors.go
The explorer's operations behind the AI surface (task #114): copy, an app's action (file_convert), the operations queue, the trash, version history, 7z/TAR archives, one's own links and file requests - each an MCP tool and a REST route under /api/ai, both answering what the explorer's own route answers.
⚠⚠ They are not written again here. Each one runs the handler the explorer calls (Ops, AppPlugins, Trash, Versions, Archive, SharesMine, Share), in process, behind the same root confinement middleware - so the permission checks, the tenant boundary, the app rules (min_role, admin_only, applies, hidden actions) and the answers are the explorer's by construction, and a fix to one door is a fix to all three. What this file adds is only what the AI surface owes on top, before the handler runs:
- paths in the AI surface's own form (aiOps.resolveStorage): a bare path of a `root:` token resolves under its root, the bound user needs at least viewer, filex's own folders do not exist;
- end-to-end encryption with the AI surface's codes (ai_e2e.go): E2E_BOUNDARY, E2E_PLAINTEXT_REFUSED, E2E_ENCRYPTED - the explorer's route refuses some of these too, but without the code an agent matches on;
- no key: every write the handler judges through gate() - and every member an archive extraction lands - is judged as the AI surface's own (syspath.Keyless), so an encrypted folder's key file is never written, copied over or restored from here (keylessDoor).
The audit row of a write is its REST route's (auth.ActionForPath), written by the /api/ai group's AuditMiddleware for REST and by mcpAuditRow for the MCP tool, with the facts the handler adds (SetAuditTarget, AddAuditDetail) in both - the door passes the recorder through, it never opens a second one.
Package handlers - ai_doors_people.go
What an agent does around a file besides its bytes (task #119, item 4): the bell (notifications_list, notification_read), a star (file_star), comments (file_comments, file_comment_add, file_comment_delete) and who may open an item (file_permissions, file_permission_users, file_permission_set, file_permission_revoke) - each an MCP tool and a REST route under /api/ai.
Like ai_doors.go these run the explorer's own handlers in process: the bell's (/api/notifications, root-confined), the star's, the comments' (with the comments.write permission its route asks) and the item permissions' (owner level and share.users, RBAC.md) - nothing is decided a second time here.
Package handlers - ai_e2e.go
End-to-end encryption on the AI surface: GET/POST /api/ai/* and the MCP file tools, and everything that writes through the same core (ShareX, the upload ticket).
The surface holds no key and never will, so it cannot decrypt a file or encrypt one. Before this file it did not know that either: file_read handed an agent an encrypted PDF as `mime: application/pdf, encoding: base64` (the agent took it for a broken PDF), file_list showed the folder's key file, and file_write put plaintext into an encrypted folder without a word (measured on v0.48.1, task #113). What it does now, each from one place:
- every row says `encrypted` and `e2e_root` (e2eRoots.mark, the rules the explorer's rows use);
- a read of ciphertext is refused with E2E_ENCRYPTED (encryptedRefusal);
- a write into an encrypted folder is refused with E2E_PLAINTEXT_REFUSED unless the caller says `allow_plaintext` (plaintextRefusal);
- a copy across the boundary is refused with E2E_BOUNDARY (e2e.GuardTransfer, the rule every transfer surface asks);
- the key file is left out of listings (syspath.Unlisted) and never written, moved or deleted from here (syspath.Keyless, through aiOps.gate);
- a public link into an encrypted folder is refused (publicLinkRefusal).
Recognising any of it is a catalogue lookup for the marker's row and a name test for `.fxe`: no content is read and no key is asked for.
Package handlers — ai_tags.go
Tags for agents: GET/POST /api/ai/tags and the MCP `file_tags` tool.
Until v0.43.0 an agent could FILTER by tag (`tag:` in file_search) but could neither read a file's tags nor set them — and there was one kind. With two kinds the agent has to be able to say which one it means, so this surface speaks the same {name, kind} items as the explorer's routes and runs the very same rules (tagOps, tags.go): personal = only the token's user sees it; team = the tenant, and needs edit permission on the file.
⚠ It never has a default kind. The explorer's legacy `tags: [names]` shape defaults new names to personal for old clients; an agent is new code, so it must name the kind of every tag it writes — an agent quietly publishing a label to the whole tenant, or hiding one the user meant to share, is exactly the kind of silent audience change the finding was about.
Package handlers — app_plugins.go
User surface for app plugins (internal/wasmplugin):
GET /api/files/plugins/actions — what applies to this caller
POST /api/files/plugins/actions/{plugin}/{action}/run — {storage_id, paths[], params} → 202 op | 200 surface
GET /api/files/plugins/views/{plugin}/{view} — ?storage_id=&path= → surface (event "open")
POST /api/files/plugins/views/{plugin}/{view}/event — {storage_id, path, state, event, action_id, data} → surface | 202 op
Every run is authorised HERE, at submit: tenant ownership of the storage, ACL on every path (viewer to read, editor when the action writes), read-only storages refused for writing actions, encrypted folders refused (the server has no key), and the action's applies rule re-checked against the real files — the menu's client-side filter is a convenience, not a gate. The ops worker that later runs the job has no user of its own.
Package handlers — app_plugins_admin.go
Admin surface for app plugins (internal/wasmplugin, docs/APP-PLUGINS.md):
GET /api/admin/app-plugins — runtime facts + every plugin
POST /api/admin/app-plugins[?dry_run=1] — install: multipart {wasm?, ui?, manifest, signature?, grant} | JSON {github_repo, ref?, permissions} | JSON {url?, manifest_url, sha256?, permissions}
(no module — `wasm`/`url` — for a language pack; wire.Manifest.IsLanguagePack)
GET /api/admin/app-plugins/{id}
PATCH /api/admin/app-plugins/{id} — {"enabled": bool}
POST /api/admin/app-plugins/{id}/rollback — back to the version the last upgrade replaced
POST /api/admin/app-plugins/{id}/upgrade — same bodies as install
POST /api/admin/app-plugins/updates/check — check every app's source now (updates.go)
DELETE /api/admin/app-plugins/{id}
GET/PUT /api/admin/app-plugins/{id}/settings
GET/PUT /api/admin/app-plugins/{id}/overrides
GET /api/admin/app-plugins/{id}/logs?after=N
GET/PUT /api/admin/app-plugins/{id}/thumbnails - the app's thumbnail limits (internal/wasmplugin/thumbnails.go)
⚠ Instance-wide, never tenant-scoped, for the same reason storage plugins are: a plugin's actions appear in every tenant's file menu. Only the supertenant may touch this surface.
⚠⚠ Installing, upgrading, going back, switching, removing and the action overrides need an administrator SIGNED IN to the panel (requireSession): an API key gets 403 and is pointed at /api/admin/plugin-requests. The review (?dry_run=1), the list, a detail, the logs and the update check stay open to an admin-scoped key.
Package handlers - app_store_intent.go
Installing an app from a store, and the licenses of paid apps (internal/appstore, docs/APP-PLUGINS.md → Installing from a store, Paid apps; docs/APP-PLUGINS-API.md → The store contract):
GET /api/admin/app-plugins/stores - the trusted stores
POST /api/admin/app-plugins/stores - {store, fingerprints}: trust a store with the keys shown
DELETE /api/admin/app-plugins/stores?store=<origin> - stop trusting it
POST /api/admin/app-plugins/store-intent - {store, token}: read an install link → its review
POST /api/admin/app-plugins/store-intent/install - {handle, permissions, associations?, license_key?}
POST /api/admin/app-plugins/store-intent/cancel - {handle}
GET /api/admin/app-plugins/licenses - every paid app's license (the panel's warning band)
GET /api/admin/app-plugins/{id}/license
PUT /api/admin/app-plugins/{id}/license - {key}: a new key, checked at once
POST /api/admin/app-plugins/{id}/license/verify - check now
GET /api/files/plugins/license/{plugin} - what the app reads about itself (fx.license.get())
Who may (the reasoning of app_plugins_admin.go and plugin_session_gate.go) ¶
Every admin route here is the PLATFORM OPERATOR's (requireSupertenant: an app runs for every tenant, so a tenant administrator gets 403) and needs an administrator SIGNED IN to the panel (requireSession): an API key of any scope - an admin key, a `root:`-confined key, an app token - is refused 403 session_required, on the reads too. Installing is already a person's decision; trusting a store decides whose install links are shown to that person at all, and a license key is not for a program to read back.
⚠ GET changes nothing here. Trusting a store is its own POST, naming the fingerprints the administrator was shown; the session cookie is SameSite Lax and every state-changing request passes the origin guard (internal/originguard), so a link or a page elsewhere cannot trust a store for anybody.
Package handlers — app_ui.go
The server half of an app's OWN interface (docs/APP-PLUGINS-API.md → An app's own interface). The interface runs in a sandboxed frame with no network; what it asks for goes through filex's frame (AppFrame, the explorer), which calls these with the PERSON's session:
PUT /api/files/plugins/ui/{plugin}/{view}/save?path=<qualified> — a new version of an opened file
PUT /api/files/plugins/ui/{plugin}/{view}/save?dir=<qualified>&name= — a NEW file there ("save as")
POST /api/files/plugins/ui/{plugin}/{view}/call — the app's module (`ui_call`)
⚠⚠ Every check is made again HERE, whatever the frame already decided: the app is running, the view is its interface, the app was granted what the call needs (files:write for a save), the file is one the view applies to, and the person may write it (confinement, ACL editor, the write gate — locks, filex's own folders, a draft only its owner's). The frame is the person's own page, but a check that lives only in a page is a check a crafted request skips.
Package handlers — app_ui_call.go
POST /api/files/plugins/ui/{plugin}/{view}/call — an app's own interface asking its module (`ui_call`, docs/APP-PLUGINS-API.md → An app's own interface). The save half is app_ui.go.
Package handlers — app_ui_chunks.go
An app's interface saving a LARGE file: the explorer streams what the interface hands it (a ReadableStream, transferred into the explorer's page) in chunks of at most 8 MiB, each one request — a reverse proxy with a body limit (nginx's default is 1 MiB) lets every one of them through — into a spool file, and the file is written to the storage ONCE, when the last chunk arrives, through the same checks and the same commit as a one-shot save (app_ui.go):
PUT …/save?path=<q>&chunk=start (body: first chunk) → {session, received}
PUT …/save?session=<id>&offset=<n> (body: next chunk) → {session, received}
PUT …/save?session=<id>&offset=<n>&final=1 (body: last chunk) → the save's answer
A session belongs to the person, the app, the view and the target it was started for; another caller naming it gets 404. An abandoned one is removed after uiChunkTTL.
Package handlers — audit.go ¶
GET /api/admin/audit?user_id=&action=&from=&to=&limit=50&offset=0
Package handlers — audit_targets.go
WHICH thing an audit row is about, in words a person reads.
⚠⚠ The Panel's Recent activity and the Audit page printed the KIND of thing and nothing else — "Kullanıcı: oluşturuldu — Kullanıcı", "Depo: senkron başlatıldı — Depo #1" (release-candidate sweep, 2026-09-21). A row stores a target TYPE and an id; the id of a user, a storage or a share is a number nobody can read, and a create has no id at all (its URL has none and the middleware never reads bodies).
Two halves, one answer (`target_name` on the wire):
- write time: the handler names what it made or removed (auth.SetAuditTarget → metadata.target_name). This is the only answer for something that no longer exists — "which user was deleted?";
- read time: an id that still resolves is named from the live row, so the rows written before this change read properly too.
⚠ Resolved through the SCOPED store the handler already holds: a tenant admin's list is filtered to its own users' rows (audit.go), and a name is only ever looked up for a row that survived that filter.
Package handlers — auth_providers.go
Admin → Identity providers: the sign-in providers this instance runs.
GET /api/admin/auth-providers
POST /api/admin/auth-providers (another instance of a driver)
PATCH /api/admin/auth-providers/{name}
DELETE /api/admin/auth-providers/{name} (an instance made with POST)
PUT /api/admin/auth-providers/{name}/tenants (which tenants sign in through it)
POST /api/admin/auth-providers/{name}/test
{name} is the provider's slug: the driver's name for its first instance (`ldap`, `oidc`, ...), so every client that addressed "the LDAP" still does (docs/TENANT-ADMIN.md).
⚠⚠ Until v0.43.0 this page wrote `auth.<name>.*` settings that NOTHING read — sign-in was built from the environment only, once, at boot — and answered "configuration saved; restart the server" to a restart that changed nothing (release-candidate sweep, 2026-09-21). The owner's decision: the page really manages sign-in, and it can never lock the instance out. The rules live in internal/authsetup; this file enforces them at the door:
- a provider the environment defines is read-only here, and says where it comes from (409 environment_managed on a change);
- password sign-in and the recovery sign-in are the environment's — the page has no switch for either (409 not_managed_here);
- a save runs the real test first; switching a provider ON while its test fails needs `confirm_failed_test` (409 test_failed names the steps) - except an operating-system provider (windows), whose only proof is a real sign-in: its test must PASS, with `test_account` {username, password} sent on that request (used once, never stored or logged), and the account that passed becomes the super administrator (409 test_failed, no way round);
- switching off the last way an administrator can sign in is refused (409 last_sign_in_method); on a multi-tenant install a tenant's last way in is refused too unless the operator confirms it (`confirm_tenant_lockout`): the operator can always bind something back;
- a secret is sealed (FILEX_SECRET_KEY) and never sent back;
- a change is applied at once (authsetup.Live.Reload) and audited with the provider, the switch and the NAMES of the fields that changed.
Instance-wide: only the platform operator (supertenant admin) sees or changes any of it. A tenant administrator gets 403 — the sign-in of the whole instance, and its secrets, are not a tenant's to read; a tenant's own sign-in lives on its provider row (/api/admin/providers).
Package handlers — auth_self.go
Self-service auth endpoints (current user). All routes require an authenticated session and act on the principal in the request context.
GET /api/auth/me — current user PATCH /api/auth/profile — update email/username/locale/timezone POST /api/auth/password — change password (requires old) POST /api/auth/totp/enroll — start TOTP enrollment POST /api/auth/totp/verify — confirm TOTP enrollment with code POST /api/auth/totp/disable — turn TOTP off (password + code)
Package handlers — comments.go (calisma:d3, v0.6 "Çalışma" (Work))
Node comment endpoints under /api/files:
GET /api/files/comments?node_id=… (auth) chronological live list
POST /api/files/comments (auth) {node_id, body} add
DELETE /api/files/comments/{id} (auth) author-or-admin soft delete
ACL: anyone who can SEE the node (≥viewer, same aclAllowID gate the other file handlers use) may read and write its thread; delete is author-or-admin regardless of node level (enforced in internal/comments). Trashed/missing nodes 404 on both read and write.
Package handlers — confine_guard.go
The token `root:` confinement, for the surfaces confine.Middleware cannot reach.
confine.Middleware (mounted on /api/files) rewrites the `?path=` query and the path fields of a JSON body, so every path-addressed read and every mutation is confined by construction. It cannot touch two shapes:
- an id-addressed request (`?id=`, `node_id`, a raw `?storage=`), because a numeric id is not a path there is nothing to rewrite; and
- a listing or search that walks a whole storage, because the rows come back AFTER the request the middleware saw.
Those are exactly the shapes handlers/thumb.go, handlers/versions.go, handlers/trash.go, handlers/shared.go and handlers/quota_storages.go already gate with confine.Root.Within. This file is the ONE place the rest of the package answers the same question, so a sixth copy of "resolve the storage, then Within" cannot drift from the other five. It is the confinement twin of the tenant guards in tenantown.go.
Package handlers — dashboard.go ¶
GET /api/admin/dashboard returns aggregated stats for the admin landing page: per-storage summary, user/share/session counts, queue depth, recent activity and a short capability summary.
Package handlers — demo_redact.go
What a public demo shows in the read-only admin pages it deliberately keeps visible. The demo guard (api/demo_guard.go) stops a visitor CHANGING the instance; this file stops the pages a visitor may still READ from handing out other people's data and the operator's secrets.
Package handlers — duplicates.go ¶
GET /api/admin/duplicates?limit=100&min_size=1 — admin-only report of files sharing (size, etag), i.e. byte-identical copies as far as the storage backend can tell (frozen v0.2 "Bul" contract #2).
Grouping runs in SQL (db.Store.ListDuplicateNodes: GROUP BY size, etag HAVING COUNT(*)>1 on live file nodes with a non-empty etag); the handler folds the flat rows into groups, computes total_waste = (count-1)*size and sorts groups by total_waste descending.
Package handlers — e2e_policy_admin.go
Who may encrypt (internal/e2epolicy), the administrator's half:
GET /api/admin/e2e — the policy the caller administers
PATCH /api/admin/e2e — SESSION ONLY: {"policy": "off|admins|permitted|approval"}
GET /api/admin/e2e/tenants — SUPERTENANT: every tenant's ceiling and policy
PATCH /api/admin/e2e/tenants/{id} — SUPERTENANT, SESSION ONLY: {"e2e_allowed": bool}
GET /api/admin/e2e/requests[?status=pending|approved|rejected|expired|used|all]
POST /api/admin/e2e/requests/{id}/approve — SESSION ONLY: {"note"?: "…"}
POST /api/admin/e2e/requests/{id}/reject — SESSION ONLY: {"reason"?: "…"}
A tenant administrator lists and decides their own tenant's requests only; another tenant's id answers 404, like an id nobody has (tenantown.go). The platform operator (the supertenant's administrator on a multi-tenant install) lists every tenant's requests and decides only the platform's own: another tenant's answers 403 not_decidable, and its row says `decidable: false` (operator decision 2026-10-03).
Whose policy "the caller administers" is decided by the tenant scope auth.TenantResolver attached, never by the request: a tenant administrator holds their own tenant's row, the supertenant's administrator the supertenant's own row — another tenant's policy is that tenant's decision; the operator's lever is the ceiling — and a single-tenant install's administrator the instance setting `e2e.policy`.
⚠ A change is a person's (sessionOnly): an admin-scoped API key may read the policy, not change it. The policy decides who may make data readable only by its key holders, and the ceiling is the operator's.
⚠ requireSupertenant and sessionOnly are called in the handler, never as a route middleware — the reasoning of supertenant.go.
The audit rows are written here through e2epolicy.Audit, not by the audit middleware (auth.ActionForPath skips /api/admin/e2e): one change is one row that names the tenant and says what the value was and what it became.
Package handlers — e2e_policy_files.go
Who may encrypt (internal/e2epolicy), the explorer's half:
POST /api/files/e2e/allowed - may I encrypt here? {"items":[{"path":"Depo://a","kind"?:"folder"|"new_folder"|"file"}]} → {"encrypt":["allowed"|"request"|"denied",…]}
POST /api/files/e2e/requests - leave a request: {"path":"Depo://a/b","kind":"folder"|"new_folder"|"file","reason":"…"}
GET /api/files/e2e/requests — the caller's own requests
A request is what the approval policy asks of somebody who holds files.encrypt: an administrator of their tenant approves it on Admin → Encryption (handlers/e2e_policy_admin.go), and the approval opens one encryption of its kind there, once, for seven days.
The service writes the audit rows; /api/files/e2e/requests is not one of the file paths the audit middleware records (auth.shouldAudit), so one event is one row.
Package handlers — external_admin.go
Admin CRUD for external services (OnlyOffice, Drawio, Mermaid, …).
GET /api/admin/external
PATCH /api/admin/external/{name}
POST /api/admin/external/{name}/test
Package handlers - file_types_admin.go
Admin → Plugins → Default apps (internal/assoc, docs/APP-PLUGINS.md → Default apps):
GET /api/admin/file-types - every kind something besides filex handles, and every kind with a rule
PUT /api/admin/file-types/{ext} - {open?: {order, off} | null, thumbnail?: {order, off} | null}; null = back to the default
DELETE /api/admin/file-types/{ext} - both capabilities back to the default
⚠ Instance-wide, like the apps whose handlers it orders: a tenant administrator gets 403 supertenant_only. A change needs an administrator signed in to the panel (an API key reads, and gets 403 session_required): switching a thumbnail handler back on hands an app the bytes of every file of that kind, which is the same kind of decision as an action override (PutOverrides). Refused on the demo instance. Each change writes its audit row (file_association.update / .reset) with what it was and what it is.
Package handlers — login_security.go
The administrator's side of the sign-in attempt limit (internal/loginguard): its settings, the locks in force, and the recent wrong attempts. Frozen contract for the admin SPA and for /api/ai/admin (and its MCP tools):
GET /api/admin/login-security
→ {settings, limits, trusted_proxies_effective, trusted_proxies_source,
trusted_defaults:{auto, loopback, private, link_local},
trusted_addresses, trusted_proxies_auto:{in_use, environment,
runtime, networks, excluded_gateways, excluded_self, interfaces,
warning, warning_detail, resolved_at}, untrusted_forwarders:
[{address, first_seen, last_seen, count, public, relay}],
untrusted_forwarders_total, your_ip, your_ip_allowlisted}
PATCH /api/admin/login-security partial body, same keys as `settings`
→ the GET answer (400 {error:"invalid_setting", field, message} on a
bad value; nothing is written unless every field is valid)
GET /api/admin/login-security/locks?scope=&locked=1&limit=
→ {items:[{id, scope, subject, fails, limit, lock_level, locked,
locked_until, retry_after, window_start, last_fail_at, last_ip,
last_protocol}], now}
POST /api/admin/login-security/unlock {scope, subject} | {all:true}
→ {ok, unlocked}
GET /api/admin/login-security/attempts?action=&from=&to=&limit=&offset=
→ {items:[{id, action, identifier, ip, protocol, reason, scope, via,
changed_fields, at, metadata}], total}
On a public demo (DemoMode) every address a visitor or the operator wrote is "hidden on the demo" in all of these answers (demo_redact.go); your_ip, the reader's own, is not.
Instance-wide ¶
Every value here is one global row, and a lock or an address is not a tenant's own: the whole surface is the platform operator's (requireSupertenant, asked INSIDE each method because /api/ai/admin and the MCP tools mount these same handler instances).
Memory first ¶
With a running limiter (Guard) every answer here is the limiter's memory — the settings and the counters it decides with — and every change goes through it: a setting is written to the table and put in force at once, an unlock lifts the lock in memory and deletes the row. Without one (a store with no limiter) the table is read and written directly.
What is never here ¶
No password, ever: the counters hold an identifier and an address, the audit rows hold the same plus the door and the reason.
Package handlers contains one file per logical HTTP route group.
Package handlers — meta.go ¶
Starred / Recently-opened endpoints, and the row shape the tag listings share with them. All require an authenticated user; starred and recent are PER-USER (stored on user_node_meta). Tags — personal and team since v0.43.0 — have their own file, tags.go.
POST /api/files/manager/star body {node_id, starred: bool}
GET /api/files/manager/star/list?storage_id=…&limit=
POST /api/files/manager/recent body {node_id}
GET /api/files/manager/recent?limit=
Package handlers — nfsexports.go
GET /api/auth/nfs-exports — list the caller's own exports
POST /api/auth/nfs-exports — mint one (the PATH is shown ONCE)
POST /api/auth/nfs-exports/{id}/state — enable / disable
DELETE /api/auth/nfs-exports/{id} — revoke permanently
⚠⚠ The path returned by POST is the whole credential. Anyone who can reach the NFS port and knows it can mount the export as this account — NFSv3 has no other way to bind an identity that does not require Kerberos. It is shown once, stored hashed, and the UI says all of this plainly rather than letting somebody discover it from a mount table.
Package handlers - openwith.go ¶
Endpoints (0.50, docs/APP-PLUGINS.md → Default apps):
GET /api/me/open-with (auth) → {"choices": {ext: handler}}
PUT /api/me/open-with/{ext} (auth) ← {"handler": "builtin" | "onlyoffice" | "app:<app>/<view>"} → {"choices": …}
DELETE /api/me/open-with/{ext} (auth) → {"choices": …}
DELETE /api/me/open-with (auth) → {"choices": {}}
"Always open this kind with this app" - ONE record per ACCOUNT (the maintainer, 2026-10-01): the browser, the desktop app and every embed read and write the same choices. A person who picked an app for .board files in the browser gets it in the desktop app too; where that app cannot open the file (it is not offered there, or the administrator switched it off for the kind) the next handler that is on opens it, and the choice is kept for where it can.
⚠⚠ NOT a key of the per-surface document. That document is PUT whole, so a shared key in it would be overwritten by whichever surface wrote last with whatever it read at boot - a choice made on the desktop undone by a palette change in a browser tab opened before it. So the choices are their own row (user_prefs, surface "account", `{"openWith": {…}}`), changed one kind at a time, and the surface document only CARRIES them on the way out: GET /api/me/prefs puts them in as `openWith` (the JSON string the client has always read), PUT /api/me/prefs drops the key (userprefs.go).
⚠ Adoption, once: an account whose row has never held a choice takes the `openWith` of its most recently written surface document (web or desktop), the one that was in use last. Choices are not merged across documents: which device's choice for a kind is "right" is not knowable, and the last one written is the one the person saw last.
Package handlers — plugin_requests.go
Plugin install requests (internal/pluginreq, docs/APP-PLUGINS.md → Install requests, docs/PLUGINS.md → Install requests):
POST /api/admin/plugin-requests — leave a request (API key or session)
GET /api/admin/plugin-requests[?status=…] — list (pending by default; `all` for every state)
GET /api/admin/plugin-requests/{id} — one request, with its frozen manifest and review
POST /api/admin/plugin-requests/{id}/approve - SESSION ONLY: install what the request froze; {"associations"?: [...]}
POST /api/admin/plugin-requests/{id}/reject — SESSION ONLY: {"reason"?: "…"}
⚠⚠ Why this exists (owner, 2026-09-28): an API key may no longer install, upgrade, remove or re-permission a plugin (requireSession). It may leave a request; an administrator signed in to the panel decides. Approve and reject are therefore session-only here AND there is no MCP tool for either (ai_admin.go): a key that could approve its own request would be the old hole with an extra step.
⚠ Instance-wide like the plugins themselves: in multi-tenant mode only the supertenant may leave, read or decide a request (requireSupertenant). A tenant administrator gets 403.
The service writes the audit rows itself (created, approved, rejected, expired, superseded), so these routes are skipped by the audit middleware (auth.ActionForPath) and one event is one row, whichever door it came in by.
Package handlers — plugins.go ¶
Admin surface for storage plugins (internal/plugin, docs/PLUGINS.md):
GET /api/admin/plugins — every plugin with its live state
POST /api/admin/plugins — install: multipart {name, file[, source]} | JSON {name,url,sha256[,source]} | JSON {name,source} | JSON {name,kind:"remote",address,token}
GET /api/admin/plugins/{id}
PATCH /api/admin/plugins/{id} — {"enabled"?: bool, "source"?: string}
POST /api/admin/plugins/{id}/restart
POST /api/admin/plugins/{id}/upgrade — multipart {file} | JSON {"from_source": true}
POST /api/admin/plugins/updates/check — read every source now; installs nothing
⚠⚠ Nothing updates itself (plugin/updates.go): a binary plugin that names a source is checked daily and its row says when a newer version is there; the administrator's {"from_source": true} upgrade installs it.
DELETE /api/admin/plugins/{id}
⚠⚠ Install, upgrade, PATCH and DELETE need an administrator SIGNED IN to the panel (requireSession): an API key gets 403 and is pointed at /api/admin/plugin-requests, where it may leave a request instead. Reading (the list, one plugin, the update check) stays open to an admin-scoped key.
⚠ Instance-wide, never tenant-scoped: a plugin is a PROCESS filex runs (or a service it trusts with storage credentials), so in multi-tenant mode only the supertenant may touch this surface. A tenant admin gets 403, not an empty list, so the boundary is visible.
Package handlers — protection.go ("Koru" v0.4)
Admin protection-settings surface, frozen contract:
GET /api/admin/protection → {"trash_retention_days":30,"versions_keep_n":0,
"share_max_ttl_days":7,"shares_over_max_ttl":0,
"drafts_limit":50,"drafts_limit_min":1,
"drafts_limit_max":1000,
"antivirus":{ … see protectionAntivirusStatus }}
PATCH /api/admin/protection → {trash_retention_days?, versions_keep_n?,
share_max_ttl_days?, drafts_limit?,
av_save_scan_window_minutes?,
av_max_scan_mb?, av_enabled?, av_mode?,
av_clamd_addr?}
The values live in the settings table (`trash.retention_days`, `versions.keep_n`, `share.max_ttl_days`, `drafts.limit`, `antivirus.enabled`, `antivirus.mode`, `antivirus.clamd_addr`, `antivirus.save_scan_window_minutes`, `antivirus.max_scan_mb`). `shares_over_max_ttl` is a read-only count of EXISTING live links that outlive the ceiling — they are reported, never shortened (see share.CountOverMaxTTL).
The antivirus block: STATUS fields and SETTING fields ¶
They are separated on purpose, because they can disagree and the disagreement is the interesting part. A SETTING is what the settings table holds — what an admin last saved. A STATUS field is what this process is actually doing. `scan_enabled` can be true while `enabled` is false (switched on, nothing to reach), and both can be true while `reachable` is false (clamd configured, clamd down).
⚠⚠ `av_enabled`, `av_mode` and `av_clamd_addr` take effect AT THE NEXT RESTART, in both directions, because the scan pipeline is wired once at boot (internal/server registers the queue handler and hands the upload surfaces an enqueue function). The response says so twice: the settings are echoed back immediately, and `restart_pending` is true for as long as the stored configuration differs from what this process booted with. The two live settings — the save-scan window and the size ceiling — are read at the point of use and are never pending.
⚠ The scanner BINARY is still not admin-writable, and for the original reason: it is a path this server EXECUTES, so a writable field for it would turn an admin account into arbitrary command execution. A clamd ADDRESS is a different thing — a dial target, never executed — and is writable, with its shape validated at save time (antivirus.AddrSetting.Check). It does let an admin point scanning at an arbitrary TCP endpoint, which is worth stating plainly, but it is the same authority an admin already has when configuring a storage backend's endpoint.
Package handlers — public_api.go
ONE PUBLIC SURFACE. Everything a stranger reaches through a filex link — a download share, a file request, an app plugin's page — is answered here as JSON, so the SPA can render all three with one shell, one PIN gate, one expiry story and the instance's own branding.
GET /api/public/branding — who this instance says it is
GET /api/public/ui-locales/{code} — one language an app adds: its strings
GET /api/public/s/{token} — a share's state
POST /api/public/s/{token}/pin — answer the PIN → unlock cookie
POST /api/public/s/{token}/event — an app page's surface event
GET /api/public/s/{token}/file/{ref} — a copy an app page exposed
GET /api/public/d/{token} — a file request's state
POST /api/public/d/{token}/pin — answer the PIN → unlock cookie
POST /api/public/d/{token}/upload — the drop itself (multipart)
⚠ NOTHING HERE IS AUTHENTICATED, and nothing here leaks. The token is the only capability; a wrong or unknown one is 404 with no other detail, and a dead one is 410 with the same shape for every reason it could be dead. The creator's address, the storage's name and the file's path never appear.
⚠ Every answer is `Cache-Control: no-store` and `X-Robots-Tag: noindex` EXCEPT /branding and /ui-locales, the two things here that are not about a link: who the instance is and the words it speaks. Both are deliberately cacheable (every public page fetches them).
⚠ The PIN gate is internal/share's — five strikes then ten minutes, counted on the share row, answered with an HMAC cookie that carries no PIN. It is the same gate the no-JS pages use, and the same one the app pages used to have to themselves.
Package handlers — public_shell.go
WHICH ANSWER A PUBLIC LINK GIVES.
/s/{token} and /d/{token} have three possible readers and must keep serving all three:
- a JavaScript browser — gets the SPA shell (the branded public surface: PIN gate, expiry, visit counter, the app page when the link carries one);
- a browser with no JavaScript, or one the shell could not load in — gets the Go pages that have always been here: the file, a PIN form, an explanation. Reduced to that, and kept, because a share link is opened by strangers on whatever browser they have;
- something that is not a browser at all — `curl -O`, wget, a download manager, a backup script — gets the BYTES, exactly as before.
The rules below are the whole decision. They are written as a function rather than as a middleware so both handlers make the same call in one place, and so a test can ask the question without a router.
Package handlers — quota.go ¶
Endpoints:
GET /api/files/quota/me (auth)
GET /api/files/quota/storages (auth, quota_storages.go)
GET /api/admin/users/{id}/quota (admin)
POST /api/admin/users/{id}/quota (admin)
PATCH /api/admin/users/{id}/quota (admin)
POST /api/admin/users/{id}/quota/recompute (admin)
The flat forms — /api/admin/quota/{user_id} and /api/admin/quota/{user_id}/recompute — are the original spelling and are still served. Note both take a USER id: there is no per-provider quota (see docs/MULTI-TENANCY.md, "Not yet").
Package handlers — quota_storages.go
Endpoint:
GET /api/files/quota/storages (auth)
"How full is this drive", for somebody who is not an administrator.
The admin panel has always had this number: `/api/admin/storages` enriches every row with `stats.total_size_bytes` (handlers/storages.go), which is what the Home page's storage card printed for an operator and for nobody else. The only other figure in reach was `/api/files/quota/me`, and that is a per-USER sum across every storage — printing it under one drive's name would be a number about the person wearing a label about the drive.
So this answers the same quantity the admin row carries, for the storages the caller is allowed to see, and nothing about the ones they are not.
Package handlers — replication_targets.go
/api/admin/replication-targets — CRUD for the new backup-only entity. Replication targets are NOT regular storages: operators never write to them, they don't show up in the Depolar list, and the file explorer never lists them as a virtual root. Each entry is a backup sink that a primary storage can fan its writes out to via `storages.replica_target_id`.
Package handlers — s3keys.go ¶
Self-service management of S3 access keys (migration 00026).
GET /api/auth/s3-keys — list the caller's own keys
POST /api/auth/s3-keys — mint one (the secret is shown ONCE)
POST /api/auth/s3-keys/{id}/state — enable / disable without deleting
DELETE /api/auth/s3-keys/{id} — revoke permanently
Self-service and not admin-only on purpose: the requirement is that every credential the product issues can connect at exactly its own permission, and a key that only an administrator can mint is a key most people never get. The permission ceiling is enforced by protocolauth.Issue, not by who is asking — a key can only ever narrow what its owner already has.
Package handlers — save_text.go
`/api/files/save-text` accepts plain-text writes from the SFC's code editor (Monaco) + markdown viewer "edit" mode. The SFC posts:
POST /api/files/save-text
{ "path": "<adapter>://<rel>", "content": "..." }
We resolve the storage from the adapter prefix, write the content through the storage Driver, then refresh the cache row's metadata (size + mtime + etag where the driver supplies one).
Whitelist: only files matching a text/code MIME class are saveable here. Binary edits go through OnlyOffice's callback channel.
Antivirus ¶
This surface does NOT call writehook.OnFileWritten, and that omission was the gap: the hook enqueues an antivirus scan, so routing every Ctrl+S through it would have queued a ClamAV pass per keystroke-burst — and the answer taken instead was to scan nothing, which made the editor a way to introduce a file that is never scanned on an install where every uploaded file is. The two branches below now split it the way the behaviour actually splits:
create (no node row for the path) → scan immediately, like an upload save (node row already existed) → schedule one scan, debounced
The debounce lives in the queue (not_before + a coalescing key), not in a timer in this process, so a restart mid-window does not lose the pending scan. See queue.AntivirusScanner.EnqueueAfterSave.
The write event ¶
The scan is the only thing this surface schedules for itself. The EVENT goes through the shared gate like every other write surface -- writehook.EmitWritten, which is OnFileWritten minus the antivirus half it has already taken care of -- so the editor is not a place where webhook subscribers hear something different from the browser upload that wrote the same file. The two branches split the event exactly as they split the scan:
create (no node row for the path) -> file.uploaded save (node row already existed) -> file.updated
Same `existing` fact, one decision, two consequences. Until this was wired the editor announced NOTHING at all: a file created or rewritten in the browser reached no webhook and no notification bell, so an integration watching a folder simply did not see edits happen.
Package handlers — search_admin.go
Admin endpoints for the embedded Bleve full-text index.
GET /api/admin/search/stats — index stats POST /api/admin/search/rebuild — drop and rebuild the index from nodes
Package handlers — shared.go ¶
"Shared with me": everything the caller reaches through a per-item grant somebody else gave them, rather than through their own role on a storage.
GET /api/files/manager/shared-with-me?limit=&offset=
The data has always existed in `file_grants`, but nothing could answer this question: Grants.List is path-scoped ("who can see THIS folder") and owner-only, so a user had no way to find out what had been shared with them unless they were told the path. That is the one item on a Drive-style navigation panel with no backing endpoint (issue #14).
Package handlers — shares_admin.go
Admin views/actions over all shares (every user, not just current user).
GET /api/admin/shares
POST /api/admin/shares/{id}/revoke
DELETE /api/admin/shares/{id}
Package handlers — shares_mine.go
"Paylaştıklarım" / "My shares" — the USER-scoped half of the share surface, and the one audited door to a link's PIN.
GET /api/shares the links the caller created (paginated)
GET /api/shares/{id}/pin that link's PIN, to its creator or to an admin
Why this file exists ¶
Until now the only list of shares was /api/admin/shares behind the admin panel, so somebody without admin rights could not see their own links at all: they minted a link from the explorer, the dialog showed it once, and after that the only way back to it was to mint another one. Every other self-service credential surface in this product (API tokens, S3 keys, SSH keys, NFS exports) already has this half. Shares did not.
⚠ This is NOT a second admin listing. It reads the SAME store query (ListAllShares) with the caller pinned as the creator, the same tenant filter and the same row envelope. What it drops is the part that is about everybody rather than about the caller — the creator's address, which on this screen is always the person reading it.
Package handlers — sshkeys.go
GET /api/auth/ssh-keys — list the caller's own keys
POST /api/auth/ssh-keys — register one (authorized_keys line)
POST /api/auth/ssh-keys/{id}/state — enable / disable without deleting
DELETE /api/auth/ssh-keys/{id} — remove permanently
⚠ This screen is a PREREQUISITE for the SFTP endpoint, not a follow-up: `ssh-copy-id` cannot work against filex. It appends to `~/.ssh/authorized_keys` over a shell, and filex has no shell — so without a place to paste a key, public-key authentication is unreachable and everybody falls back to sending their account password to a file server.
Package handlers — storage_drivers.go
GET /api/admin/storage-drivers — the config contract of every registered storage driver (see storage/descriptor.go).
Why its own admin route instead of a field on /api/capabilities: capabilities is public (embedders and the share/drop pages fetch it without a session) and already hot; driver config schemas — including which fields hold credentials, and the operator-facing hints for them — are only useful to an admin building a storage. `storage_drivers: []string` stays on capabilities untouched for every existing caller.
Package handlers — storages_admin.go
Extra admin actions on storages beyond plain CRUD:
POST /api/admin/storages/test — try a connection without saving
POST /api/admin/storages/discover — list the folders under a root, to mount several at once
GET /api/admin/storages/{id}/sync-runs — recent runs for one storage
GET /api/admin/storages/{id}/drift — recent sync conflicts
Package handlers — supertenant.go ¶
ONE gate for the admin surfaces that are INSTANCE-WIDE.
The distinction the /api/admin block does not draw for you ¶
Every route under /api/admin has already passed `auth.RequireAdmin`, and `auth.TenantResolver` has attached the requester's tenant scope. Neither of those denies anything by itself: the resolver only labels the request, and the scoped store filters exactly three list queries (storages, enabled storages, users). So in multi-tenant mode "admin" means *an* admin — of any tenant — and a route whose effect is instance-wide is, by default, open to all of them.
Most admin routes are fine with that because their DATA belongs to a tenant. A handful do not: they read and write a single global row, or drive a single process, that every tenant then lives with. Those are the ones that must ask this question, and the consequence of forgetting is not a leak but a takeover — the tenant admin of the smallest customer switching antivirus off for the whole platform, or repointing the shared document server at a host they control.
Single-tenant installs are untouched, by construction ¶
`auth.TenantResolver` returns the next handler unchanged when multi-tenant mode is off (auth/tenant_middleware.go), so NO scope is ever attached and `tenant.FromContext` reports absence. Absence means "unscoped" and passes here. The ordinary admin of a non-multi-tenant instance therefore still administers everything, with no flag to thread and nothing to configure — the gate is invisible until somebody turns tenancy on.
Why the gate lives in the handler and not on the route ¶
Because the route is not the only door. `/api/ai/admin` (handlers/ai_admin.go `Register`) mounts the SAME handler instances behind an `admin`-scoped API token, and the MCP admin tools drive those same methods in-process through `AIAdmin.invoke`. A chi middleware in routes.go would gate the browser's door and leave the other two standing open. A check at the top of the method is on the inside of all three.
Package handlers — surface_conditions.go
The HOST half of `show_when` / `required_when`.
The contract (APP-PLUGINS-API.md → Fields, PLUGIN-KIT.md → Screens) makes a plugin author two promises about a field that depends on another field:
"Both are checked on the client AND re-checked by the host at submit: a value belonging to a hidden field is dropped before the job runs, so it cannot arrive as a surprise, and a `required_when` field left empty refuses the job."
The browser half is `packages/core/src/lib/surfaceConditions.ts`, called from `usePluginSurface` on every event it posts. This file is the other half, and it is a LINE-BY-LINE port of that file rather than a second opinion about the same rules: the two halves have to agree exactly, or a plugin gets one answer on screen and another in its job.
⚠ Why the host has to ask the plugin what the screen looks like. A surface is not in the manifest — the plugin DRAWS it per event — so the host has no standing list of fields to check against. At a submit it therefore asks the plugin to draw the screen these values belong to (one `change` call, the same question the browser asks on every keystroke), reads the form fields out of that answer, and applies the rules to the values before the real event is handed over. The conditions are then evaluated by the same cascade the browser uses, so neither side can be talked into a different answer by a crafted request.
⚠ It strips the VALUES, not the job's params. The params are the plugin's own map — it may compute, rename or default anything in there — and dropping keys out of it would delete the plugin's own work. The values are the person's answers, which is exactly what the promise is about; a plugin that never sees a hidden field's answer cannot put it in a job.
Package handlers — sync_admin.go
Admin views over sync runs (cross-storage list + per-run detail).
GET /api/admin/sync-runs
GET /api/admin/sync-runs/{id}
Package handlers — tags.go ¶
Personal and team tags (v0.43.0, migration 00055).
GET /api/files/manager/tags?node_id=… the tags on one file the caller can see GET /api/files/manager/tags?storage_id=… the tags in use in one storage GET /api/files/manager/tags/all every tag the caller can see POST /api/files/manager/tags set a file's tags GET /api/files/manager/tagged?tag=…[&kind=personal|team]
The finding this answers ¶
⚠⚠ Tags used to be ONE shared label per file (node_meta `tag:<name>`), with no owner and no tenant, although routes.go introduced them as "Per-user metadata". Measured on v0.42 (tester, 2026-09-22, and reproduced here before the fix): user A tagged a file "Müşteri Teklifi"; user B's and the admin's `tags/all` answered `["müşteri teklifi"]`; B's `POST {tags:[]}` answered 200 and the tag was gone for A; and with multi-tenancy on, tenant bravo's `tags/all` listed tenant alpha's tag. The name had also been lower-cased.
# The rules (the owner's decision: "personal ve team olarak ikiye ayrılır, tenant based tagging yapılır")
- PERSONAL — one person's own label, like a star. Only its owner sees it, adds it or removes it. Anyone who can see a file may personal-tag it.
- TEAM — shared with everyone in the TENANT who can see the file. Adding or removing one needs EDIT permission on the file (the ACL level, `editor` or `owner`); a viewer sees team tags and cannot change them.
- A team tag never crosses a tenant boundary. A confined caller (a tenant in multi-tenant mode) sees its own tenant's team tags plus tenant-0 ones ("the instance": made before a tenant existed on that storage); never another tenant's. An unconfined caller — single-tenant mode, where the tenant IS the instance, or the platform supertenant, which already reaches every storage — sees every team tag on the files it can see.
- A tag is only ever named to someone who can see at least one file that carries it: a tag NAME is information too ("Layoffs Q4").
What an old client gets ¶
A `POST {node_id, tags:[names]}` with no `kind` — every client before v0.43.0 — creates PERSONAL tags. Deliberately the narrow default:
- A request that does not say who should see a label must not show it to anyone else. The finding WAS an unintended audience; defaulting the old shape to team would reproduce it for every stale tab and script.
- It always works. Any caller who can see a file may personal-tag it; a team default would turn a viewer's old client into a stream of 403s.
- It is what the code always claimed ("per-user"), and what the tester expected.
Names in that list that match a tag the file ALREADY carries keep that tag as it is (team stays team) — an old client round-trips the list it read, and an untouched team tag in it is not a change. A team tag the old client leaves OUT is a removal, and needs edit permission like any other.
Case ¶
Names keep the case they were typed in; two names are one tag when internal/tagname.Key says so (case-folded, Turkish I/ı/İ/i merged, NFC). See that package for why strings.EqualFold is not the rule.
Package handlers: tenant_self.go
A tenant runs itself (docs/TENANT-ADMIN.md):
GET /api/admin/tenant the tenant: addresses, sign-in, domains
POST /api/admin/tenant/auth-providers its own OIDC or LDAP
PATCH /api/admin/tenant/auth-providers/{name}
DELETE /api/admin/tenant/auth-providers/{name}
POST /api/admin/tenant/auth-providers/{name}/test
POST /api/admin/tenant/domains {domain}: an own domain, pending
POST /api/admin/tenant/domains/{id}/check look at its CNAME now
PUT /api/admin/tenant/domains/{id}/certificate {cert_pem, key_pem}: bring a certificate
DELETE /api/admin/tenant/domains/{id}/certificate
DELETE /api/admin/tenant/domains/{id}
PUT /api/admin/tenant/insecure {allow}: the operator's switch
WHOSE tenant: a tenant's administrator acts on their own and nothing else; the platform operator (an unconfined administrator) names one with `?tenant=<id>`. Another tenant's id from a tenant's administrator is 404, the same answer as a tenant that does not exist (handlers/tenantown.go).
The rules the owner decided (2026-09-30, 2026-10-01):
- a tenant's administrator adds their OWN OIDC or LDAP only - the operating-system providers and the header proxy are the operator's, and binding a shared provider is the operator's too;
- a tenant's own provider is guarded (internal/netguard, LDAP only over ldaps:// or StartTLS, the CA pasted, never a file on the server) unless the operator ticked "allow insecure and internal-network providers" for the tenant - which only the operator may change, audited;
- the last way a tenant's administrator can sign in is never switched off by that administrator;
- an own domain is proven by a CNAME to the tenant's platform subdomain, belongs to one tenant at a time, and may carry the tenant's own certificate.
Package handlers — tenantown.go ¶
ONE ownership predicate for the admin routes that take a raw {id}.
A different question from supertenant.go ¶
`supertenant.go` asks "may this tenant touch an instance-wide switch" and answers 403 when it may not — the SURFACE is being refused and the operator needs to read why. This file asks a narrower and more dangerous question: "does the caller's tenant own the ROW it just named". The routes here are legitimate tenant features — a tenant admin absolutely may reset their own user's password, revoke their own share, purge their own trash. They may not do it to somebody else's, and until this file existed nothing asked.
That distinction is why the failure is worse than an over-broad control. Measured on the pre-fix build (`TestOwnership_ForeignIdsAreRefused`), an admin of one tenant reached FIFTEEN of sixteen id-taking admin routes belonging to another tenant — including `POST /users/{id}/reset-password`, which answered 200 and returned the other customer's new cleartext password in the response body, and `GET /storages/{id}`, which returns the storage's whole config blob (for an S3 or SFTP storage: the access key and secret).
Why 404 and not 403 ¶
A 403 confirms the row exists. Repeat it over an id range and the endpoint becomes a census of the platform's other customers — how many storages, how many users, which ids are live. A foreign id must be indistinguishable from one that never existed, so these refusals borrow the shape of the "not found" the handler would otherwise have produced.
Single-tenant installs are untouched, by construction ¶
Same mechanism as supertenant.go: `auth.TenantResolver` attaches no scope when multi-tenant mode is off, absence means "unscoped", and unscoped passes every predicate here. There is no flag to set. The supertenant passes too — the platform operator must keep being able to administer a tenant's rows, including recovering a locked-out tenant admin.
Why the check lives in the handler ¶
Because the chi route is not the only door — `/api/ai/admin` mounts the same handler instances behind an admin-scoped API token and the MCP admin tools drive them in-process. Same reasoning as supertenant.go; see its header.
Package handlers — trash.go ¶
Endpoints:
GET /api/files/manager/trash (auth) list trashed
POST /api/files/manager/restore (auth) body {node_id}
DELETE /api/admin/trash/{id} (admin) immediate single purge
POST /api/admin/trash/empty?older_than_days=N (admin) start a batch purge
GET /api/admin/trash/empty (admin) that purge's progress
Package handlers — userprefs.go ¶
Endpoints:
GET /api/me/prefs?surface=web (auth) → {"surface": "...", "prefs": <object>}
PUT /api/me/prefs?surface=web (auth) ← {"prefs": <object>}
What a person chose about the interface itself — theme, palette, density, language. One JSON document per person PER SURFACE (`web` | `desktop`), stored by migration 00047. The account's "open with" choices (0.50) are NOT part of it: GET carries them as `openWith`, PUT drops the key, and /api/me/open-with changes them (openwith.go).
⚠⚠ WHY THE SERVER. The owner picked a theme in one browser and the other browser did not have it. `localStorage` is per BROWSER, never per person: a second machine, a private window, a cleared site and the desktop app are each a fresh empty set of choices, with nothing to notice and no error.
⚠ NOT a replacement for the view prefs (00039, viewprefs.go). Those are how each FOLDER was left and are written on every navigation; these are the surface's own appearance and are read once at sign-in. Two tables because they change at completely different rates and one of them is per folder.
⚠ A caller only ever reaches THEIR OWN row. There is no user id in the path or the body, and the only identity this file consults is the session's, so there is nothing for a client to substitute.
Package handlers — users_admin.go
Admin-only extra actions on users.
POST /api/admin/users/{id}/reset-password
Package handlers — versions.go ¶
Endpoints under /api/files/versions.
GET /api/files/versions?node_id=… (viewer) list snapshots
POST /api/files/versions/snapshot (editor) snapshot now
POST /api/files/versions/restore (editor) restore one
DELETE /api/admin/versions/{id} (admin) hard delete
⚠⚠ Every one of them takes a raw node id, so every one of them resolves the node and authorizes it FIRST — see authorizedNode. Until that existed the only gate here was tenancy, which meant that on a single-tenant install (the vast majority) there was no gate at all: a `viewer` account could POST /restore and overwrite the live bytes of any file whose node id it could name, and POST /snapshot to write objects into any storage on the box.
Package handlers — viewprefs.go ¶
Endpoints:
GET /api/files/manager/view-prefs (auth) → {"prefs": <object>, "instance_default": <object|null>}
PUT /api/files/manager/view-prefs (auth) ← {"prefs": <object>}
How each person left each folder — view mode, sort key and direction, plus their column widths, order and visibility. One JSON document per user, stored against the user row by migration 00039.
⚠⚠ WHY THIS IS SERVER-SIDE at all, when theme, density and the start page are not. `localStorage` is per BROWSER, not per person: on a shared machine the next account to sign in inherits the previous one's folder arrangements, silently and with no error. The owner ruled that out explicitly ("her user kendi görünümünü görür"), and a server-side document also follows a person to the desktop app and to their other machines, which is what that sentence really asks for.
Index ¶
- Constants
- Variables
- func AppNewDocTypes(reg *wasmplugin.Registry) []newdoc.Type
- func ArchiveName(paths []string, override string) string
- func DemoRefusal() map[string]string
- func ImportTheme(raw []byte) (*model.CustomTheme, error)
- func NewArchiveTicketStore() *archiveTicketStore
- func NewUploadTicketStore() *uploadTicketStore
- func RequireAdminPermission(resolver *acl.Resolver, p perm.Perm) func(http.Handler) http.Handler
- func RequirePermission(resolver *acl.Resolver, p perm.Perm) func(http.Handler) http.Handler
- func RequirePersonalCaller(next http.Handler) http.Handler
- func ResolveThemeID(requested string, custom []*model.CustomTheme) string
- func RetiredPageAPI() http.Handler
- func RetiredPagePrefix() http.Handler
- func SetAntivirusEnqueue(fn func(ctx context.Context, n *model.Node))
- func SetAntivirusEnqueueAfterSave(fn func(ctx context.Context, n *model.Node))
- func SetAppLabels(f func(string) wire.Text)
- func SetChangeEmitter(e ChangeEmitter)
- func SetLockReasons(f func(*model.AppPluginLock) (string, wire.Text))
- func SetNotifySink(s notify.Service)
- func ThemeAllTokens() []string
- func ThemeAuthoredColorTokens() []string
- func ValidateCustomTheme(t *model.CustomTheme) error
- type AI
- func (h *AI) AppActions(w http.ResponseWriter, r *http.Request)
- func (h *AI) AppRun(w http.ResponseWriter, r *http.Request)
- func (h *AI) ArchiveCreate(w http.ResponseWriter, r *http.Request)
- func (h *AI) ArchiveExtract(w http.ResponseWriter, r *http.Request)
- func (h *AI) AttachACL(r *acl.Resolver)
- func (h *AI) AttachBody(b *filebody.Resolver)
- func (h *AI) AttachDoors(d *AIDoors)
- func (h *AI) AttachE2EPolicy(s *e2epolicy.Service)
- func (h *AI) AttachSearchIndex(idx *search.Index)
- func (h *AI) AttachStaged(s *StagedUpload)
- func (h *AI) AttachTenants(rv tenanturl.Resolver)
- func (h *AI) AttachThumbs(p *thumb.Pipeline)
- func (h *AI) AttachTickets(s *uploadTicketStore)
- func (h *AI) CommentAdd(w http.ResponseWriter, r *http.Request)
- func (h *AI) CommentDelete(w http.ResponseWriter, r *http.Request)
- func (h *AI) CommentsList(w http.ResponseWriter, r *http.Request)
- func (h *AI) Convert(w http.ResponseWriter, r *http.Request)
- func (h *AI) Copy(w http.ResponseWriter, r *http.Request)
- func (h *AI) Delete(w http.ResponseWriter, r *http.Request)
- func (h *AI) Download(w http.ResponseWriter, r *http.Request)
- func (h *AI) FileRequest(w http.ResponseWriter, r *http.Request)
- func (h *AI) Info(w http.ResponseWriter, r *http.Request)
- func (h *AI) List(w http.ResponseWriter, r *http.Request)
- func (h *AI) Mkdir(w http.ResponseWriter, r *http.Request)
- func (h *AI) Move(w http.ResponseWriter, r *http.Request)
- func (h *AI) NotificationRead(w http.ResponseWriter, r *http.Request)
- func (h *AI) NotificationsList(w http.ResponseWriter, r *http.Request)
- func (h *AI) OpCancel(w http.ResponseWriter, r *http.Request)
- func (h *AI) OpGet(w http.ResponseWriter, r *http.Request)
- func (h *AI) OpsList(w http.ResponseWriter, r *http.Request)
- func (h *AI) PermissionRevoke(w http.ResponseWriter, r *http.Request)
- func (h *AI) PermissionSet(w http.ResponseWriter, r *http.Request)
- func (h *AI) PermissionUsers(w http.ResponseWriter, r *http.Request)
- func (h *AI) PermissionsList(w http.ResponseWriter, r *http.Request)
- func (h *AI) Root(w http.ResponseWriter, r *http.Request)
- func (h *AI) Search(w http.ResponseWriter, r *http.Request)
- func (h *AI) Share(w http.ResponseWriter, r *http.Request)
- func (h *AI) SharesList(w http.ResponseWriter, r *http.Request)
- func (h *AI) Star(w http.ResponseWriter, r *http.Request)
- func (h *AI) TagsGet(w http.ResponseWriter, r *http.Request)
- func (h *AI) TagsSet(w http.ResponseWriter, r *http.Request)
- func (h *AI) TrashList(w http.ResponseWriter, r *http.Request)
- func (h *AI) TrashRestore(w http.ResponseWriter, r *http.Request)
- func (h *AI) Unshare(w http.ResponseWriter, r *http.Request)
- func (h *AI) Unzip(w http.ResponseWriter, r *http.Request)
- func (h *AI) Upload(w http.ResponseWriter, r *http.Request)
- func (h *AI) UploadTicket(w http.ResponseWriter, r *http.Request)
- func (h *AI) VersionRestore(w http.ResponseWriter, r *http.Request)
- func (h *AI) VersionSnapshot(w http.ResponseWriter, r *http.Request)
- func (h *AI) VersionsList(w http.ResponseWriter, r *http.Request)
- func (h *AI) Zip(w http.ResponseWriter, r *http.Request)
- type AIAdmin
- type AIAdminDeps
- type AIDoors
- type AIMCP
- func (h *AIMCP) AttachACL(r *acl.Resolver)
- func (h *AIMCP) AttachBody(b *filebody.Resolver)
- func (h *AIMCP) AttachDoors(d *AIDoors)
- func (h *AIMCP) AttachE2EPolicy(s *e2epolicy.Service)
- func (h *AIMCP) AttachSearchIndex(idx *search.Index)
- func (h *AIMCP) AttachStaged(s *StagedUpload)
- func (h *AIMCP) AttachTenants(rv tenanturl.Resolver)
- func (h *AIMCP) AttachThumbs(p *thumb.Pipeline)
- func (h *AIMCP) AttachTickets(s *uploadTicketStore)
- func (h *AIMCP) ServeHTTP(w http.ResponseWriter, r *http.Request)
- type AITokens
- type ActivityRow
- type AppPlugins
- func (h *AppPlugins) Actions(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) Catalogue(ctx context.Context, storageID int64, rel string) (*model.Node, error)
- func (h *AppPlugins) CommitSibling(ctx context.Context, storageID int64, dir, name string, r io.Reader, ...) (string, error)
- func (h *AppPlugins) CommitVersion(ctx context.Context, storageID int64, rel string, r io.Reader, size int64, ...) error
- func (h *AppPlugins) Run(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) UICall(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) UISave(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) Users(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) View(w http.ResponseWriter, r *http.Request)
- func (h *AppPlugins) ViewEvent(w http.ResponseWriter, r *http.Request)
- type AppPluginsAdmin
- func (h *AppPluginsAdmin) CheckUpdates(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Delete(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Get(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) GetOverrides(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) GetSettings(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) ImportSigningCA(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Install(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) List(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Locks(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Logs(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Patch(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) PutOverrides(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) PutSettings(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) PutThumbLimits(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Rollback(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) RotateSigningCA(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) SigningCA(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) SigningCAs(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) ThumbLimits(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Unlock(w http.ResponseWriter, r *http.Request)
- func (h *AppPluginsAdmin) Upgrade(w http.ResponseWriter, r *http.Request)
- type AppStore
- func (h *AppStore) AppLicense(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) Cancel(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) Install(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) Intent(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) License(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) Licenses(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) ListStores(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) MountAdmin(r chi.Router)
- func (h *AppStore) PutLicense(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) TrustStore(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) UntrustStore(w http.ResponseWriter, r *http.Request)
- func (h *AppStore) VerifyLicense(w http.ResponseWriter, r *http.Request)
- type Appearance
- type AppearancePayload
- type AppearanceSource
- type Archive
- func (a *Archive) Add(w http.ResponseWriter, r *http.Request)
- func (a *Archive) AttachACL(r *acl.Resolver)
- func (a *Archive) AttachArchiveEngine(engine *archivecli.Service)
- func (a *Archive) AttachBody(b *filebody.Resolver)
- func (a *Archive) AttachDownloadTickets(s *archiveTicketStore)
- func (a *Archive) AttachFileLinks(s FileLinkStreamer, multiTenant bool)
- func (a *Archive) AttachOps(service *ops.Service)
- func (a *Archive) AttachSearchIndex(i *search.Index)
- func (a *Archive) AttachThumbs(p *thumb.Pipeline)
- func (a *Archive) Create(w http.ResponseWriter, r *http.Request)
- func (a *Archive) DownloadArchive(w http.ResponseWriter, r *http.Request)
- func (a *Archive) DownloadTicket(w http.ResponseWriter, r *http.Request)
- func (a *Archive) Extract(w http.ResponseWriter, r *http.Request)
- func (a *Archive) List(w http.ResponseWriter, r *http.Request)
- type ArchiveAdmin
- type Audit
- type Auth
- func (h *Auth) Handoff(w http.ResponseWriter, r *http.Request)
- func (h *Auth) Login(w http.ResponseWriter, r *http.Request)
- func (h *Auth) Logout(w http.ResponseWriter, r *http.Request)
- func (h *Auth) Methods(w http.ResponseWriter, r *http.Request)
- func (h *Auth) OIDCCallback(w http.ResponseWriter, r *http.Request)
- func (h *Auth) OIDCStart(w http.ResponseWriter, r *http.Request)
- func (h *Auth) SSOFor(r *http.Request) []ssoMethod
- func (h *Auth) WhoAmI(w http.ResponseWriter, r *http.Request)
- type AuthProviders
- func (h *AuthProviders) Create(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) Delete(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) List(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) SetTenants(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) SyncStart(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) SyncStatus(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) Test(w http.ResponseWriter, r *http.Request)
- func (h *AuthProviders) Update(w http.ResponseWriter, r *http.Request)
- type AuthSelf
- func (h *AuthSelf) ChangePassword(w http.ResponseWriter, r *http.Request)
- func (h *AuthSelf) Me(w http.ResponseWriter, r *http.Request)
- func (h *AuthSelf) TotpDisable(w http.ResponseWriter, r *http.Request)
- func (h *AuthSelf) TotpEnroll(w http.ResponseWriter, r *http.Request)
- func (h *AuthSelf) TotpVerify(w http.ResponseWriter, r *http.Request)
- func (h *AuthSelf) UpdateProfile(w http.ResponseWriter, r *http.Request)
- type Branding
- type BrandingConfig
- type BrandingSource
- type Capabilities
- type CapabilitiesShort
- type ChangeEmitter
- type Cloud
- func (h *Cloud) Checkout(w http.ResponseWriter, r *http.Request)
- func (h *Cloud) Plans(w http.ResponseWriter, r *http.Request)
- func (h *Cloud) Register(r chi.Router)
- func (h *Cloud) Signup(w http.ResponseWriter, r *http.Request)
- func (h *Cloud) Status(w http.ResponseWriter, r *http.Request)
- func (h *Cloud) Verify(w http.ResponseWriter, r *http.Request)
- func (h *Cloud) Webhook(w http.ResponseWriter, r *http.Request)
- type Comments
- type CustomCSS
- type Dashboard
- type DesktopAuth
- type DiscoveredFolder
- type Drop
- func (h *Drop) AttachACL(r *acl.Resolver)
- func (h *Drop) AttachBranding(b *BrandingSource)
- func (h *Drop) AttachLocale(def string)
- func (h *Drop) AttachShell(shell http.Handler)
- func (h *Drop) AttachTenants(rv tenanturl.Resolver)
- func (h *Drop) Page(w http.ResponseWriter, r *http.Request)
- func (h *Drop) Upload(w http.ResponseWriter, r *http.Request)
- type Duplicates
- type E2E
- func (h *E2E) AttachACL(r *acl.Resolver)
- func (h *E2E) AttachCleanup(v E2ECleanupVersions, t E2ECleanupTrash, th E2ECleanupThumbs, ...)
- func (h *E2E) Cleanup(w http.ResponseWriter, r *http.Request)
- func (h *E2E) EscrowChallenge(w http.ResponseWriter, r *http.Request)
- func (h *E2E) EscrowUsed(w http.ResponseWriter, r *http.Request)
- func (h *E2E) PasswordChanged(w http.ResponseWriter, r *http.Request)
- type E2ECleanupIndex
- type E2ECleanupThumbs
- type E2ECleanupTrash
- type E2ECleanupVersions
- type E2EPolicyAdmin
- func (h *E2EPolicyAdmin) ApproveRequest(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) Get(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) ListRequests(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) Patch(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) PatchTenant(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) RejectRequest(w http.ResponseWriter, r *http.Request)
- func (h *E2EPolicyAdmin) Tenants(w http.ResponseWriter, r *http.Request)
- type E2EPolicyFiles
- type ExternalAdmin
- type FTPSFacts
- type FileLinkStreamer
- type FileTypesAdmin
- type FolderPreviewSwitch
- type Grants
- func (h *Grants) AdminDelete(w http.ResponseWriter, r *http.Request)
- func (h *Grants) AdminDeleteGroup(w http.ResponseWriter, r *http.Request)
- func (h *Grants) AdminList(w http.ResponseWriter, r *http.Request)
- func (h *Grants) AttachInvite(sh *share.Service, m *mailer.Service, publicURL string)
- func (h *Grants) AttachTenants(rv tenanturl.Resolver)
- func (h *Grants) Create(w http.ResponseWriter, r *http.Request)
- func (h *Grants) Delete(w http.ResponseWriter, r *http.Request)
- func (h *Grants) DeleteGroup(w http.ResponseWriter, r *http.Request)
- func (h *Grants) Invite(w http.ResponseWriter, r *http.Request)
- func (h *Grants) List(w http.ResponseWriter, r *http.Request)
- func (h *Grants) Resolve(w http.ResponseWriter, r *http.Request)
- func (h *Grants) SearchGroups(w http.ResponseWriter, r *http.Request)
- func (h *Grants) SearchUsers(w http.ResponseWriter, r *http.Request)
- func (h *Grants) ShareMail(w http.ResponseWriter, r *http.Request)
- func (h *Grants) Update(w http.ResponseWriter, r *http.Request)
- func (h *Grants) UpdateGroup(w http.ResponseWriter, r *http.Request)
- type GroupsAdmin
- func (h *GroupsAdmin) AddMembers(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Create(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Delete(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Detach(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Get(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) List(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Memberships(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) RemoveMember(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) Update(w http.ResponseWriter, r *http.Request)
- func (h *GroupsAdmin) UserGroups(w http.ResponseWriter, r *http.Request)
- type LazyCatalogue
- type LoginSecurity
- func (h *LoginSecurity) Attempts(w http.ResponseWriter, r *http.Request)
- func (h *LoginSecurity) Get(w http.ResponseWriter, r *http.Request)
- func (h *LoginSecurity) Locks(w http.ResponseWriter, r *http.Request)
- func (h *LoginSecurity) Patch(w http.ResponseWriter, r *http.Request)
- func (h *LoginSecurity) Unlock(w http.ResponseWriter, r *http.Request)
- type Manager
- func (h *Manager) AttachACL(r *acl.Resolver)
- func (h *Manager) AttachBody(b *filebody.Resolver)
- func (h *Manager) AttachChangeLog(l *realtime.ChangeLog)
- func (h *Manager) AttachLazy(l LazyCatalogue)
- func (h *Manager) AttachOps(o *ops.Service)
- func (h *Manager) AttachSearchIndex(idx *search.Index)
- func (h *Manager) AttachStaged(s *StagedUpload)
- func (h *Manager) AttachThumbPipeline(p ThumbPipeline)
- func (h *Manager) CountDrafts(w http.ResponseWriter, r *http.Request)
- func (h *Manager) CreateDraft(w http.ResponseWriter, r *http.Request)
- func (h *Manager) DiscardDraft(w http.ResponseWriter, r *http.Request)
- func (h *Manager) EnsureDir(ctx context.Context, st *model.Storage, rel string) (*int64, error)
- func (h *Manager) GetDraft(w http.ResponseWriter, r *http.Request)
- func (h *Manager) IngestFile(ctx context.Context, st *model.Storage, destRel, filename string, ...) (*model.Node, error)
- func (h *Manager) List(w http.ResponseWriter, r *http.Request)
- func (h *Manager) ListDrafts(w http.ResponseWriter, r *http.Request)
- func (h *Manager) Mutate(w http.ResponseWriter, r *http.Request)
- func (h *Manager) NameTaken(ctx context.Context, storageID int64, src, dst string) (bool, error)
- func (h *Manager) Read(w http.ResponseWriter, r *http.Request)
- func (h *Manager) SaveDraft(w http.ResponseWriter, r *http.Request)
- func (h *Manager) ServeLinkedFile(w http.ResponseWriter, r *http.Request, s *model.Storage, rel string)
- func (h *Manager) Stat(w http.ResponseWriter, r *http.Request)
- func (h *Manager) SyncCopy(ctx context.Context, storageID int64, src, dst string)
- func (h *Manager) SyncCopyAcross(ctx context.Context, srcStorageID int64, src string, dstStorageID int64, ...)
- func (h *Manager) SyncHardDelete(ctx context.Context, storageID int64, src string)
- func (h *Manager) SyncMove(ctx context.Context, storageID int64, src, dst string)
- func (h *Manager) SyncRename(ctx context.Context, storageID int64, src, dst string)
- func (h *Manager) SyncSoftDelete(ctx context.Context, storageID int64, src, trashRel string)
- type Meta
- func (h *Meta) AttachACL(r *acl.Resolver)
- func (h *Meta) AttachThumbSigner(s *thumb.Signer)
- func (h *Meta) GetTags(w http.ResponseWriter, r *http.Request)
- func (h *Meta) ListAllTags(w http.ResponseWriter, r *http.Request)
- func (h *Meta) ListRecent(w http.ResponseWriter, r *http.Request)
- func (h *Meta) ListStarred(w http.ResponseWriter, r *http.Request)
- func (h *Meta) SetRecent(w http.ResponseWriter, r *http.Request)
- func (h *Meta) SetStar(w http.ResponseWriter, r *http.Request)
- func (h *Meta) SetTags(w http.ResponseWriter, r *http.Request)
- func (h *Meta) TaggedNodes(w http.ResponseWriter, r *http.Request)
- type NFSExports
- type Notifications
- func (h *Notifications) AdminList(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) AdminTest(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) AdminUpdateWebhookConfig(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) AdminWebhookConfig(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) GetSettings(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) List(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) MarkAllRead(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) MarkRead(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) UnreadCount(w http.ResponseWriter, r *http.Request)
- func (h *Notifications) UpdateSettings(w http.ResponseWriter, r *http.Request)
- type OnlyOffice
- func (h *OnlyOffice) AttachACL(r *acl.Resolver)
- func (h *OnlyOffice) AttachBody(b *filebody.Resolver)
- func (h *OnlyOffice) Callback(w http.ResponseWriter, r *http.Request)
- func (h *OnlyOffice) Config(w http.ResponseWriter, r *http.Request)
- func (h *OnlyOffice) Diagnose(w http.ResponseWriter, r *http.Request)
- func (h *OnlyOffice) Fetch(w http.ResponseWriter, r *http.Request)
- type OpenWith
- type Ops
- func (o *Ops) AttachACL(r *acl.Resolver)
- func (o *Ops) Cancel(w http.ResponseWriter, r *http.Request)
- func (o *Ops) List(w http.ResponseWriter, r *http.Request)
- func (o *Ops) Status(w http.ResponseWriter, r *http.Request)
- func (o *Ops) Submit(w http.ResponseWriter, r *http.Request)
- func (o *Ops) SubmitCopy(w http.ResponseWriter, r *http.Request)
- func (o *Ops) SubmitDelete(w http.ResponseWriter, r *http.Request)
- func (o *Ops) SubmitMove(w http.ResponseWriter, r *http.Request)
- type PermissionsAdmin
- func (h *PermissionsAdmin) Catalogue(w http.ResponseWriter, _ *http.Request)
- func (h *PermissionsAdmin) CreateRule(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) DeleteRule(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) DismissGap(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) GetDefaults(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) GetUserPermissions(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) GetUserRoles(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) ListGaps(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) ListOverrides(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) ListRules(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) MyPermissions(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) PreviewRole(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) PutDefaults(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) PutUserPermissions(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) PutUserRoles(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) RestoreGap(w http.ResponseWriter, r *http.Request)
- func (h *PermissionsAdmin) UpdateRule(w http.ResponseWriter, r *http.Request)
- type PluginRequests
- func (h *PluginRequests) Approve(w http.ResponseWriter, r *http.Request)
- func (h *PluginRequests) Create(w http.ResponseWriter, r *http.Request)
- func (h *PluginRequests) Get(w http.ResponseWriter, r *http.Request)
- func (h *PluginRequests) List(w http.ResponseWriter, r *http.Request)
- func (h *PluginRequests) Reject(w http.ResponseWriter, r *http.Request)
- type Plugins
- func (h *Plugins) CheckUpdates(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Delete(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Get(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Install(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) List(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Logs(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Patch(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Restart(w http.ResponseWriter, r *http.Request)
- func (h *Plugins) Upgrade(w http.ResponseWriter, r *http.Request)
- type Protection
- type Providers
- func (h *Providers) Create(w http.ResponseWriter, r *http.Request)
- func (h *Providers) Delete(w http.ResponseWriter, r *http.Request)
- func (h *Providers) Get(w http.ResponseWriter, r *http.Request)
- func (h *Providers) LinkStorage(w http.ResponseWriter, r *http.Request)
- func (h *Providers) List(w http.ResponseWriter, r *http.Request)
- func (h *Providers) RealmSuggestion(w http.ResponseWriter, r *http.Request)
- func (h *Providers) UnlinkStorage(w http.ResponseWriter, r *http.Request)
- func (h *Providers) Update(w http.ResponseWriter, r *http.Request)
- type PublicAPI
- func (h *PublicAPI) AttachACL(r *acl.Resolver)
- func (h *PublicAPI) AttachApps(a *AppPlugins)
- func (h *PublicAPI) AttachBranding(b *BrandingSource)
- func (h *PublicAPI) AttachDrop(d *Drop)
- func (h *PublicAPI) AttachLocale(def string)
- func (h *PublicAPI) Branding(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) DropState(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) DropUpload(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) Entries(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) Event(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) File(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) PIN(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) Share(w http.ResponseWriter, r *http.Request)
- func (h *PublicAPI) UILocale(w http.ResponseWriter, r *http.Request)
- type PublicApp
- type PublicBranding
- type PublicDrop
- type PublicEntry
- type PublicNode
- type PublicShare
- type PublicUILocale
- type Queue
- func (h *Queue) AttachStore(s db.Store)
- func (h *Queue) Cancel(w http.ResponseWriter, r *http.Request)
- func (h *Queue) Get(w http.ResponseWriter, r *http.Request)
- func (h *Queue) List(w http.ResponseWriter, r *http.Request)
- func (h *Queue) Retry(w http.ResponseWriter, r *http.Request)
- func (h *Queue) Stats(w http.ResponseWriter, r *http.Request)
- type QueueStats
- type Quota
- func (h *Quota) AdminGet(w http.ResponseWriter, r *http.Request)
- func (h *Quota) AdminRecompute(w http.ResponseWriter, r *http.Request)
- func (h *Quota) AdminSet(w http.ResponseWriter, r *http.Request)
- func (h *Quota) AttachACL(r *acl.Resolver)
- func (h *Quota) AttachLazy(l LazyCatalogue)
- func (h *Quota) Me(w http.ResponseWriter, r *http.Request)
- func (h *Quota) StorageUsage(w http.ResponseWriter, r *http.Request)
- type Replica
- func (h *Replica) CountFailures(w http.ResponseWriter, r *http.Request)
- func (h *Replica) CreateRule(w http.ResponseWriter, r *http.Request)
- func (h *Replica) DeleteRule(w http.ResponseWriter, r *http.Request)
- func (h *Replica) FixAll(w http.ResponseWriter, r *http.Request)
- func (h *Replica) FixOne(w http.ResponseWriter, r *http.Request)
- func (h *Replica) GetReport(w http.ResponseWriter, r *http.Request)
- func (h *Replica) GetSettings(w http.ResponseWriter, r *http.Request)
- func (h *Replica) ListFailures(w http.ResponseWriter, r *http.Request)
- func (h *Replica) ListRules(w http.ResponseWriter, r *http.Request)
- func (h *Replica) RunReportNow(w http.ResponseWriter, r *http.Request)
- func (h *Replica) UpdateRule(w http.ResponseWriter, r *http.Request)
- func (h *Replica) UpdateSettings(w http.ResponseWriter, r *http.Request)
- type ReplicationTargets
- func (h *ReplicationTargets) Create(w http.ResponseWriter, r *http.Request)
- func (h *ReplicationTargets) Delete(w http.ResponseWriter, r *http.Request)
- func (h *ReplicationTargets) Get(w http.ResponseWriter, r *http.Request)
- func (h *ReplicationTargets) List(w http.ResponseWriter, r *http.Request)
- func (h *ReplicationTargets) Update(w http.ResponseWriter, r *http.Request)
- type Response
- type S3Keys
- type SSHKeys
- type SanitizedCSS
- type SaveText
- type Search
- type SearchAdmin
- type SelfTokens
- type Settings
- func (h *Settings) AttachAppearance(a *AppearanceSource)
- func (h *Settings) AttachBranding(b *BrandingSource)
- func (h *Settings) AttachMailer(m *mailer.Service)
- func (h *Settings) List(w http.ResponseWriter, r *http.Request)
- func (h *Settings) SMTPTest(w http.ResponseWriter, r *http.Request)
- func (h *Settings) Set(w http.ResponseWriter, r *http.Request)
- func (h *Settings) Update(w http.ResponseWriter, r *http.Request)
- type Share
- func (h *Share) AttachACL(r *acl.Resolver)
- func (h *Share) AttachApps(a *AppPlugins)
- func (h *Share) AttachBody(b *filebody.Resolver)
- func (h *Share) AttachBranding(b *BrandingSource)
- func (h *Share) AttachIndex(idx *search.Index)
- func (h *Share) AttachLocale(def string)
- func (h *Share) AttachShell(shell http.Handler)
- func (h *Share) AttachTenants(rv tenanturl.Resolver)
- func (h *Share) AttachThumbs(p *thumb.Pipeline)
- func (h *Share) HandleBrowseFile(w http.ResponseWriter, r *http.Request)
- func (h *Share) HandleCreate(w http.ResponseWriter, r *http.Request)
- func (h *Share) HandleDelete(w http.ResponseWriter, r *http.Request)
- func (h *Share) HandleDownload(w http.ResponseWriter, r *http.Request)
- func (h *Share) HandleList(w http.ResponseWriter, r *http.Request)
- func (h *Share) HandleMetadata(w http.ResponseWriter, r *http.Request)
- type ShareX
- func (h *ShareX) AttachACL(r *acl.Resolver)
- func (h *ShareX) AttachBody(b *filebody.Resolver)
- func (h *ShareX) AttachE2EPolicy(s *e2epolicy.Service)
- func (h *ShareX) AttachSearchIndex(idx *search.Index)
- func (h *ShareX) AttachStaged(s *StagedUpload)
- func (h *ShareX) AttachTenants(rv tenanturl.Resolver)
- func (h *ShareX) AttachThumbs(p *thumb.Pipeline)
- func (h *ShareX) Upload(w http.ResponseWriter, r *http.Request)
- type Shared
- type SharesAdmin
- func (h *SharesAdmin) AttachApps(reg *wasmplugin.Registry)
- func (h *SharesAdmin) AttachTenants(rv tenanturl.Resolver)
- func (h *SharesAdmin) Delete(w http.ResponseWriter, r *http.Request)
- func (h *SharesAdmin) List(w http.ResponseWriter, r *http.Request)
- func (h *SharesAdmin) ListAppPluginShares(w http.ResponseWriter, r *http.Request)
- func (h *SharesAdmin) Revoke(w http.ResponseWriter, r *http.Request)
- type SharesMine
- type StagedUpload
- func (h *StagedUpload) Abort(w http.ResponseWriter, r *http.Request)
- func (h *StagedUpload) AttachACL(r *acl.Resolver)
- func (h *StagedUpload) Begin(w http.ResponseWriter, r *http.Request)
- func (h *StagedUpload) Commit(w http.ResponseWriter, r *http.Request)
- func (h *StagedUpload) CommitUpload(ctx context.Context, uploadID string) error
- func (h *StagedUpload) IngestStream(ctx context.Context, storageID int64, storageKey string, src io.Reader, ...) (*model.Node, error)
- func (h *StagedUpload) Put(w http.ResponseWriter, r *http.Request)
- func (h *StagedUpload) RecoverUnstored(ctx context.Context) int
- func (h *StagedUpload) RunSweeper(ctx context.Context, interval time.Duration)
- func (h *StagedUpload) ShouldStage(size int64) bool
- func (h *StagedUpload) StagedReady() bool
- func (h *StagedUpload) StagedThreshold() int64
- func (h *StagedUpload) Status(w http.ResponseWriter, r *http.Request)
- func (h *StagedUpload) Sweep(ctx context.Context) (int, int)
- type StorageDrivers
- type StorageSummary
- type StorageUsageRow
- type Storages
- func (h *Storages) Create(w http.ResponseWriter, r *http.Request)
- func (h *Storages) Delete(w http.ResponseWriter, r *http.Request)
- func (h *Storages) Get(w http.ResponseWriter, r *http.Request)
- func (h *Storages) List(w http.ResponseWriter, r *http.Request)
- func (h *Storages) SetOrder(w http.ResponseWriter, r *http.Request)
- func (h *Storages) TriggerSync(w http.ResponseWriter, r *http.Request)
- func (h *Storages) Update(w http.ResponseWriter, r *http.Request)
- type StoragesAdmin
- type SyncAdmin
- type TLSHooks
- type TenantSelf
- func (h *TenantSelf) AddDomain(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) CheckDomain(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) CreateProvider(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) DeleteCertificate(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) DeleteDomain(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) DeleteProvider(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) Get(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) SetCertificate(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) SetInsecure(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) TestProvider(w http.ResponseWriter, r *http.Request)
- func (h *TenantSelf) UpdateProvider(w http.ResponseWriter, r *http.Request)
- type ThemeExport
- type ThemeWire
- type Themes
- type Thumb
- type ThumbPipeline
- type ThumbRefresher
- type ThumbRepair
- func (h *ThumbRepair) Generators(w http.ResponseWriter, r *http.Request)
- func (h *ThumbRepair) Problems(w http.ResponseWriter, r *http.Request)
- func (h *ThumbRepair) Settings(w http.ResponseWriter, r *http.Request)
- func (h *ThumbRepair) Start(w http.ResponseWriter, r *http.Request)
- func (h *ThumbRepair) Status(w http.ResponseWriter, r *http.Request)
- func (h *ThumbRepair) UpdateSettings(w http.ResponseWriter, r *http.Request)
- type ThumbSettingsForgetter
- type TicketUpload
- func (h *TicketUpload) AttachACL(r *acl.Resolver)
- func (h *TicketUpload) AttachBody(b *filebody.Resolver)
- func (h *TicketUpload) AttachE2EPolicy(s *e2epolicy.Service)
- func (h *TicketUpload) AttachSearchIndex(idx *search.Index)
- func (h *TicketUpload) AttachStaged(s *StagedUpload)
- func (h *TicketUpload) AttachThumbs(p *thumb.Pipeline)
- func (h *TicketUpload) Upload(w http.ResponseWriter, r *http.Request)
- type Trash
- func (h *Trash) AdminEmpty(w http.ResponseWriter, r *http.Request)
- func (h *Trash) AttachACL(r *acl.Resolver)
- func (h *Trash) AttachOps(o *ops.Service)
- func (h *Trash) AttachSearchIndex(i *search.Index)
- func (h *Trash) EmptyStatus(w http.ResponseWriter, r *http.Request)
- func (h *Trash) List(w http.ResponseWriter, r *http.Request)
- func (h *Trash) Purge(w http.ResponseWriter, r *http.Request)
- func (h *Trash) PurgeNode(ctx context.Context, storageID, nodeID int64) error
- func (h *Trash) Restore(w http.ResponseWriter, r *http.Request)
- func (h *Trash) RestoreNode(ctx context.Context, storageID, nodeID int64) error
- type Update
- type Upload
- type Usage
- type UserPrefs
- type Users
- func (h *Users) Create(w http.ResponseWriter, r *http.Request)
- func (h *Users) Delete(w http.ResponseWriter, r *http.Request)
- func (h *Users) Get(w http.ResponseWriter, r *http.Request)
- func (h *Users) List(w http.ResponseWriter, r *http.Request)
- func (h *Users) Update(w http.ResponseWriter, r *http.Request)
- type UsersAdmin
- type VersionSnapshotter
- type Versions
- func (h *Versions) AttachACL(r *acl.Resolver)
- func (h *Versions) AttachSearchIndex(i *search.Index)
- func (h *Versions) HardDelete(w http.ResponseWriter, r *http.Request)
- func (h *Versions) List(w http.ResponseWriter, r *http.Request)
- func (h *Versions) Restore(w http.ResponseWriter, r *http.Request)
- func (h *Versions) Snapshot(w http.ResponseWriter, r *http.Request)
- type ViewPrefs
- type WS
- type WebhooksAdmin
- func (h *WebhooksAdmin) Create(w http.ResponseWriter, r *http.Request)
- func (h *WebhooksAdmin) Delete(w http.ResponseWriter, r *http.Request)
- func (h *WebhooksAdmin) List(w http.ResponseWriter, r *http.Request)
- func (h *WebhooksAdmin) Test(w http.ResponseWriter, r *http.Request)
- func (h *WebhooksAdmin) Update(w http.ResponseWriter, r *http.Request)
Constants ¶
const ( // SurfaceWeb is the browser. SurfaceWeb = "web" // SurfaceDesktop is the desktop application. SurfaceDesktop = "desktop" )
Surfaces a preference document may belong to.
const AuditActionPinReveal = share.AuditActionPinReveal
AuditActionPinReveal is the audit row every PIN read writes — success or refusal-after-authorization alike. Exported so a test can name it rather than repeat the string, and so an operator grepping the audit table has one spelling to grep for.
const BrandingSettingTheme = "branding.theme"
BrandingSettingTheme is the settings key holding the public shell's default appearance.
const BuiltinDefaultThemeID = "default"
BuiltinDefaultThemeID is the stock palette: the product's own colours, and the answer whenever a named theme cannot be resolved.
CodeEntryUnavailable is the refusal's code.
const CustomCSSEnabledSettingKey = "ui.custom_css_enabled"
CustomCSSEnabledSettingKey is the switch. Absent, or anything other than a true-ish string, means OFF — the direction that makes a missing row safe.
const CustomCSSImmuneClass = "fe-css-immune"
CustomCSSImmuneClass marks a subtree the operator stylesheet may not match inside. The admin Appearance panel and destructive confirmations carry it.
⚠ A plain class and not a data attribute, so a single `@scope` limit can name it; `fe-`prefixed like every other class the packages own.
const CustomCSSMaxBytes = 64 * 1024
CustomCSSMaxBytes caps the stored sheet. 64 KiB is roughly ten times the largest hand-written token override we ship, and small enough that a sheet with Turkish comments in it is never what makes a page slow. The admin page shows the same number and counts UTF-8 bytes, so the two agree.
const CustomCSSSettingKey = "ui.custom_css"
CustomCSSSettingKey is the settings row holding the operator stylesheet.
const CustomThemePrefix = "custom:"
CustomThemePrefix marks a custom theme's palette id apart from every built-in one. Stored WITHOUT it (custom_themes.theme_key), served WITH it.
const DefaultFolderViewSettingKey = "ui.default_folder_view"
DefaultFolderViewSettingKey is the operator's default folder view: what a folder opens as for a person who has neither changed that folder nor set a default of their own. Resolution, per field: the folder's own memory → the person's default → THIS → filex's built-in (list, name ↑).
⚠ Instance-wide, so `allowSettingWrite` refuses it to a tenant admin for the same reason it refuses `ui.default_theme`: it is not `branding.*`, so it has no per-tenant home, and a tenant admin writing it would set the view every other tenant's people open their folders in.
const DefaultThemeSettingKey = "ui.default_theme"
DefaultThemeSettingKey names the palette a person sees before they have chosen one — and the ONLY palette an anonymous visitor's share page can follow, since that page has no account to read a preference from.
const PluginRequestsPath = "/api/admin/plugin-requests"
PluginRequestsPath is where somebody who may not install a plugin leaves a request for it (plugin_requests.go). The refusals below name it.
const PrepareOptInHeader = "X-Filex-Accept-Prepare"
PrepareOptInHeader is how a programmatic client says it can use the "preparing" answer (202 + JSON + Retry-After) instead of a slow stream.
const ProbeTimeout = 10 * time.Second
ProbeTimeout bounds one "Test connection" attempt.
⚠ This is NOT a driver setting, and lowering the drivers' own retry budgets would be the wrong fix. The S3 driver retries six times with a backoff capped at ten seconds (see s3.newRetryer) precisely so a *sync run* rides out an object store's transient 503 instead of dying on one listing page — measured against Hetzner Object Storage, twelve times in a month. That budget is right for a background worker and wrong for a person waiting on a button: with an endpoint that refuses the connection outright, the six attempts ran to completion and this endpoint answered in 23.6s and 29.7s on two consecutive measured calls. Cypress spec 43-storages-crud spends its whole duration here.
So the retry budget stays and the PROBE gets a deadline. Every driver honours the context — the AWS SDK abandons its remaining attempts, and a hung SFTP or WebDAV dial is bounded by the same line — and a local-path probe, which answered in 6ms, never comes near it. Ten seconds is far more than a healthy remote store needs to list one prefix and short enough that the answer arrives while the operator is still looking at the form.
const SurfaceAccount = "account"
SurfaceAccount is the user_prefs row that holds what belongs to the account rather than to one surface. It is not a surface a client may name in /api/me/prefs (prefsSurface refuses it): it is written one kind at a time, here.
const ThemeExportVersion = 1
ThemeExportVersion is the shape marker carried by an exported file. A theme built for one customer is meant to be carried to another, which means the file outlives the version that wrote it; a marker is what lets a later filex refuse a file it cannot read instead of importing half of it.
Variables ¶
ErrStagingUnavailable means this instance cannot stage: no staging directory, no ops queue, or no storage resolver. Callers treat it as "write synchronously instead", never as a failure — an operator who has not configured staging must still be able to upload.
var ScopedRescanTimeout = 10 * time.Minute
ScopedRescanTimeout bounds a folder rescan (TriggerSync with ?path=), which answers inside the request.
Functions ¶
func AppNewDocTypes ¶ added in v0.48.0
func AppNewDocTypes(reg *wasmplugin.Registry) []newdoc.Type
AppNewDocTypes is every row the running apps add, as document types; nil without a registry.
func ArchiveName ¶
ArchiveName picks the filename for an archive of `paths`.
The rule, in order:
- an explicit name from the caller, sanitized;
- one source → that source's own basename, so downloading the folder "Faturalar" gives you "Faturalar.zip";
- many sources sharing a parent → the parent's basename, because that is the folder the person was standing in;
- anything else → "filex-N-items".
A name that says something is not decoration. "download.zip" in a downloads folder next to four other "download (3).zip" is a file nobody can identify a week later.
func DemoRefusal ¶ added in v0.50.0
DemoRefusal is a public demo's answer to a state-changing request on an operator surface: api.DemoGuard writes it for the routes, and the admin MCP tools - which reach the same handlers in-process, past every route - answer it too (AIAdmin.invoke).
func ImportTheme ¶
func ImportTheme(raw []byte) (*model.CustomTheme, error)
ImportTheme parses and validates a portable document.
func NewArchiveTicketStore ¶
func NewArchiveTicketStore() *archiveTicketStore
NewArchiveTicketStore builds the in-memory store.
func NewUploadTicketStore ¶
func NewUploadTicketStore() *uploadTicketStore
NewUploadTicketStore builds the in-memory ticket store.
func RequireAdminPermission ¶ added in v0.49.0
RequireAdminPermission gates one route of the delegated admin area. A full administrator (auth.CallerMayAdminister — the account role, and a token that may administer) passes as before. Anybody else passes only with the admin permission p, and only from a signed-in session: a self-service token can never carry the admin scope (tokens_self.go), so a delegated administrator using one would be the only token that administers without it.
⚠ This is the ONLY door into the routes it guards: they are registered outside the RequireAdmin group precisely so this can admit a non-admin. Every handler behind it must therefore hold its own line against escalation (see refuseAdminTarget) — the route cannot know which account a request is about.
func RequirePermission ¶ added in v0.49.0
RequirePermission is route middleware for a permission that is not about a path: the whole door is the action (minting an S3 key, the agent API, posting a comment). It must run after the auth middleware has put the user on the context. A nil resolver (tests) allows.
func RequirePersonalCaller ¶
RequirePersonalCaller refuses a request that arrived on an app token. Mounted on the self-service credential routes in routes.go — one place, so a new credential surface joins the rule by being registered inside the group rather than by remembering to copy a check.
func ResolveThemeID ¶
func ResolveThemeID(requested string, custom []*model.CustomTheme) string
ResolveThemeID answers "which palette should this be?" for an id that may name a theme which no longer exists.
⚠⚠ THIS IS HOW DELETION IS SAFE. Nothing rewrites the stored choices of the people who were using a deleted theme; every read RESOLVES instead. A cleanup pass has to know every place an id can be written — localStorage in one browser, a per-user document in the database, the instance default in settings — and it is wrong the moment a fourth place appears. Resolution is right by construction: an id that is not in the list of themes that exist is the stock palette, everywhere.
func RetiredPageAPI ¶
RetiredPageAPI answers the retired /api/p/<token>/… addresses. The JSON surface moved to /api/public/s/<token>, so the redirect points there.
func RetiredPagePrefix ¶
RetiredPagePrefix answers the retired /p/<token> address.
An app plugin's public page is a share now, so its link is /s/<token>. The old prefix redirects there permanently: a link already sitting in somebody's inbox then lands on a page that can explain itself (and, for the pages that existed before the change, say honestly that they are gone) instead of on a 404 with no context.
⚠ 301 and not a rewrite. The address changed for good, and a redirect is what makes a browser, a mail client and a crawler all agree on that.
func SetAntivirusEnqueue ¶
SetAntivirusEnqueue installs the enqueue function used after writes. Call once at startup; passing nil disables scanning.
func SetAntivirusEnqueueAfterSave ¶
SetAntivirusEnqueueAfterSave installs the debounced enqueue used when the browser's text editor SAVES over a file that already exists.
Why a second sink rather than a flag on the first: the two have different contracts, and confusing them is how the gap this closes got created. The immediate one scans now, and every upload surface wants it. This one schedules one scan per file per editing window and drops the requests that arrive inside it — correct for Ctrl+S, wrong for anything that writes a whole file once.
func SetAppLabels ¶
SetAppLabels wires the registry's AppLabel.
func SetChangeEmitter ¶
func SetChangeEmitter(e ChangeEmitter)
SetChangeEmitter installs the realtime emitter. Call once at startup after constructing the hub, e.g. handlers.SetChangeEmitter(hub). Passing nil disables emission.
func SetLockReasons ¶
func SetLockReasons(f func(*model.AppPluginLock) (string, wire.Text))
SetLockReasons wires the registry's LockReason.
func SetNotifySink ¶
SetNotifySink installs the notify service used for file-event emission. Call once at startup; passing nil disables emission.
func ThemeAllTokens ¶
func ThemeAllTokens() []string
ThemeAllTokens exposes the whole allowlist, in declaration order.
func ThemeAuthoredColorTokens ¶
func ThemeAuthoredColorTokens() []string
ThemeAuthoredColorTokens exposes the authored colour list so a token added here and not to the editor is caught by a test rather than discovered.
func ValidateCustomTheme ¶
func ValidateCustomTheme(t *model.CustomTheme) error
ValidateCustomTheme checks a whole theme before it is stored or exported. It normalises in place (trimmed name, lowercased key) so callers store exactly what was checked.
Types ¶
type AI ¶
type AI struct {
// contains filtered or unexported fields
}
AI is the token-authenticated REST surface consumed by AI agents and the work.example.com FilexClient. It is a thin HTTP adapter over aiOps; the routes are mounted under /api/ai behind auth.APITokenMiddleware + RequireScope.
Contract (all JSON unless noted):
GET /api/ai/files?path=<adapter://dir> → {entries:[…]}
GET /api/ai/info?path=<adapter://file> → {entry:{…}}
GET /api/ai/download?path=<adapter://file> → raw bytes (stream)
POST /api/ai/upload → {entry:{…}} (see body below)
POST /api/ai/upload/ticket {"path":"…"} → {url,curl,…} (credential-free
upload URL; upload_ticket.go)
POST /api/ai/delete {"path":"…"} → {ok:true}
POST /api/ai/mkdir {"path":"…"} → {entry:{…}}
POST /api/ai/move {"src":"…","dst":"…"} → {entry:{…}}
GET /api/ai/search?path=<adapter://>&q=… → {entries:[…]}
POST /api/ai/zip {"sources":[…],"dest":"…"} → {entry:{…}} (server-side)
POST /api/ai/unzip {"src":"…","dest":"…"} → {ok,extracted}
func NewAI ¶
func NewAI(store db.Store, resolver func(int64) (storage.Driver, error), shareSvc *share.Service, publicURL string) *AI
NewAI constructs the AI REST handler. shareSvc + publicURL power the share endpoints (pass nil shareSvc to disable sharing).
func (*AI) AppActions ¶ added in v0.50.0
func (h *AI) AppActions(w http.ResponseWriter, r *http.Request)
AppActions → GET /api/ai/apps/actions?path=
func (*AI) AppRun ¶ added in v0.50.0
func (h *AI) AppRun(w http.ResponseWriter, r *http.Request)
AppRun → POST /api/ai/apps/run {plugin, action, paths, params?}.
func (*AI) ArchiveCreate ¶ added in v0.50.0
func (h *AI) ArchiveCreate(w http.ResponseWriter, r *http.Request)
ArchiveCreate → POST /api/ai/archive/create {sources, dest, format?, password?, allow_plaintext?}.
func (*AI) ArchiveExtract ¶ added in v0.50.0
func (h *AI) ArchiveExtract(w http.ResponseWriter, r *http.Request)
ArchiveExtract → POST /api/ai/archive/extract {path, dest?, members?, password?, allow_plaintext?}.
func (*AI) AttachACL ¶
AttachACL wires the RBAC resolver into the AI REST surface's ops core so every /api/ai file op is gated by the bound user's grants + role ceiling.
func (*AI) AttachBody ¶
AttachBody wires the byte-source resolver so /api/ai reads serve a file that is still being transferred out of staging.
func (*AI) AttachDoors ¶ added in v0.50.0
AttachDoors wires the explorer's handlers into the AI REST surface.
func (*AI) AttachE2EPolicy ¶ added in v0.51.0
AttachE2EPolicy wires the rule into the agent REST surface: WriteStream (upload), Zip and Unzip ask it.
func (*AI) AttachSearchIndex ¶
AttachSearchIndex wires the search index into the ops core, so a file an agent writes is searchable at once rather than when the next storage sync walks the folder.
func (*AI) AttachStaged ¶
func (h *AI) AttachStaged(s *StagedUpload)
AttachStaged routes writes above the chunk threshold through filex's staging area, exactly as the browser and the CLI do (nil = synchronous writes).
func (*AI) AttachTenants ¶
AttachTenants wires the shared origin resolver (internal/tenanturl).
func (*AI) AttachThumbs ¶
AttachThumbs wires the thumbnail pipeline so AI-surface writes dispatch generation like manager uploads (nil = thumbnails skipped).
func (*AI) AttachTickets ¶
func (h *AI) AttachTickets(s *uploadTicketStore)
AttachTickets wires the shared ticket store into the AI REST surface.
func (*AI) CommentAdd ¶ added in v0.50.0
func (h *AI) CommentAdd(w http.ResponseWriter, r *http.Request)
CommentAdd → POST /api/ai/comments {path, text}.
func (*AI) CommentDelete ¶ added in v0.50.0
func (h *AI) CommentDelete(w http.ResponseWriter, r *http.Request)
CommentDelete → POST /api/ai/comments/{id}/delete
func (*AI) CommentsList ¶ added in v0.50.0
func (h *AI) CommentsList(w http.ResponseWriter, r *http.Request)
CommentsList → GET /api/ai/comments?path=
func (*AI) Convert ¶ added in v0.50.0
func (h *AI) Convert(w http.ResponseWriter, r *http.Request)
Convert → POST /api/ai/convert {path, target}.
func (*AI) Copy ¶ added in v0.50.0
func (h *AI) Copy(w http.ResponseWriter, r *http.Request)
Copy → POST /api/ai/copy {src, dst}.
func (*AI) Delete ¶
func (h *AI) Delete(w http.ResponseWriter, r *http.Request)
Delete → POST /api/ai/delete {"path":"…"}.
func (*AI) Download ¶
func (h *AI) Download(w http.ResponseWriter, r *http.Request)
Download → GET /api/ai/download?path=<adapter://file> (streams bytes).
func (*AI) FileRequest ¶ added in v0.50.0
func (h *AI) FileRequest(w http.ResponseWriter, r *http.Request)
FileRequest → POST /api/ai/share/request {path, max_uploads?, drop_settings?, expires_in_days?, pin?}.
func (*AI) Info ¶
func (h *AI) Info(w http.ResponseWriter, r *http.Request)
Info → GET /api/ai/info?path=<adapter://file>
func (*AI) List ¶
func (h *AI) List(w http.ResponseWriter, r *http.Request)
List → GET /api/ai/files?path=<adapter://dir>
func (*AI) Mkdir ¶
func (h *AI) Mkdir(w http.ResponseWriter, r *http.Request)
Mkdir → POST /api/ai/mkdir {"path":"…"}.
func (*AI) Move ¶
func (h *AI) Move(w http.ResponseWriter, r *http.Request)
Move → POST /api/ai/move {"src":"…","dst":"…"}.
func (*AI) NotificationRead ¶ added in v0.50.0
func (h *AI) NotificationRead(w http.ResponseWriter, r *http.Request)
NotificationRead → POST /api/ai/notifications/read {id} | {all: true}.
func (*AI) NotificationsList ¶ added in v0.50.0
func (h *AI) NotificationsList(w http.ResponseWriter, r *http.Request)
NotificationsList → GET /api/ai/notifications?unread=&limit=&offset=
func (*AI) OpCancel ¶ added in v0.50.0
func (h *AI) OpCancel(w http.ResponseWriter, r *http.Request)
OpCancel → POST /api/ai/ops/{id}/cancel
func (*AI) OpGet ¶ added in v0.50.0
func (h *AI) OpGet(w http.ResponseWriter, r *http.Request)
OpGet → GET /api/ai/ops/{id}
func (*AI) OpsList ¶ added in v0.50.0
func (h *AI) OpsList(w http.ResponseWriter, r *http.Request)
OpsList → GET /api/ai/ops?status=
func (*AI) PermissionRevoke ¶ added in v0.50.0
func (h *AI) PermissionRevoke(w http.ResponseWriter, r *http.Request)
PermissionRevoke → POST /api/ai/permissions/{id}/revoke {group?}.
func (*AI) PermissionSet ¶ added in v0.50.0
func (h *AI) PermissionSet(w http.ResponseWriter, r *http.Request)
PermissionSet → POST /api/ai/permissions {path, user_id | group_id, level}.
func (*AI) PermissionUsers ¶ added in v0.50.0
func (h *AI) PermissionUsers(w http.ResponseWriter, r *http.Request)
PermissionUsers → GET /api/ai/permissions/users?q=
func (*AI) PermissionsList ¶ added in v0.50.0
func (h *AI) PermissionsList(w http.ResponseWriter, r *http.Request)
PermissionsList → GET /api/ai/permissions?path=
func (*AI) Root ¶
func (h *AI) Root(w http.ResponseWriter, r *http.Request)
Root → GET /api/ai/root. Reports the caller's confinement root + reachable storages so a confined agent knows how to address paths instead of guessing.
func (*AI) Search ¶
func (h *AI) Search(w http.ResponseWriter, r *http.Request)
Search → GET /api/ai/search?path=<adapter://>&q=…
Goes through aiNameSearch rather than aiOps.Search so this speaks the same query language as the MCP file_search tool and the web endpoints: separator-blind text and `tag:` filters. The response shape is unchanged — entries, no snippets; content search stays on the MCP tool.
func (*AI) Share ¶
func (h *AI) Share(w http.ResponseWriter, r *http.Request)
Share → POST /api/ai/share. Mints a public /s/<token> link for a file/folder (folders download as a ZIP). Returns the URL + a one-time PIN if requested.
func (*AI) SharesList ¶ added in v0.50.0
func (h *AI) SharesList(w http.ResponseWriter, r *http.Request)
SharesList → GET /api/ai/shares?active=&limit=&offset=
func (*AI) Star ¶ added in v0.50.0
func (h *AI) Star(w http.ResponseWriter, r *http.Request)
Star → POST /api/ai/star {path, star?} (star defaults to true).
func (*AI) TagsGet ¶
func (h *AI) TagsGet(w http.ResponseWriter, r *http.Request)
TagsGet → GET /api/ai/tags?path=
func (*AI) TagsSet ¶
func (h *AI) TagsSet(w http.ResponseWriter, r *http.Request)
TagsSet → POST /api/ai/tags {path, tags:[{name, kind}]}. `tags` is the whole set the user can see afterwards; [] clears it.
func (*AI) TrashList ¶ added in v0.50.0
func (h *AI) TrashList(w http.ResponseWriter, r *http.Request)
TrashList → GET /api/ai/trash?storage=&limit=&offset=
func (*AI) TrashRestore ¶ added in v0.50.0
func (h *AI) TrashRestore(w http.ResponseWriter, r *http.Request)
TrashRestore → POST /api/ai/trash/restore {node_ids}.
func (*AI) Unshare ¶
func (h *AI) Unshare(w http.ResponseWriter, r *http.Request)
Unshare → POST /api/ai/unshare {"token":"…"}. Revokes a share by token.
func (*AI) Unzip ¶
func (h *AI) Unzip(w http.ResponseWriter, r *http.Request)
Unzip → POST /api/ai/unzip {"src":"…","dest":"…"}. Extracts a stored zip into the dest dir ON THE SERVER (zip-slip protected, confined to the token root).
func (*AI) Upload ¶
func (h *AI) Upload(w http.ResponseWriter, r *http.Request)
Upload → POST /api/ai/upload. Accepts JSON (base64/text) or multipart.
A multipart body is STREAMED: the part is handed to WriteStream as a reader, so a large capture never has to exist in memory, and above the chunk threshold it goes through filex's staging area like every other client.
func (*AI) UploadTicket ¶
func (h *AI) UploadTicket(w http.ResponseWriter, r *http.Request)
UploadTicket → POST /api/ai/upload/ticket (scope: write).
func (*AI) VersionRestore ¶ added in v0.50.0
func (h *AI) VersionRestore(w http.ResponseWriter, r *http.Request)
VersionRestore → POST /api/ai/versions/restore {path, version_id, snapshot_current?}.
func (*AI) VersionSnapshot ¶ added in v0.50.0
func (h *AI) VersionSnapshot(w http.ResponseWriter, r *http.Request)
VersionSnapshot → POST /api/ai/versions/snapshot {path}.
func (*AI) VersionsList ¶ added in v0.50.0
func (h *AI) VersionsList(w http.ResponseWriter, r *http.Request)
VersionsList → GET /api/ai/versions?path=
type AIAdmin ¶
type AIAdmin struct {
// contains filtered or unexported fields
}
AIAdmin exposes the full admin panel over the token-authenticated AI surface — both as REST endpoints under /api/ai/admin/* and as admin_* MCP tools — so a single bearer token (scope `admin`) lets an AI agent drive users, storages, settings, replica, queue, notifications, audit … exactly like the native /admin SPA does.
Design: AIAdmin owns one instance of every existing admin handler (Dashboard, Users, Storages, …). The REST routes mount those handler methods directly behind an admin-elevating middleware (chi fills the URL params, the body/query pass straight through). The MCP tools invoke the very same handler methods IN-PROCESS via `invoke` — a synthetic request + buffered recorder, no network round-trip, no logic duplicated.
Authorization: the admin surface is gated by RequireScope("admin") at the route / getServer level. Once past that gate the bound user is elevated to an admin principal (elevatedPrincipal) so the underlying handler logic runs with an admin-authorized context — we deliberately call the handler logic directly rather than the RequireAdmin-wrapped HTTP route.
func NewAIAdmin ¶
func NewAIAdmin(d AIAdminDeps) *AIAdmin
NewAIAdmin constructs the admin AI surface from shared deps. Each wrapped handler is the same type the native /admin routes use, so behaviour is identical — only the auth front-door differs.
type AIAdminDeps ¶
type AIAdminDeps struct {
Store db.Store
Caps *capability.Service
Worker *syncpkg.Worker
Queue queue.Driver
Notify notify.Service
Trash *trash.Service
Ops *ops.Service
Index *search.Index
ReplicaService *replica.Service
ReplicaCron *replica.CronScheduler
ReplicaReloader *replica.RulesReloader
// External + EnvManagedExternal mirror what the native /admin routes get,
// so the MCP admin surface reports and applies external-service changes the
// same way the UI does.
External *external.Resolver
EnvManagedExternal map[string]bool
// DemoMode marks a public playground, so the wrapped handlers redact the
// same things they redact on the native /admin routes. The token surface
// is not a way around a demo's rules.
DemoMode bool
// PublicURL + PublicURLSet feed the external-service advisories: the MCP
// admin surface must report the same three-address picture the UI does,
// or an agent reading it back gets the narrower answer the UI stopped
// giving (issue #17).
PublicURL string
PublicURLSet bool
// ReversePath is the panel's third-leg check (ExternalAdmin.ReversePath),
// so admin_external_test measures the document server's route back to
// filex and its JWT the way the page's Test does. Nil = not measured.
ReversePath func(ctx context.Context, t *onlyoffice.Target) onlyoffice.ReverseResult
// AuthLive is the running set of sign-in providers, so the admin MCP
// tools change sign-in through the same handler, with the same guards, as
// the page (handlers/auth_providers.go).
AuthLive *authsetup.Live
// Plugins / AppPlugins / PluginRequests back the plugin tools: reading
// the installed plugins and leaving install requests. Nil = that kind is
// off (the handlers answer 503, as on the panel's routes).
Plugins *plugin.Manager
AppPlugins *wasmplugin.Registry
AppPluginsDisabledReason string
PluginRequests *pluginreq.Service
// Assoc backs the Default apps tool (read only): which app opens and
// draws which kind of file. Nil = app plugins are off.
Assoc *assoc.Service
// LoginGuard + EnvTrustedProxies back the sign-in security tools: the same
// handler the panel uses, over the same running limiter.
LoginGuard *loginguard.Guard
EnvTrustedProxies string
// MultiTenant mirrors FILEX_MULTI_TENANT for the tenant tools: the list
// says whether tenants can sign in at all (maintenance mode when off).
MultiTenant bool
// ArchiveEngine backs the archive settings tools: the same engine the
// panel's Archives page reads and probes. Nil = those tools answer 503.
ArchiveEngine *archivecli.Service
}
AIAdminDeps carries the shared services the wrapped admin handlers need. Mirrors the subset of api.Deps relevant to the admin surface (declared here to avoid an api→handlers→api import cycle).
type AIDoors ¶ added in v0.50.0
type AIDoors struct {
Ops *Ops
Apps *AppPlugins
Trash *Trash
Versions *Versions
Archive *Archive
// The bell, stars, comments and item permissions (ai_doors_people.go).
Notifications *Notifications
Meta *Meta
Comments *Comments
Grants *Grants
// ACL answers the per-route permission the explorer's route asks before
// its handler (plugins.run on an app's action).
ACL *acl.Resolver
}
AIDoors are the explorer's own handlers, exactly as routes.go builds them for /api/files and /api/shares. The AI surface runs them in process. A nil field leaves its tools answering 503.
type AIMCP ¶
type AIMCP struct {
// contains filtered or unexported fields
}
AIMCP exposes filex as a Model Context Protocol server over streamable HTTP (JSON-RPC). The same aiOps core that backs the REST handler powers each MCP tool, so AI agents can drive filex directly while work.example.com's FilexClient uses the REST surface.
Transport: streamable HTTP in stateless + JSON-response mode (one JSON-RPC request → one JSON response), which is what laravel/mcp's HTTP client speaks. Mounted at POST/GET /api/ai/mcp behind auth.APITokenMiddleware + RequireScope("mcp").
Auth model: the route's middleware has already validated the AI token and stashed the principal on the request context by the time getServer runs. getServer reads that principal and builds a per-request server whose tools close over an aiOps bound to the store + resolver. If the principal is absent (should never happen behind the middleware) getServer returns nil and the SDK serves 400.
func NewAIMCP ¶
func NewAIMCP(store db.Store, resolver func(int64) (storage.Driver, error), admin *AIAdmin, shareSvc *share.Service, publicURL string) *AIMCP
NewAIMCP builds the MCP HTTP handler. `admin` powers the admin_* tools, which are only registered for tokens carrying the `admin` scope; pass nil to disable the admin tool surface entirely. shareSvc + publicURL power the file_share / file_unshare tools.
func (*AIMCP) AttachACL ¶
AttachACL wires the RBAC resolver so every per-request MCP tool op is gated by the bound user's grants + role ceiling (same enforcement as the REST AI).
func (*AIMCP) AttachBody ¶
AttachBody wires the byte-source resolver so MCP reads serve a file that is still being transferred out of staging.
func (*AIMCP) AttachDoors ¶ added in v0.50.0
AttachDoors wires the explorer's handlers the door tools run in process.
func (*AIMCP) AttachE2EPolicy ¶ added in v0.51.0
AttachE2EPolicy wires the rule into every ops core the MCP surface builds per call (getServer).
func (*AIMCP) AttachSearchIndex ¶
AttachSearchIndex wires the search index; every aiOps this handler builds per tool call gets it.
func (*AIMCP) AttachStaged ¶
func (h *AIMCP) AttachStaged(s *StagedUpload)
AttachStaged routes MCP tool writes above the chunk threshold through the staging area, so an agent gets the same acknowledge-then-transfer behaviour as the browser and the CLI (nil = synchronous writes).
func (*AIMCP) AttachTenants ¶
AttachTenants wires the shared origin resolver (internal/tenanturl). The per-call ops core below is rebuilt for every request, so the resolver is held here and stamped onto each one.
func (*AIMCP) AttachThumbs ¶
AttachThumbs wires the thumbnail pipeline so MCP tool writes dispatch generation like manager uploads (nil = thumbnails skipped).
func (*AIMCP) AttachTickets ¶
func (h *AIMCP) AttachTickets(s *uploadTicketStore)
AttachTickets wires the shared upload-ticket store so the MCP surface can mint credential-free upload URLs for large local files.
func (*AIMCP) ServeHTTP ¶
func (h *AIMCP) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP delegates to the SDK's streamable handler. The client's address is resolved here, once, and carried on the context: a tool call has no *http.Request of its own, and the audit rows its writes leave name the address like every other door's (mcpAuditWrite, AIAdmin.auditInvoke).
type AITokens ¶
type AITokens struct {
// contains filtered or unexported fields
}
AITokens is the admin handler for issuing / listing / revoking API tokens used by AI agents, the MCP server, and the work.example.com FilexClient.
Routes (admin-only):
GET /api/admin/ai-tokens → {tokens:[…]} (no secret)
POST /api/admin/ai-tokens → {token:"<plain>", row:{…}} (plaintext ONCE)
DELETE /api/admin/ai-tokens/{id} → {ok:true}
func NewAITokens ¶
func NewAITokens(store db.Store, pauth *protocolauth.Resolver) *AITokens
NewAITokens constructs the admin token handler.
func (*AITokens) Create ¶
func (h *AITokens) Create(w http.ResponseWriter, r *http.Request)
Create issues a new token. The plaintext value is returned ONCE — only its sha256 hash is persisted.
func (*AITokens) Delete ¶
func (h *AITokens) Delete(w http.ResponseWriter, r *http.Request)
Delete revokes a token by id.
type ActivityRow ¶
type ActivityRow struct {
*model.AuditEntry
UserEmail string `json:"user_email,omitempty"`
// TargetName: which thing, in words (handlers/audit_targets.go).
TargetName string `json:"target_name,omitempty"`
// UserName is who acted, as every screen names a person (PersonLabel).
UserName string `json:"user_name,omitempty"`
}
ActivityRow is one Recent activity line: the audit row, flat, plus the e-mail of the account that acted.
⚠ The page prints the actor under every action, and the payload never had one — the audit LIST joins users.email (db.AuditEntryWithUser), the dashboard read the bare rows — so each line said "— · Profile".
type AppPlugins ¶
type AppPlugins struct {
Registry *wasmplugin.Registry
Store db.Store
ACL *acl.Resolver
Ops *ops.Service
StorageResolver func(int64) (storage.Driver, error)
Index *search.Index
Thumbs *thumb.Pipeline
// Quota is the per-person ceiling an interface's save is held to, like
// every other write (security review UI-7). Nil = no ceiling.
Quota *quota.Service
// E2EPolicy is who may encrypt (e2e_policy_gate.go): CommitSibling — an
// interface's "save as" and a job's output — asks it before it creates a
// key file or a `.fxe`. nil = not wired.
E2EPolicy *e2epolicy.Service
// Assoc keeps which handler opens which kind of file (internal/assoc):
// the listing carries its rules, and an interface switched off for a
// kind is refused on it (openAllowed). Nil: every viewer opens its kinds.
Assoc *assoc.Service
}
AppPlugins is the user-facing handler set + the OutputSink the registry commits job outputs through.
func NewAppPlugins ¶
func NewAppPlugins(reg *wasmplugin.Registry, store db.Store, aclr *acl.Resolver, opsSvc *ops.Service, resolver func(int64) (storage.Driver, error), index *search.Index, thumbs *thumb.Pipeline) *AppPlugins
NewAppPlugins constructs the handler and wires it as the registry's sink.
func (*AppPlugins) Actions ¶
func (h *AppPlugins) Actions(w http.ResponseWriter, r *http.Request)
Actions answers the menu list for this caller.
func (*AppPlugins) Catalogue ¶
func (h *AppPlugins) Catalogue(ctx context.Context, storageID int64, rel string) (*model.Node, error)
Catalogue records a file the storage has and the catalogue has not seen yet (wasmplugin.Cataloguer): share_create's answer when a job's own input has no node for the link to point at.
func (*AppPlugins) CommitSibling ¶
func (h *AppPlugins) CommitSibling(ctx context.Context, storageID int64, dir, name string, r io.Reader, size int64, actor *int64) (string, error)
CommitSibling writes a new file into dir, picking a free name, and runs the shared post-write bookkeeping.
func (*AppPlugins) CommitVersion ¶
func (h *AppPlugins) CommitVersion(ctx context.Context, storageID int64, rel string, r io.Reader, size int64, actor *int64) error
CommitVersion overwrites rel in place; the previous bytes become a version when versioning is on (writehook.BeforeOverwrite).
func (*AppPlugins) Run ¶
func (h *AppPlugins) Run(w http.ResponseWriter, r *http.Request)
Run submits an action. An action that declares a view answers the view's opening surface instead of queueing; the surface's submit comes back through ViewEvent, which queues.
func (*AppPlugins) UICall ¶ added in v0.48.0
func (h *AppPlugins) UICall(w http.ResponseWriter, r *http.Request)
UICall is the app's own interface asking its module (`ui_call`). The files the interface was opened with are judged for the person asking exactly like a view's — confinement, ACL viewer, not in an encrypted folder — and handed to the module as refs.
func (*AppPlugins) UISave ¶ added in v0.48.0
func (h *AppPlugins) UISave(w http.ResponseWriter, r *http.Request)
UISave writes what an app's interface saved.
func (*AppPlugins) Users ¶
func (h *AppPlugins) Users(w http.ResponseWriter, r *http.Request)
Users is the people-picker's directory search: GET /api/files/plugins/users?plugin=<name>&q=. It answers only for a running plugin that holds users:lookup (403 otherwise), through the tenant-scoped store — a tenant's picker never lists another tenant's people.
func (*AppPlugins) View ¶
func (h *AppPlugins) View(w http.ResponseWriter, r *http.Request)
View answers a view's opening surface (event "open").
func (*AppPlugins) ViewEvent ¶
func (h *AppPlugins) ViewEvent(w http.ResponseWriter, r *http.Request)
ViewEvent answers a change/submit/action event. A surface carrying a job request is queued with the same checks as Run.
type AppPluginsAdmin ¶
type AppPluginsAdmin struct {
Registry *wasmplugin.Registry
// DisabledReason is shown when Registry is nil (config flag vs. arch).
DisabledReason string
// Audit records an administrator's forced unlock; nil = not recorded.
Audit func(ctx context.Context, userID *int64, action string, storageID int64, rel, ip string) error
// Assoc keeps which app opens and draws which kind (internal/assoc): the
// install review's File types group, the choices made there, and the
// rules an app's removal takes it out of. Nil: none of that.
Assoc *assoc.Service
// OnRemoved is told an app was removed (internal/appstore forgets its
// license and lifts its hold). Nil: nobody is.
OnRemoved func(ctx context.Context, app string)
}
AppPluginsAdmin is the handler set. Registry may be nil when the runtime is disabled by configuration; every route then answers 503.
func NewAppPluginsAdmin ¶
func NewAppPluginsAdmin(reg *wasmplugin.Registry, disabledReason string) *AppPluginsAdmin
NewAppPluginsAdmin constructs the handler.
func (*AppPluginsAdmin) CheckUpdates ¶ added in v0.47.0
func (h *AppPluginsAdmin) CheckUpdates(w http.ResponseWriter, r *http.Request)
CheckUpdates asks every app's source for a newer version now, applies what may be applied, and answers what happened with the list redrawn: POST /api/admin/app-plugins/updates/check. A check already running (the daily one) is waited for rather than doubled.
⚠ Detached from the request: a check installs apps, and an administrator who closes the tab must not cut one in half (Upgrade holds its own swap either way).
func (*AppPluginsAdmin) Delete ¶
func (h *AppPluginsAdmin) Delete(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) Get ¶
func (h *AppPluginsAdmin) Get(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) GetOverrides ¶
func (h *AppPluginsAdmin) GetOverrides(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) GetSettings ¶
func (h *AppPluginsAdmin) GetSettings(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) ImportSigningCA ¶
func (h *AppPluginsAdmin) ImportSigningCA(w http.ResponseWriter, r *http.Request)
ImportSigningCA takes over signing with an authority the operator already has: POST /api/admin/app-plugins/signing/ca/import, multipart with `cert` and `key` (PEM), or JSON {cert_pem, key_pem}.
The current authority is retired, never deleted. An encrypted key is refused with the command that decrypts it rather than a shrug: the encrypted container formats in the wild are more varied than any one library reads, and half-working import is worse than a clear instruction.
func (*AppPluginsAdmin) Install ¶
func (h *AppPluginsAdmin) Install(w http.ResponseWriter, r *http.Request)
Install installs an app — or, with ?dry_run=1, answers its review and installs nothing. The review is open to an admin-scoped API key (it is how an agent sees what it would be asking for); the install itself needs a signed-in administrator (requireSession).
func (*AppPluginsAdmin) List ¶
func (h *AppPluginsAdmin) List(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) Locks ¶
func (h *AppPluginsAdmin) Locks(w http.ResponseWriter, r *http.Request)
Locks lists the live app-plugin file locks: GET /api/admin/app-plugins/locks[?storage_id=].
func (*AppPluginsAdmin) Logs ¶
func (h *AppPluginsAdmin) Logs(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) Patch ¶
func (h *AppPluginsAdmin) Patch(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) PutOverrides ¶
func (h *AppPluginsAdmin) PutOverrides(w http.ResponseWriter, r *http.Request)
PutOverrides changes who may use an app's actions and where (admin_only, enabled, applies): a permission change, so a signed-in administrator's.
func (*AppPluginsAdmin) PutSettings ¶
func (h *AppPluginsAdmin) PutSettings(w http.ResponseWriter, r *http.Request)
func (*AppPluginsAdmin) PutThumbLimits ¶ added in v0.50.0
func (h *AppPluginsAdmin) PutThumbLimits(w http.ResponseWriter, r *http.Request)
PutThumbLimits answers PUT /api/admin/app-plugins/{id}/thumbnails: a signed-in administrator's, like every change to what an app is handed.
func (*AppPluginsAdmin) Rollback ¶ added in v0.48.0
func (h *AppPluginsAdmin) Rollback(w http.ResponseWriter, r *http.Request)
Rollback puts the version an upgrade replaced back: POST /api/admin/app-plugins/{id}/rollback. No new approval — that version's grant was approved when it was installed (wasmplugin/versions.go).
func (*AppPluginsAdmin) RotateSigningCA ¶
func (h *AppPluginsAdmin) RotateSigningCA(w http.ResponseWriter, r *http.Request)
RotateSigningCA retires the current CA; the next signature mints a new one. Old signatures stay verifiable with the old certificate.
func (*AppPluginsAdmin) SigningCA ¶
func (h *AppPluginsAdmin) SigningCA(w http.ResponseWriter, r *http.Request)
SigningCA answers the tenant CA certificate (PEM) that readers import to trust the signatures app plugins make here. The CA is created on first use. Supertenant only (the admin surface), per the caller's tenant.
func (*AppPluginsAdmin) SigningCAs ¶
func (h *AppPluginsAdmin) SigningCAs(w http.ResponseWriter, r *http.Request)
SigningCAs lists the tenant's signing authorities, live and retired: GET /api/admin/app-plugins/signing/cas.
Retired ones are listed on purpose. They are what a signature made before the last rotation verifies against, so an operator has to be able to see that they are still there — and that nothing is quietly deleting them.
func (*AppPluginsAdmin) ThumbLimits ¶ added in v0.50.0
func (h *AppPluginsAdmin) ThumbLimits(w http.ResponseWriter, r *http.Request)
ThumbLimits answers GET /api/admin/app-plugins/{id}/thumbnails: the app's thumbnail limits in force, as stored, the defaults, the bounds, the kinds.
func (*AppPluginsAdmin) Unlock ¶
func (h *AppPluginsAdmin) Unlock(w http.ResponseWriter, r *http.Request)
Unlock lifts one lock by force: DELETE /api/admin/app-plugins/locks with {storage_id, path}. The audit row names who did it.
func (*AppPluginsAdmin) Upgrade ¶
func (h *AppPluginsAdmin) Upgrade(w http.ResponseWriter, r *http.Request)
type AppStore ¶ added in v0.52.0
type AppStore struct {
Svc *appstore.Service
Admin *AppPluginsAdmin
// Demo: a public demo trusts no store (its admin account is public).
Demo bool
// PublicURL is this filex's configured address (FILEX_PUBLIC_URL, when
// set): a link's filex_origin must be its origin. Empty: the origin the
// request arrived at.
PublicURL string
}
AppStore is the handler set. Svc nil answers 503 (the runtime is off).
func NewAppStore ¶ added in v0.52.0
func NewAppStore(svc *appstore.Service, admin *AppPluginsAdmin, demo bool) *AppStore
NewAppStore builds the handler set over the Apps admin handlers it shares the review and the install with.
func (*AppStore) AppLicense ¶ added in v0.52.0
func (h *AppStore) AppLicense(w http.ResponseWriter, r *http.Request)
AppLicense answers what an app reads about its own license: the status, and for a valid one its dates - never the key, the store or the holder (every user who may run apps, and any token with that right, reads this). A free app answers {"status": "free"}.
func (*AppStore) Cancel ¶ added in v0.52.0
func (h *AppStore) Cancel(w http.ResponseWriter, r *http.Request)
Cancel ends a reviewed link without installing: the store is told `cancelled`, and the link is used up here.
func (*AppStore) Install ¶ added in v0.52.0
func (h *AppStore) Install(w http.ResponseWriter, r *http.Request)
Install installs (or upgrades) what a reviewed link names: the repository is read AGAIN and held to the link's pins again - what the administrator approved is what lands, not what the source serves by now - then the same install a GitHub install is. A paid app is held from before it is installed until the store confirms its license.
func (*AppStore) Intent ¶ added in v0.52.0
func (h *AppStore) Intent(w http.ResponseWriter, r *http.Request)
Intent reads an install link and answers its review: the store must be trusted (otherwise the answer is the trust question - the store and its key fingerprints - and nothing was fetched from the link), the link signed, current and unused, and what the repository serves must be what the store pinned. Then the same dry run a GitHub install's review is.
func (*AppStore) License ¶ added in v0.52.0
func (h *AppStore) License(w http.ResponseWriter, r *http.Request)
func (*AppStore) Licenses ¶ added in v0.52.0
func (h *AppStore) Licenses(w http.ResponseWriter, r *http.Request)
func (*AppStore) ListStores ¶ added in v0.52.0
func (h *AppStore) ListStores(w http.ResponseWriter, r *http.Request)
func (*AppStore) MountAdmin ¶ added in v0.52.0
MountAdmin registers the admin routes inside /api/admin/app-plugins. The static paths are registered before the caller's /{id} routes match them.
func (*AppStore) PutLicense ¶ added in v0.52.0
func (h *AppStore) PutLicense(w http.ResponseWriter, r *http.Request)
func (*AppStore) TrustStore ¶ added in v0.52.0
func (h *AppStore) TrustStore(w http.ResponseWriter, r *http.Request)
TrustStore trusts a store with the keys it publishes, provided they are the ones the administrator was shown (`fingerprints`, as the trust question listed them).
func (*AppStore) UntrustStore ¶ added in v0.52.0
func (h *AppStore) UntrustStore(w http.ResponseWriter, r *http.Request)
func (*AppStore) VerifyLicense ¶ added in v0.52.0
func (h *AppStore) VerifyLicense(w http.ResponseWriter, r *http.Request)
type Appearance ¶
type Appearance struct{ Source *AppearanceSource }
Appearance serves the public themes endpoint.
func NewAppearance ¶
func NewAppearance(src *AppearanceSource) *Appearance
NewAppearance constructs the public handler.
func (*Appearance) Get ¶
func (h *Appearance) Get(w http.ResponseWriter, r *http.Request)
Get returns the instance's themes and default. Public (no auth): the login page and every share page need it before a session exists.
⚠ It carries NO raw CSS. That used to ride /api/branding, which is how the login page ended up wearing an operator stylesheet it must not wear — see custom_css.go.
type AppearancePayload ¶
type AppearancePayload struct {
Themes []ThemeWire `json:"themes"`
DefaultThemeID string `json:"default_theme_id"`
}
AppearancePayload is GET /api/appearance.
type AppearanceSource ¶
AppearanceSource resolves and caches the instance's themes.
ONE cache entry, not one per host: unlike branding there is no tenant overlay, because the table has no tenant column and the admin routes are supertenant-only. A nil receiver is safe and yields the stock answer.
func NewAppearanceSource ¶
func NewAppearanceSource(store db.Store) *AppearanceSource
NewAppearanceSource constructs the shared source.
func (*AppearanceSource) DefaultTokens ¶
DefaultTokens returns the token map of the instance default theme for the requested variant, or nil when the default is a built-in palette.
⚠ This is what the PUBLIC pages use, and it reads the instance default and nothing else. A share page, a PIN gate and a signing page are looked at by people with no account here; resolving anybody's personal palette on them would be both impossible and a disclosure — the visitor would learn which palette the sender had picked.
func (*AppearanceSource) Invalidate ¶
func (a *AppearanceSource) Invalidate()
Invalidate drops the cache — called after any theme or default write.
func (*AppearanceSource) Payload ¶
func (a *AppearanceSource) Payload(ctx context.Context) AppearancePayload
Payload returns the effective appearance for this instance.
type Archive ¶
type Archive struct {
Store db.Store
StorageResolver func(int64) (storage.Driver, error)
ACL *acl.Resolver
// Body resolves where a member's bytes are: the driver, or filex's
// staging area while a staged upload is still transferring. Nil-safe.
Body *filebody.Resolver
// Index and Thumbs feed the shared catalogue bookkeeper below. Both
// optional.
Index *search.Index
Thumbs *thumb.Pipeline
// Tickets holds the minted "download this selection as one archive"
// authorizations (archive_download.go). Nil disables that endpoint pair
// rather than crashing it.
Tickets *archiveTicketStore
// Engine supplies optional 7-Zip/RAR processing and the live archive
// policy. Nil keeps the historical built-in ZIP-only behaviour.
Engine *archivecli.Service
// Ops moves expensive archive creation out of the request. Nil retains the
// synchronous path for lightweight embedders and focused handler tests.
Ops *ops.Service
// Files streams the one file a `"mode":"file"` link names (the browser's
// drag-out link, download_link.go). Nil answers that mode with 503.
Files FileLinkStreamer
// MultiTenant decides whether a link's redeem is held to the tenant host
// it was minted on and to its owner's tenant scope.
MultiTenant bool
// E2EPolicy is who may encrypt (e2e_policy_gate.go): extraction skips a
// member that would create a key file or a `.fxe`, and add and create
// refuse a new archive under such a name. nil = not wired.
E2EPolicy *e2epolicy.Service
}
Archive handles zip-listing, zip-extract, and zip-create operations.
All zip ops materialize the source archive to a tmp file (since archive/zip needs an io.ReaderAt + Seeker), then stream extracts back to storage.
func NewArchive ¶
NewArchive constructs an Archive handler.
func (*Archive) Add ¶
func (a *Archive) Add(w http.ResponseWriter, r *http.Request)
Add packs members into a (new or existing) zip archive on the same storage.
If the destination zip exists, we download it, append the new entries, then re-upload. Names are zip-slip protected on the read side.
func (*Archive) AttachACL ¶
AttachACL wires the RBAC resolver: list needs ≥viewer on the archive, extract/add need ≥editor on the write target (+ ≥viewer on sources read).
func (*Archive) AttachArchiveEngine ¶ added in v0.44.2
func (a *Archive) AttachArchiveEngine(engine *archivecli.Service)
AttachArchiveEngine wires optional external archive providers.
func (*Archive) AttachBody ¶
AttachBody wires the byte-source resolver so a file that is still being transferred can be zipped/extracted like any other.
func (*Archive) AttachDownloadTickets ¶
func (a *Archive) AttachDownloadTickets(s *archiveTicketStore)
AttachDownloadTickets wires the ticket store. Without it the mint endpoint answers 503 rather than panicking — a handler assembled without its store is a wiring bug, and a nil map write is a worse way to find out.
func (*Archive) AttachFileLinks ¶ added in v0.45.0
func (a *Archive) AttachFileLinks(s FileLinkStreamer, multiTenant bool)
AttachFileLinks wires one-file links. Without it `"mode":"file"` answers 503.
func (*Archive) AttachOps ¶ added in v0.44.2
AttachOps wires the shared long-running file-operation queue.
func (*Archive) AttachSearchIndex ¶
AttachSearchIndex / AttachThumbs wire the two optional halves of the bookkeeper.
func (*Archive) AttachThumbs ¶
AttachThumbs wires thumbnail generation for extracted members.
func (*Archive) Create ¶ added in v0.44.2
func (a *Archive) Create(w http.ResponseWriter, r *http.Request)
Create writes a new archive from files and folders already in filex. It refuses an occupied destination rather than silently replacing user data; the historical /archive/add endpoint retains ZIP update semantics, which solid 7z and RAR archives cannot share safely.
func (*Archive) DownloadArchive ¶
func (a *Archive) DownloadArchive(w http.ResponseWriter, r *http.Request)
DownloadArchive streams a minted archive. Public route (`GET /z/{ticket}`) — the ticket is the authority, see the package comment.
func (*Archive) DownloadTicket ¶
func (a *Archive) DownloadTicket(w http.ResponseWriter, r *http.Request)
DownloadTicket resolves a selection into an archive and returns the URL that streams it.
Refusals, and why each is its own answer rather than a shared 400:
403 one of the named paths is not readable by this caller 404 the storage is not this tenant's 409 the selection resolved to no readable file at all 413 more members than archiveMaxMembers
The 409 matters: a selection of three empty folders is not an error the user made, but an empty ZIP arriving as a "successful" download is the kind of thing people file bugs about six months later. Say so at the click.
type ArchiveAdmin ¶ added in v0.44.2
type ArchiveAdmin struct {
Store db.Store
Service *archivecli.Service
}
ArchiveAdmin exposes validated archive policy and read-only provider status. Executable paths are deliberately absent: they are process configuration, not data an HTTP administrator may turn into a command.
func NewArchiveAdmin ¶ added in v0.44.2
func NewArchiveAdmin(store db.Store, service *archivecli.Service) *ArchiveAdmin
func (*ArchiveAdmin) Get ¶ added in v0.44.2
func (h *ArchiveAdmin) Get(w http.ResponseWriter, r *http.Request)
func (*ArchiveAdmin) Patch ¶ added in v0.44.2
func (h *ArchiveAdmin) Patch(w http.ResponseWriter, r *http.Request)
func (*ArchiveAdmin) Test ¶ added in v0.44.2
func (h *ArchiveAdmin) Test(w http.ResponseWriter, r *http.Request)
Test performs an actual encrypted create/list/extract round trip. A version command alone proves that a file exists, not that its codecs and crypto work.
type Audit ¶
type Audit struct {
Store db.Store
// DemoMode marks a public playground, where this page is readable by
// whoever read the credentials off the landing page - and the addresses
// (and the names typed at the sign-in form) in it belong to the other
// visitors. See demoMask.
DemoMode bool
}
Audit handles /api/admin/audit.
type Auth ¶
type Auth struct {
Store db.Store
LocalAuth auth.LoginDriver
OIDCAuth auth.OIDCDriver
PublicURL string
MultiTenant bool
// CookieDomain (FILEX_COOKIE_DOMAIN) is the GLOBAL session-cookie Domain
// attribute. In multi-tenant mode it is only the last-resort fallback —
// see cookieDomain() for the per-provider resolution. Applied on clear
// too — a logout without the matching Domain leaves the old cookie
// behind.
CookieDomain string
// Tenants resolves the per-request origin for absolute URLs. See
// internal/tenanturl — it is the one implementation of this rule, and
// redirectBase below is now just its caller.
Tenants tenanturl.Resolver
// OIDCLocalLogout (FILEX_OIDC_LOGOUT=local) keeps sign-out inside filex:
// the IdP's session is left open, as it was before RP-initiated logout.
OIDCLocalLogout bool
// Guard limits wrong sign-in attempts per account identifier and per
// address (internal/loginguard). Nil = no limit, which is what a handler
// built without one (a test, an embedder) had before.
Guard *loginguard.Guard
// EmailToken is the installation's e-mail token (FILEX_OS_LOGIN_EMAIL_TOKEN,
// identity.EmailToken): a realm's derived addresses end in it
// (`alex@acme.local`). "" = the default.
EmailToken string
// Handoffs holds the one-use tickets that carry a sign-in from the
// platform's address to a tenant's own (see handOff; auth.HandoffStore).
// Nil = no handoff: the session is opened where the person signed in, as
// before.
Handoffs *auth.HandoffStore
// Live is the running set of sign-in providers: the sign-in page asks it
// which methods a realm has (Methods). Nil on a harness that has none;
// Methods then answers from LocalAuth / OIDCAuth alone.
Live *authsetup.Live
}
Auth handles login/logout/oidc routes.
func NewAuth ¶
func NewAuth(store db.Store, local auth.LoginDriver, oidc auth.OIDCDriver, publicURL string, multiTenant bool, cookieDomain string) *Auth
NewAuth constructs an Auth handler.
func (*Auth) Handoff ¶ added in v0.50.0
func (h *Auth) Handoff(w http.ResponseWriter, r *http.Request)
Handoff completes a sign-in handed over by handOff: POST /api/auth/handoff {"code"}, on the tenant's own address. Answers like Login — the session cookie, and {user, token} — or 401 for a ticket that is unknown, spent, expired, issued for another purpose, presented on another host (it is spent all the same), or whose account may no longer sign in here (auth.CheckHandoffTarget). Every refusal of a real ticket is audited.
func (*Auth) Login ¶
func (h *Auth) Login(w http.ResponseWriter, r *http.Request)
Login authenticates email + password and sets the session cookie.
When the resolved user has TOTP enabled, a valid second-factor code is mandatory: password success alone does NOT grant a session. The session is minted by LocalAuth.Login (which owns the password check), so on a missing/invalid TOTP code we revoke that just-created session before returning — no usable cookie is ever handed out and no orphan session lingers.
func (*Auth) Logout ¶
func (h *Auth) Logout(w http.ResponseWriter, r *http.Request)
Logout clears the cookie + revokes the server-side session — and, for an OIDC session, answers with the IdP's end-session URL as `logout_url` so the web app can end the IdP's session too (see idpLogoutURL).
Optional body: {"return_to": "/admin/login" | "/drive/login"} — the sign-in page of the front door the person was using, where the IdP sends the browser back once it is done.
func (*Auth) Methods ¶ added in v0.50.0
func (h *Auth) Methods(w http.ResponseWriter, r *http.Request)
Methods answers GET /api/auth/methods?realm=: how a sign-in for the tenant the address and the realm name may go (docs/TENANT-ADMIN.md):
{"password": bool, "recovery": bool, "sso": [{"id", "label"}]}
The sign-in page asks before it draws its buttons, and again when the Realm field changes.
⚠ No realm oracle. For a realm that names a tenant, `password` is whether the instance answers a password form at all (never whether THAT tenant has a directory), and a realm nobody has answers exactly like a tenant with no SSO. A realm with SSO shows its buttons: starting its sign-in would send the browser to its identity provider anyway. `recovery` is the platform's own tenant's only.
func (*Auth) OIDCCallback ¶
func (h *Auth) OIDCCallback(w http.ResponseWriter, r *http.Request)
OIDCCallback completes the OIDC flow.
func (*Auth) OIDCStart ¶
func (h *Auth) OIDCStart(w http.ResponseWriter, r *http.Request)
OIDCStart redirects to the IdP: `?instance=<slug>&realm=<realm>` start one OIDC of the tenant the address or the realm names (docs/TENANT-ADMIN.md).
A start the tenant has no such OIDC for, or a realm nobody has, goes back to the sign-in page (`?error=oidc`), like a failed callback: the start is a browser navigation, and a JSON body would dead-end the person. No reason code for those two: "this realm has no such SSO" against "no such realm" would tell a stranger which realms exist. Any other failure to start goes back the same way, with a code only when the person can act on it (too many sign-ins in flight: try again shortly); its words stay in the log.
type AuthProviders ¶
type AuthProviders struct {
Store db.Store
// Live is the running set of providers; nil on a harness that has none,
// where nothing can be changed.
Live *authsetup.Live
// DemoMode marks a public playground: nothing about sign-in changes there.
DemoMode bool
}
AuthProviders handles /api/admin/auth-providers.
func NewAuthProviders ¶
func NewAuthProviders(store db.Store, live *authsetup.Live) *AuthProviders
NewAuthProviders constructs the handler.
func (*AuthProviders) Create ¶ added in v0.50.0
func (h *AuthProviders) Create(w http.ResponseWriter, r *http.Request)
Create makes another instance of a managed driver (a second LDAP, an OIDC for some tenants only): POST /api/admin/auth-providers {driver, slug?, label?, enabled?, config?, tenants?, confirm_failed_test?, test_account?}. It is saved like Update saves (the real test first), and serves the platform's own tenant unless `tenants` names others (a multi-tenant install).
func (*AuthProviders) Delete ¶ added in v0.50.0
func (h *AuthProviders) Delete(w http.ResponseWriter, r *http.Request)
Delete removes an instance made with Create (DELETE /api/admin/auth-providers/{name}). A driver's first instance is switched off, not deleted (its settings rows are the environment-independent record of it); an environment provider is the environment's.
func (*AuthProviders) List ¶
func (h *AuthProviders) List(w http.ResponseWriter, r *http.Request)
List returns every sign-in provider and how it stands.
func (*AuthProviders) SetTenants ¶ added in v0.50.0
func (h *AuthProviders) SetTenants(w http.ResponseWriter, r *http.Request)
SetTenants says which tenants sign in through an instance (PUT /api/admin/auth-providers/{name}/tenants {tenants: [ids], confirm_tenant_lockout?}): bindings added and removed so the list is exactly this one. A multi-tenant install's; a tenant's own instance serves its tenant only, ever. Removing the last way in of a tenant is the lockout rule's.
func (*AuthProviders) SyncStart ¶ added in v0.52.0
func (h *AuthProviders) SyncStart(w http.ResponseWriter, r *http.Request)
SyncStart starts an LDAP instance's directory sync and answers 202 at once — a directory of thousands takes longer than a request should; the page polls SyncStatus. 404 when the provider cannot sync, 409 while a run is going.
POST /api/admin/auth-providers/{name}/sync
func (*AuthProviders) SyncStatus ¶ added in v0.52.0
func (h *AuthProviders) SyncStatus(w http.ResponseWriter, r *http.Request)
SyncStatus answers an LDAP instance's directory sync: whether it can run (a running LDAP provider), whether it is running now, its interval and the last run's report (docs/LDAP.md → Directory sync).
GET /api/admin/auth-providers/{name}/sync
func (*AuthProviders) Test ¶
func (h *AuthProviders) Test(w http.ResponseWriter, r *http.Request)
Test checks a provider's configuration for real, step by step.
⚠⚠ It used to answer `{"ok": true}` for anything (release-candidate sweep, 2026-09-21). A test button that always passes is worse than none.
A page provider is tested on the form as it stands (unsaved edits laid over what is stored; a secret left blank is the stored one, opened for the test and never returned). A provider the environment defines is tested on the environment's configuration — the form is read-only there, so a draft is ignored.
func (*AuthProviders) Update ¶
func (h *AuthProviders) Update(w http.ResponseWriter, r *http.Request)
Update saves one provider, applies it at once, and audits it.
type AuthSelf ¶
type AuthSelf struct {
Store db.Store
// ACL resolves the caller's per-user permissions for Me (package perm).
// Nil (tests) leaves them out of the answer.
ACL *acl.Resolver
}
AuthSelf wraps the self-service profile/password/TOTP routes.
func (*AuthSelf) ChangePassword ¶
func (h *AuthSelf) ChangePassword(w http.ResponseWriter, r *http.Request)
ChangePassword verifies the old password then writes a new bcrypt hash and revokes other sessions to force re-login.
func (*AuthSelf) Me ¶
func (h *AuthSelf) Me(w http.ResponseWriter, r *http.Request)
Me returns the authenticated user.
Wire shape mirrors LoginResponse: `{user: {…}}`. The frontend auth store reads `me.user`; without the wrapper user.value stayed undefined and TopNav fell back to "? —" forever.
func (*AuthSelf) TotpDisable ¶
func (h *AuthSelf) TotpDisable(w http.ResponseWriter, r *http.Request)
TotpDisable clears the user's TOTP secret if both password + code match.
func (*AuthSelf) TotpEnroll ¶
func (h *AuthSelf) TotpEnroll(w http.ResponseWriter, r *http.Request)
TotpEnroll generates a new pending secret + QR SVG + recovery codes.
The user must call /totp/verify with a valid code from their authenticator app to actually activate it; this endpoint does NOT enable TOTP yet.
func (*AuthSelf) TotpVerify ¶
func (h *AuthSelf) TotpVerify(w http.ResponseWriter, r *http.Request)
TotpVerify activates TOTP if the given code matches the pending secret.
func (*AuthSelf) UpdateProfile ¶
func (h *AuthSelf) UpdateProfile(w http.ResponseWriter, r *http.Request)
UpdateProfile patches the current user's profile fields.
type Branding ¶
type Branding struct {
Source *BrandingSource
}
Branding serves the public branding endpoint.
func NewBranding ¶
func NewBranding(src *BrandingSource) *Branding
NewBranding constructs the public branding handler.
type BrandingConfig ¶
type BrandingConfig struct {
Name string `json:"name"`
LogoURL string `json:"logo_url"`
Accent string `json:"accent"`
HidePoweredBy bool `json:"hide_powered_by"`
// SSOLabel is the operator's own text for the sign-in page's SSO button
// (issue #28). Empty means the product's translated default.
SSOLabel string `json:"sso_label"`
}
BrandingConfig is the effective branding payload — the JSON shape of GET /api/branding and the input to the public-page chrome builder.
type BrandingSource ¶
type BrandingSource struct {
Store db.Store
MultiTenant bool
// Appearance supplies the instance THEME for the server-rendered public
// pages (tema:v1). It hangs off the branding source rather than being
// threaded through every public handler because those handlers already
// hold exactly one appearance-ish dependency, and adding a second
// constructor argument to eight call sites to express "and also the
// theme" would be ceremony, not clarity. Nil-safe.
Appearance *AppearanceSource
// contains filtered or unexported fields
}
BrandingSource resolves the effective branding for a request host from the settings store, with a small TTL cache (public share pages are hot paths). A nil *BrandingSource is safe everywhere and yields the zero config — unwired handlers keep their exact pre-branding output.
func NewBrandingSource ¶
func NewBrandingSource(store db.Store, multiTenant bool) *BrandingSource
NewBrandingSource constructs a BrandingSource.
func (*BrandingSource) AttachAppearance ¶
func (b *BrandingSource) AttachAppearance(a *AppearanceSource)
AttachAppearance wires the theme source used by the public-page chrome.
func (*BrandingSource) For ¶
func (b *BrandingSource) For(ctx context.Context, host string) BrandingConfig
For resolves the effective branding for a host: global branding.* keys, overlaid (in multi-tenant mode) by the host's tenant.<id>.branding.* keys.
func (*BrandingSource) ForRequest ¶
func (b *BrandingSource) ForRequest(r *http.Request) BrandingConfig
ForRequest resolves branding for an HTTP request (host-scoped in multi-tenant mode). Nil-receiver safe.
func (*BrandingSource) Invalidate ¶
func (b *BrandingSource) Invalidate()
Invalidate drops the cache — called after any branding settings write.
type Capabilities ¶
type Capabilities struct {
Service *capability.Service
// Store + MultiTenant power the per-tenant branding block: in multi-tenant
// mode the (pre-auth, host-resolved) capabilities answer carries only THIS
// host's tenant identity — never the existence of other tenants
// (docs/MULTI-TENANCY.md §12 + isolation checklist).
Store db.Store
MultiTenant bool
// Mail, when wired, answers whether outgoing mail can be sent right now
// (configured and verified). Published as `mail.ready`, a boolean with no
// host in it. Nil = the field is absent and clients keep offering mail.
Mail interface {
Ready(ctx context.Context) bool
}
/* kimlik:e3 cloud */
// CloudEnabled mirrors FILEX_CLOUD (set by BuildRouter only when the flag
// is on). While false — the default — the capabilities payload carries NO
// cloud field at all, keeping the flag-off wire format byte-identical.
CloudEnabled bool
// AppDocs lists the rows the running apps add to the "New" menu
// (`new_documents`). Told to a signed-in person only: which apps an
// instance runs is not a property of the build. Nil = none.
AppDocs func() []newdoc.Type
/* wiring:e2 */
// E2EEscrow is the installation escrow PUBLIC key, or nil when escrow is
// off. It is published deliberately: the browser needs it to wrap a new
// encrypted folder's master key to the operator, and a user about to
// create such a folder is entitled to know, before they create it, that
// their operator holds a second key to it.
E2EEscrow *e2e.EscrowKey
// E2EPolicy answers `e2e_policy`: whether the caller's tenant may use
// encryption at all, and its policy (internal/e2epolicy). Nil publishes
// nothing, which is what a server from before the policy said.
E2EPolicy *e2epolicy.Service
// Tenants + PublicURLSet feed `public_url`: the address this deployment is
// reached at, for the connection guides (see Get). Zero values publish
// nothing, which is what every test that builds this handler by hand gets.
Tenants tenanturl.Resolver
PublicURLSet bool
// Archive publishes the non-sensitive creation policy used by the explorer
// so the create dialog honours the operator's configured default.
Archive *archivecli.Service
// SSO lists the sign-in page's SSO buttons for the address asked on (the
// tenant it names, else the platform's own): `auth_sso` [{id, label}].
// Nil = the field is absent and the page keeps its one button from
// `auth_drivers` (docs/TENANT-ADMIN.md).
SSO func(r *http.Request) []ssoMethod
// Queued names the changes this server runs as jobs of its operations queue
// when asked with `queued=1`: "rename" on POST /api/files/manager?action=rename,
// "restore" on POST /api/files/manager/restore, "purge" on
// DELETE /api/admin/trash/{id}. Published as `queued`;
// empty publishes nothing, and the explorer changes inside the request as
// it always did.
Queued []string
}
Capabilities exposes /api/capabilities.
func NewCapabilities ¶
func NewCapabilities(svc *capability.Service, store db.Store, multiTenant bool) *Capabilities
NewCapabilities constructs a Capabilities handler.
func (*Capabilities) Get ¶
func (h *Capabilities) Get(w http.ResponseWriter, r *http.Request)
Get returns the runtime feature snapshot.
We emit BOTH the rich nested shape (filex-core admin SPA) AND a flat alias set (legacy embed.js + filex-core SFC fallback expected: `ffmpeg / ghostscript / max_chunk_mb / upload_limit_mb / onlyoffice_url / drawio_url`). Cheap to ship both - keeps the SFC happy without breaking the existing admin UI bindings.
⚠ 0.50: no `libreoffice` any more. LibreOffice left the images and the office engine is the connected OnlyOffice Document Server, so a flag named after a program filex never runs could only ever be false - a reader would take it for "office documents cannot be previewed or converted here".
type CapabilitiesShort ¶
type CapabilitiesShort struct {
FFmpeg bool `json:"ffmpeg"`
Ghostscript bool `json:"ghostscript"`
// No `libreoffice` since 0.50: office documents are OnlyOffice's
// (onlyoffice_reachable), and filex runs no LibreOffice anywhere.
OnlyOfficeReachable bool `json:"onlyoffice_reachable"`
}
CapabilitiesShort is the trimmed capability summary embedded in the dashboard payload (full capabilities are at /api/capabilities).
type ChangeEmitter ¶
type ChangeEmitter interface {
EmitChange(storageID int64, dir string, ev realtime.ChangeEvent)
}
ChangeEmitter is the minimal surface the file-mutation handlers need to publish folder-change events to realtime (WebSocket) subscribers. The realtime hub (*realtime.Hub) satisfies it. Keeping it an interface means the mutation handlers don't hard-depend on a concrete hub — a nil emitter simply disables live updates (the default in tests and when realtime is unwired).
type Cloud ¶
type Cloud struct {
Svc *cloud.Service
Stripe *cloud.StripeClient
// MultiTenant is echoed in /status — a real cloud launch requires
// FILEX_MULTI_TENANT=1 (a signed-up tenant is a provider row).
MultiTenant bool
}
Cloud handles /api/cloud.
func (*Cloud) Checkout ¶
func (h *Cloud) Checkout(w http.ResponseWriter, r *http.Request)
Checkout drafts a Stripe checkout session. Without STRIPE_SECRET → 503.
func (*Cloud) Plans ¶
func (h *Cloud) Plans(w http.ResponseWriter, r *http.Request)
Plans returns the configured catalog.
func (*Cloud) Signup ¶
func (h *Cloud) Signup(w http.ResponseWriter, r *http.Request)
Signup provisions a disabled tenant + mints the verification token.
func (*Cloud) Status ¶
func (h *Cloud) Status(w http.ResponseWriter, r *http.Request)
Status is the simple state snapshot (the brief's "durum endpoint'i").
type Comments ¶
Comments wraps the node-comment HTTP routes.
func NewComments ¶
NewComments constructs the handler (service built in-place — no other subsystem needs it, the trash purge hook talks to the store directly).
func (*Comments) AttachACL ¶
AttachACL wires the RBAC resolver (nil = ACL unwired → allow, matching every other file handler).
func (*Comments) Create ¶
func (h *Comments) Create(w http.ResponseWriter, r *http.Request)
Create adds a comment to a node the caller can see and emits the comment.added event (webhook v2 targets filter on the name).
POST /api/files/comments {node_id, body} → {comment: {…}}
type CustomCSS ¶
CustomCSS serves the operator stylesheet to signed-in browsers.
func NewCustomCSS ¶
NewCustomCSS constructs the handler.
func (*CustomCSS) Get ¶
func (h *CustomCSS) Get(w http.ResponseWriter, r *http.Request)
Get returns the effective stylesheet for this installation.
⚠ `Cache-Control: no-store`. This is per-installation rather than per-person, so a cache would be correct — but it is also the thing an operator turns off in a hurry, and a sheet still sitting in a shared cache after the switch was flipped is the exact failure the switch exists to end.
type Dashboard ¶
type Dashboard struct {
Store db.Store
Caps *capability.Service
// Queue answers "Queue depth": the jobs waiting or running in the op queue
// (the same counters the Queue page's Pending and Running cards show).
// Nil reads as an empty queue.
Queue QueueStats
// DemoMode marks a public playground. The `recent_activity` block below
// is the same audit rows the Audit page serves, client addresses and all,
// on the FIRST page of the admin panel. See demoMask.recent.
DemoMode bool
}
Dashboard handles /api/admin/dashboard.
func NewDashboard ¶
NewDashboard constructs the handler. q is the op queue ("Queue depth"); nil reads as an empty queue.
type DesktopAuth ¶
type DesktopAuth struct {
// contains filtered or unexported fields
}
DesktopAuth authorizes the desktop app through the BROWSER instead of a native form.
Why not a native login form: it can only ever do username+password. An install that authenticates with Keycloak/OIDC — passkeys, MFA, corporate SSO — cannot be signed into that way at all. The browser already knows how to do all of it (and usually already has the session), so the desktop app sends the user there and waits to be handed a credential.
The flow, and why each piece exists:
desktop → picks verifier + state, opens the browser at
<server>/admin/login?desktop_state=…&desktop_challenge=sha256(verifier)
browser → user signs in HOWEVER this server does it (local, OIDC, passkey)
SPA → POST /api/auth/desktop/complete (session-authenticated)
server mints a durable token, parks it behind a ONE-TIME code
SPA → navigates to filex://auth?state=…&code=…
desktop → POST /api/auth/desktop/exchange {state, code, verifier}
server checks sha256(verifier) == challenge, returns the token
The deep link carries only a one-time code, never the token: a custom-scheme URL is handed around by the OS and lands in shell histories and logs. The verifier never leaves the desktop process, so a code someone else captures is worthless to them (PKCE, RFC 7636 in spirit).
func NewDesktopAuth ¶
func NewDesktopAuth(store db.Store) *DesktopAuth
func (*DesktopAuth) Complete ¶
func (h *DesktopAuth) Complete(w http.ResponseWriter, r *http.Request)
Complete is called BY THE SPA once the browser session is authenticated, so it runs with whatever identity the server established — password, OIDC, it does not matter here. It mints the desktop's durable token and parks it behind a one-time code.
⚠⚠ A browser SESSION, and nothing else. The route sits in the group that also accepts API tokens (routes.go), and until this check any token could call it and walk away with a brand-new read,write,delete token for its owner — no `root:` confinement, whatever scopes the calling token had. Measured with a read-only integration token: 200, and a full token parked for collection. Now that the minted token is a PERSON's (kind `user`), the same hole would also let an embed's shared proxy token turn itself into a personal credential, which is exactly what RequirePersonalCaller exists to prevent. A token of either kind is refused, and nothing is minted or parked.
POST /api/auth/desktop/complete
func (*DesktopAuth) Exchange ¶
func (h *DesktopAuth) Exchange(w http.ResponseWriter, r *http.Request)
Exchange trades the one-time code for the token. Unauthenticated on purpose — the desktop app has no session yet; possession of the verifier IS the proof.
POST /api/auth/desktop/exchange
type DiscoveredFolder ¶
DiscoveredFolder is one folder directly under a probed root, with the root value a storage mounted on it would carry.
type Drop ¶
type Drop struct {
// ACL, when wired, closes a link whose creator may no longer make it
// (link_creator.go). Nil in tests built by hand: every link answers.
ACL *acl.Resolver
Store db.Store
Manager *Manager
Service *share.Service
Notify notify.Service
Mailer *mailer.Service
PublicURL string
// Branding resolves the public pages' identity (wiring:e1). Nil-safe.
Branding *BrandingSource
// DefaultLocale is the fallback language for the drop pages — used when
// the visitor's browser asks for neither of the two we ship. Same
// contract as Share.DefaultLocale; see publicLocale.
DefaultLocale string
// Tenants resolves which origin the owner-notification e-mail links to.
Tenants tenanturl.Resolver
// Shell is the SPA document a JavaScript browser gets at /d/{token}; the
// page in this file is the no-JS answer behind it. nil (frontend not
// bundled) means every visitor gets the Go page. See public_shell.go.
Shell http.Handler
// contains filtered or unexported fields
}
Drop serves the public file-drop (upload link) endpoints — the inverse of the Share download link. A visitor with a /d/{token} link can drop one or more files INTO the linked folder without an account and, critically, without ever seeing, listing or downloading the folder's existing contents ("blind drop"). The target folder is resolved server-side from the token; the anonymous client can never influence the destination path.
func NewDrop ¶
func NewDrop(store db.Store, mgr *Manager, svc *share.Service, nf notify.Service, ml *mailer.Service, publicURL string) *Drop
NewDrop constructs the file-drop handler. mgr provides the shared ingest path (IngestFile / EnsureDir); notify + mailer are optional (nil disables that channel).
func (*Drop) AttachACL ¶ added in v0.49.0
AttachACL wires the permission resolver: a file request closes when its creator may no longer make one, and the creator's blocked file types apply to what is dropped (link_creator.go).
func (*Drop) AttachBranding ¶
func (h *Drop) AttachBranding(b *BrandingSource)
AttachBranding wires the shared branding source (wiring:e1).
func (*Drop) AttachLocale ¶
AttachLocale sets the fallback language for the drop pages.
func (*Drop) AttachShell ¶
AttachShell wires the SPA document served to JavaScript browsers.
func (*Drop) AttachTenants ¶
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*Drop) Page ¶
func (h *Drop) Page(w http.ResponseWriter, r *http.Request)
Page renders the public upload UI (or a PIN gate). Mirrors the download side's /s/{token}: a PIN-protected drop shows the PIN form first; once the PIN is accepted (via POST) the uploader page is rendered with the PIN embedded so the browser's upload carries it.
type Duplicates ¶
Duplicates serves the admin duplicate-file report.
func NewDuplicates ¶
func NewDuplicates(store db.Store) *Duplicates
NewDuplicates constructs the handler.
func (*Duplicates) Report ¶
func (h *Duplicates) Report(w http.ResponseWriter, r *http.Request)
Report handles GET /api/admin/duplicates.
limit — max groups returned (default 100, capped at 1000)
min_size — minimum file size in bytes to consider (default 1, so
zero-byte files never flood the report)
type E2E ¶
type E2E struct {
Store db.Store
ACL *acl.Resolver
Escrow *e2e.EscrowKey // nil when escrow is disabled for this installation
// contains filtered or unexported fields
}
E2E serves the escrow challenge/report pair. Everything else about end-to-end encryption happens in the browser.
func (*E2E) AttachCleanup ¶ added in v0.48.0
func (h *E2E) AttachCleanup(v E2ECleanupVersions, t E2ECleanupTrash, th E2ECleanupThumbs, ix E2ECleanupIndex)
AttachCleanup wires what POST /api/files/e2e/cleanup removes from. Each may be nil; that part is then skipped (and reported as zero).
func (*E2E) Cleanup ¶ added in v0.48.0
func (h *E2E) Cleanup(w http.ResponseWriter, r *http.Request)
Cleanup removes what filex holds from before a folder was encrypted.
POST /api/files/e2e/cleanup {path, versions, trash}
→ {versions_deleted, trash_purged, thumbnails_dropped, index_cleared}
Thumbnails and extracted search content are always dropped: they are caches of plaintext the folder no longer has. Versions and trash entries are someone's safety net and are removed only when asked, and only by the folder's owner or an administrator.
func (*E2E) EscrowChallenge ¶
func (h *E2E) EscrowChallenge(w http.ResponseWriter, r *http.Request)
EscrowChallenge mints a proof-of-possession challenge for an encrypted folder, or for a single encrypted file (`.fxe`, its own root).
POST /api/files/e2e/escrow/challenge {path} → {id, challenge}
`challenge` is base64 RSA-OAEP(escrow public key, nonce). A caller without the private half learns nothing from it.
func (*E2E) EscrowUsed ¶
func (h *E2E) EscrowUsed(w http.ResponseWriter, r *http.Request)
EscrowUsed redeems the challenge and notifies the owner of the folder (or of the single encrypted file) that was opened.
POST /api/files/e2e/escrow/used {path, id, nonce} → {ok, notified}
`notified` reports whether an owner could be identified. When the folder has no recorded owner the event still goes out — as an admin-visible broadcast rather than to nobody.
func (*E2E) PasswordChanged ¶ added in v0.48.0
func (h *E2E) PasswordChanged(w http.ResponseWriter, r *http.Request)
PasswordChanged records a folder's (or a single encrypted file's) password change and tells the owner.
POST /api/files/e2e/password-changed {path, via, rekey} → {ok, notified}
type E2ECleanupIndex ¶ added in v0.48.0
type E2ECleanupIndex interface {
IndexNodeContent(ctx context.Context, n *model.Node, content string) error
}
E2ECleanupIndex blanks a node's extracted content in the search index.
type E2ECleanupThumbs ¶ added in v0.48.0
E2ECleanupThumbs drops a node's thumbnail (the JPEG and its row).
type E2ECleanupTrash ¶ added in v0.48.0
type E2ECleanupTrash interface {
List(ctx context.Context, storageID *int64, limit, offset int) ([]trash.TrashEntry, int, error)
PurgeOne(ctx context.Context, nodeID int64) error
}
E2ECleanupTrash is the trash surface the cleanup needs.
type E2ECleanupVersions ¶ added in v0.48.0
type E2ECleanupVersions interface {
List(ctx context.Context, nodeID int64) ([]*model.NodeVersion, error)
HardDeleteVersion(ctx context.Context, versionID int64) error
}
E2ECleanupVersions is the versioning surface the cleanup needs.
type E2EPolicyAdmin ¶ added in v0.51.0
type E2EPolicyAdmin struct {
Store db.Store
Policy *e2epolicy.Service
MultiTenant bool
// Requests decides the approval policy's requests (e2e_policy_files.go
// leaves them).
Requests *e2epolicy.Requests
}
E2EPolicyAdmin is the handler set.
func NewE2EPolicyAdmin ¶ added in v0.51.0
NewE2EPolicyAdmin constructs the handler.
func (*E2EPolicyAdmin) ApproveRequest ¶ added in v0.51.0
func (h *E2EPolicyAdmin) ApproveRequest(w http.ResponseWriter, r *http.Request)
ApproveRequest opens one encryption for the requester at the place asked: POST /api/admin/e2e/requests/{id}/approve. Session only.
func (*E2EPolicyAdmin) Get ¶ added in v0.51.0
func (h *E2EPolicyAdmin) Get(w http.ResponseWriter, r *http.Request)
Get answers the policy this administrator holds: GET /api/admin/e2e.
func (*E2EPolicyAdmin) ListRequests ¶ added in v0.51.0
func (h *E2EPolicyAdmin) ListRequests(w http.ResponseWriter, r *http.Request)
ListRequests answers the requests this administrator decides: GET /api/admin/e2e/requests[?status=…]. Pending by default.
func (*E2EPolicyAdmin) Patch ¶ added in v0.51.0
func (h *E2EPolicyAdmin) Patch(w http.ResponseWriter, r *http.Request)
Patch sets the policy this administrator holds: PATCH /api/admin/e2e {"policy": "…"}. Session only. A tenant whose ceiling is off may still choose one: it applies the day the operator switches the ceiling on.
func (*E2EPolicyAdmin) PatchTenant ¶ added in v0.51.0
func (h *E2EPolicyAdmin) PatchTenant(w http.ResponseWriter, r *http.Request)
PatchTenant switches a tenant's ceiling: PATCH /api/admin/e2e/tenants/{id} {"e2e_allowed": false}. Supertenant only, session only, multi-tenant only. Off, nobody in that tenant may start encrypting — its administrators included; what is already encrypted keeps working.
func (*E2EPolicyAdmin) RejectRequest ¶ added in v0.51.0
func (h *E2EPolicyAdmin) RejectRequest(w http.ResponseWriter, r *http.Request)
RejectRequest closes a request: POST /api/admin/e2e/requests/{id}/reject. Session only.
func (*E2EPolicyAdmin) Tenants ¶ added in v0.51.0
func (h *E2EPolicyAdmin) Tenants(w http.ResponseWriter, r *http.Request)
Tenants answers every tenant's ceiling and policy: GET /api/admin/e2e/tenants. Supertenant only.
type E2EPolicyFiles ¶ added in v0.51.0
type E2EPolicyFiles struct {
Store db.Store
Policy *e2epolicy.Service
Requests *e2epolicy.Requests
}
E2EPolicyFiles is the handler set.
func NewE2EPolicyFiles ¶ added in v0.51.0
func NewE2EPolicyFiles(store db.Store, policy *e2epolicy.Service, requests *e2epolicy.Requests) *E2EPolicyFiles
NewE2EPolicyFiles constructs the handler.
func (*E2EPolicyFiles) Allowed ¶ added in v0.51.0
func (h *E2EPolicyFiles) Allowed(w http.ResponseWriter, r *http.Request)
Allowed answers, for each path, what the explorer may offer there: POST /api/files/e2e/allowed {"items":[{"path":"Depo://a","kind":"new_folder"}]} → {"encrypt":["allowed"|"request"|"denied",…], "reasons":["",…]}, in the order of the items. A reason names the layer that said no, as a 403 e2e_not_allowed would (e2epolicy.Reason; approval_required with "request", "" with "allowed"): the CLI says it before asking for a password.
An item's kind says which encryption is asked about: `folder` (the path is the folder to encrypt where it is), `new_folder` (the folder to make a new encrypted folder in), `file` (the file to encrypt), or none for the one the catalogue says. The answer is e2epolicy.Service.AnswersForKinds there - the rule every create door asks (CheckCreate), with the approvals that door would spend looked for and never spent, so the menu offers exactly what the server then accepts. A kind it does not know answers "denied". It changes nothing, so it needs `read`, like POST ?action=allowed beside it.
⚠ Asked for administrators too, unlike the permission question: policy `off` stops them, and so does their tenant's ceiling.
A path it cannot place — a storage outside the caller's tenant (the list is the tenant's, tenantstore), "..", a storage nobody has — answers "denied". So does a path outside the caller's root, when a token's `root:` scope or X-Filex-Root confines them to a folder: nothing of the rule is heard there.
⚠ confine.Middleware refuses such a path in a JSON body, but it reads no other body, and this handler decodes JSON whatever the Content-Type says — so the root is read here too, before any rule is asked, as place() reads it for a request.
The answer is a list and not keyed by path: a folder-confined token's paths are rewritten on the way in (confine.Middleware), so the caller would not recognise them.
func (*E2EPolicyFiles) CreateRequest ¶ added in v0.51.0
func (h *E2EPolicyFiles) CreateRequest(w http.ResponseWriter, r *http.Request)
CreateRequest leaves a request: POST /api/files/e2e/requests. 201 with the new request, 200 with the one already waiting for this person and place. 404 path_missing when nothing is at the path, 429 too_many_pending when the person has e2epolicy.MaxPendingPerPerson waiting already.
func (*E2EPolicyFiles) MyRequests ¶ added in v0.51.0
func (h *E2EPolicyFiles) MyRequests(w http.ResponseWriter, r *http.Request)
MyRequests answers the caller's own requests, newest first: GET /api/files/e2e/requests. A token confined to a folder (`root:`) sees the ones inside it, as it sees nothing else outside it.
type ExternalAdmin ¶
type ExternalAdmin struct {
Store db.Store
Caps *capability.Service
// Live is the runtime resolver every consumer reads through. Invalidated
// on write so an operator's change takes effect on the next request.
Live *external.Resolver
// EnvManaged names the services pinned by env/YAML. Those rows are
// re-asserted from the environment at every boot, so a change made here
// applies live but does not survive a restart — and the API says so.
EnvManaged map[string]bool
// PublicURL is the address filex hands to the document server for the
// document fetch and the save callback. It is the third address in the
// three-address problem (see internal/external/advisory.go) and the one
// nothing used to check, so it travels with every List and Test response.
PublicURL string
// PublicURLSet is false when nobody chose PublicURL and it defaulted to
// http://localhost:5212. Worth saying out loud: an operator who never set
// it does not know a default is in play.
PublicURLSet bool
// ReversePath measures the third leg — the document server's route BACK to
// filex — by asking it to download a one-shot URL of ours, through the same
// signed fetch endpoint a document uses, and watching for the request. It
// also asks whether the document server enforces JWT. A nil target means
// the configuration in force. Nil when OnlyOffice is not wired, and only
// meaningful for that service: nothing else calls filex back.
ReversePath func(ctx context.Context, t *onlyoffice.Target) onlyoffice.ReverseResult
}
ExternalAdmin handles /api/admin/external.
func NewExternalAdmin ¶
func NewExternalAdmin(store db.Store, caps *capability.Service, live *external.Resolver, envManaged map[string]bool) *ExternalAdmin
NewExternalAdmin constructs the handler.
func (*ExternalAdmin) AttachPublicURL ¶
func (h *ExternalAdmin) AttachPublicURL(publicURL string, set bool)
AttachPublicURL wires the process's public URL into the advisories. Kept out of NewExternalAdmin so the two call sites (routes.go and the MCP admin surface) cannot silently disagree about the constructor signature.
func (*ExternalAdmin) List ¶
func (h *ExternalAdmin) List(w http.ResponseWriter, r *http.Request)
List returns every configured external service.
func (*ExternalAdmin) Test ¶
func (h *ExternalAdmin) Test(w http.ResponseWriter, r *http.Request)
Test runs an immediate health probe and returns the state.
func (*ExternalAdmin) Update ¶
func (h *ExternalAdmin) Update(w http.ResponseWriter, r *http.Request)
Update upserts a row and re-runs the health probe.
type FTPSFacts ¶
type FTPSFacts struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
// ⚠ The passive range matters as much as the port: a firewall that blocks
// it makes every transfer HANG with no error on either side, which is the
// classic FTP failure and impossible to guess at from the client end.
PasvMin int `json:"pasv_min"`
PasvMax int `json:"pasv_max"`
// SelfSigned is true when no certificate was configured, so the guide can
// say the channel is encrypted but unverified rather than letting somebody
// discover it from a client warning.
SelfSigned bool `json:"self_signed"`
}
FTPSFacts is what an FTP client needs to be told, computed on the server.
type FileLinkStreamer ¶ added in v0.45.0
type FileLinkStreamer interface {
ServeLinkedFile(w http.ResponseWriter, r *http.Request, s *model.Storage, rel string)
}
FileLinkStreamer serves the bytes of one file for a link. *Manager is the implementation: the link goes through the same download path as the explorer's own Download (Range, staging, MIME, disposition), not a copy of it.
type FileTypesAdmin ¶ added in v0.50.0
type FileTypesAdmin struct {
Assoc *assoc.Service
// Demo refuses every change (the admin account of a demo is public).
Demo bool
}
FileTypesAdmin serves the Default apps screen.
func NewFileTypesAdmin ¶ added in v0.50.0
func NewFileTypesAdmin(svc *assoc.Service, demo bool) *FileTypesAdmin
NewFileTypesAdmin constructs the handler. A nil service (app plugins off) still lists filex's own kinds - none - and refuses changes with 503.
func (*FileTypesAdmin) List ¶ added in v0.50.0
func (h *FileTypesAdmin) List(w http.ResponseWriter, r *http.Request)
List answers GET /api/admin/file-types.
func (*FileTypesAdmin) Put ¶ added in v0.50.0
func (h *FileTypesAdmin) Put(w http.ResponseWriter, r *http.Request)
Put answers PUT /api/admin/file-types/{ext}.
func (*FileTypesAdmin) Reset ¶ added in v0.50.0
func (h *FileTypesAdmin) Reset(w http.ResponseWriter, r *http.Request)
Reset answers DELETE /api/admin/file-types/{ext}: both capabilities back to the default order.
type FolderPreviewSwitch ¶ added in v0.50.0
type FolderPreviewSwitch interface{ FolderPreviews() bool }
FolderPreviewSwitch reports whether folders show their pictures (*thumb.Pipeline: thumb.FolderPreviewsSetting, cached).
type Grants ¶
type Grants struct {
Store db.Store
ACL *acl.Resolver
Mailer *mailer.Service
PublicURL string
// Tenants resolves which origin the invite e-mails link to. ⚠ The
// account-created mail carries a temporary password next to a login URL —
// on the operator's host that login cannot succeed, and the operator's
// hostname leaks to every tenant that adds a user.
Tenants tenanturl.Resolver
}
Grants is the per-file/per-folder permission-management API backing the explorer's right-side "İzinler" (Permissions) panel. It is mounted under /api/files/permissions inside the authenticated group (session OR token), so confine.Middleware still applies to any path fields.
Authorization for every endpoint: the caller must be an admin OR hold owner-level (acl.LevelOwner) on the target path. Viewer/editor accounts and non-owning users get 403 and never see the panel.
func (*Grants) AdminDelete ¶
func (h *Grants) AdminDelete(w http.ResponseWriter, r *http.Request)
AdminDelete revokes any grant (admin override).
DELETE /api/admin/grants/{id}
func (*Grants) AdminDeleteGroup ¶ added in v0.50.0
func (h *Grants) AdminDeleteGroup(w http.ResponseWriter, r *http.Request)
AdminDeleteGroup revokes any group's grant (admin override).
DELETE /api/admin/grants/groups/{id}
func (*Grants) AdminList ¶
func (h *Grants) AdminList(w http.ResponseWriter, r *http.Request)
AdminList returns every grant across all storages, enriched with storage name + user email, for the admin panel's global "İzinler" overview (who has what, where). Admin-only via the /api/admin route group.
GET /api/admin/grants → {grants:[{id, storage_name, path_prefix, user_email, level, …}]}
func (*Grants) AttachInvite ¶
AttachInvite wires the share service + mailer + public URL used by the email-invite flow (existing user → grant, admin → create user, else share).
func (*Grants) AttachTenants ¶
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*Grants) Create ¶
func (h *Grants) Create(w http.ResponseWriter, r *http.Request)
Create (upsert) a grant for a user — or a group — on a path.
POST /api/files/permissions {path, user_id | group_id, level, is_dir?}
func (*Grants) Delete ¶
func (h *Grants) Delete(w http.ResponseWriter, r *http.Request)
Delete revokes a grant.
DELETE /api/files/permissions/{id}
func (*Grants) DeleteGroup ¶ added in v0.50.0
func (h *Grants) DeleteGroup(w http.ResponseWriter, r *http.Request)
DeleteGroup revokes a group grant.
DELETE /api/files/permissions/groups/{id}
func (*Grants) Invite ¶
func (h *Grants) Invite(w http.ResponseWriter, r *http.Request)
Invite grants access to an email address. Three outcomes (owner/admin only):
- existing account → a direct ACL grant (mode "granted")
- no account + caller is admin + create_user → new account + grant, temp password mailed (or returned for on-screen display) (mode "user_created")
- otherwise → a public share link, mailed or returned (mode "shared")
Mail is sent only when SMTP is configured AND verified; otherwise the link / temp password comes back in the response for the UI to show.
POST /api/files/permissions/invite {path, email, level, create_user?, role?}
func (*Grants) List ¶
func (h *Grants) List(w http.ResponseWriter, r *http.Request)
List returns the direct + inherited grants for a path so the panel can show who has access (including permissions cascading from parent folders).
GET /api/files/permissions?path=<adapter://rel>
func (*Grants) Resolve ¶
func (h *Grants) Resolve(w http.ResponseWriter, r *http.Request)
Resolve looks up a user by email so the panel can decide between a direct grant (existing account) and the invite flow (no account).
GET /api/files/permissions/resolve?email=<addr>
func (*Grants) SearchGroups ¶ added in v0.50.0
func (h *Grants) SearchGroups(w http.ResponseWriter, r *http.Request)
SearchGroups returns the groups matching q (by name) that the caller could grant to, so the permissions panel can offer them beside people. Like SearchUsers, any authenticated user may call it; it carries names only.
GET /api/files/permissions/groups?q=<substr>
func (*Grants) SearchUsers ¶
func (h *Grants) SearchUsers(w http.ResponseWriter, r *http.Request)
SearchUsers returns existing accounts matching q (email or display name) so the permissions panel can autocomplete as the owner types. Any authenticated user may call it (the panel itself is owner-gated); results are capped and carry no secrets.
GET /api/files/permissions/users?q=<substr>
func (*Grants) ShareMail ¶
func (h *Grants) ShareMail(w http.ResponseWriter, r *http.Request)
ShareMail emails an already-created public share link to an address. It does NOT create a share — it delivers a link the caller just made (with their chosen expiry/PIN) in the share tab. Gated editor+ on the path, the same capability that created the link. Best-effort: returns {emailed:false} when SMTP isn't verified so the UI keeps showing the link for manual delivery.
POST /api/files/permissions/share-mail {path, email, url}
func (*Grants) Update ¶
func (h *Grants) Update(w http.ResponseWriter, r *http.Request)
Update changes a grant's level.
PATCH /api/files/permissions/{id} {level}
func (*Grants) UpdateGroup ¶ added in v0.50.0
func (h *Grants) UpdateGroup(w http.ResponseWriter, r *http.Request)
UpdateGroup changes a group grant's level.
PATCH /api/files/permissions/groups/{id} {level}
type GroupsAdmin ¶ added in v0.50.0
GroupsAdmin serves /api/admin/groups* and /api/admin/users/{id}/groups.
func NewGroupsAdmin ¶ added in v0.50.0
func NewGroupsAdmin(store db.Store, resolver *acl.Resolver) *GroupsAdmin
NewGroupsAdmin constructs the handler.
func (*GroupsAdmin) AddMembers ¶ added in v0.50.0
func (h *GroupsAdmin) AddMembers(w http.ResponseWriter, r *http.Request)
AddMembers puts people in a group by hand. A person already in it through a link becomes a manual member — they stay whatever the directory says.
POST /api/admin/groups/{id}/members {"user_ids":[3,4]}
func (*GroupsAdmin) Create ¶ added in v0.50.0
func (h *GroupsAdmin) Create(w http.ResponseWriter, r *http.Request)
Create adds a group.
POST /api/admin/groups {name, description, role_id, links, provider_id?}
func (*GroupsAdmin) Delete ¶ added in v0.50.0
func (h *GroupsAdmin) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a group, its memberships and its folder grants. Its members keep their level underneath; one with another group holding a role gets that group's role.
DELETE /api/admin/groups/{id}
func (*GroupsAdmin) Detach ¶ added in v0.52.0
func (h *GroupsAdmin) Detach(w http.ResponseWriter, r *http.Request)
Detach keeps a group whose directory group is gone as an ordinary filex group: it is no longer the directory's, its dead LDAP link goes, and its folders, role and hand-added members stay. Only for a group flagged removed — one the directory still has is the directory's, and which directory groups exist is managed on the directory. An administrator's: links are.
POST /api/admin/groups/{id}/detach
func (*GroupsAdmin) Get ¶ added in v0.50.0
func (h *GroupsAdmin) Get(w http.ResponseWriter, r *http.Request)
Get returns one group with its members and its folder grants.
GET /api/admin/groups/{id}
func (*GroupsAdmin) List ¶ added in v0.50.0
func (h *GroupsAdmin) List(w http.ResponseWriter, r *http.Request)
List returns the groups the caller's tenant may see, in id order, each with how many members and folder grants it has.
GET /api/admin/groups
func (*GroupsAdmin) Memberships ¶ added in v0.52.0
func (h *GroupsAdmin) Memberships(w http.ResponseWriter, r *http.Request)
Memberships answers every person's groups the caller may see, keyed by user id — the Users list's Groups column in one call instead of one per row. Each carries the membership's source (added, SSO, LDAP).
func (*GroupsAdmin) RemoveMember ¶ added in v0.50.0
func (h *GroupsAdmin) RemoveMember(w http.ResponseWriter, r *http.Request)
RemoveMember takes a person out of a group, however they came to be in it. One who is in it through a link joins again at their next sign-in if the directory still says so.
DELETE /api/admin/groups/{id}/members/{user_id}
func (*GroupsAdmin) Update ¶ added in v0.50.0
func (h *GroupsAdmin) Update(w http.ResponseWriter, r *http.Request)
Update replaces a group's name, description, role and links. Its tenant never changes.
PUT /api/admin/groups/{id}
func (*GroupsAdmin) UserGroups ¶ added in v0.50.0
func (h *GroupsAdmin) UserGroups(w http.ResponseWriter, r *http.Request)
UserGroups returns the groups one account is in, and how.
GET /api/admin/users/{id}/groups → {"groups":[{id, name, role_id, source}]}
type LazyCatalogue ¶ added in v0.44.2
type LazyCatalogue interface {
// CatalogueOpened: somebody listed dir of a lazy storage. Never blocks.
CatalogueOpened(storageID int64, dir string)
// CatalogueCurrent: is the storage lazy, and does its catalogue vouch
// for dir right now?
CatalogueCurrent(storageID int64, dir string) (lazy, current bool)
// CatalogueCoverage: how much of st the catalogue covers; nil = all.
CatalogueCoverage(ctx context.Context, st *model.Storage) *syncpkg.CatalogueCoverage
// CatalogueSizePartial: which of these folders' sizes leave something out.
CatalogueSizePartial(ctx context.Context, storageID int64, dirs []string) map[string]bool
// CatalogueSubtree: catalogue everything under dir (a desktop sync pair).
CatalogueSubtree(storageID int64, dir string)
// NoteActivity: somebody is using the storage (slows the filler).
NoteActivity(storageID int64)
}
LazyCatalogue is what the file handlers ask the sync worker about a storage's catalogue (internal/sync lazy_api.go; docs/LAZY-CATALOGUE.md). *sync.Worker satisfies it. nil everywhere means "the catalogue is whatever the last scan left", which is how every handler behaved before it existed.
type LoginSecurity ¶ added in v0.50.0
type LoginSecurity struct {
Store db.Store
// Guard is the running limiter; its settings cache is dropped after a
// write so the next attempt sees the new numbers.
Guard *loginguard.Guard
// EnvTrustedProxies is FILEX_TRUSTED_PROXIES, shown as the source of the
// trusted-proxy list when the setting is empty.
EnvTrustedProxies string
// DemoMode marks a public playground, where these pages are readable by
// whoever read the published credentials: the visitors' addresses (the
// locks, the trail) and the operator's (the allow-list, the proxies) are
// masked - demo_redact.go. On a normal install nothing is.
DemoMode bool
}
LoginSecurity handles /api/admin/login-security.
func NewLoginSecurity ¶ added in v0.50.0
func NewLoginSecurity(store db.Store, guard *loginguard.Guard, envTrustedProxies string) *LoginSecurity
NewLoginSecurity constructs the handler.
func (*LoginSecurity) Attempts ¶ added in v0.50.0
func (h *LoginSecurity) Attempts(w http.ResponseWriter, r *http.Request)
Attempts lists the sign-in trail: every wrong attempt, lock, release, allow-list pass and settings change, newest first. It is the audit log filtered to `login.` and `login_security.` - one name for each event whichever door it came through (auth.DoorAction), the door in `via`; ?action= narrows to one of them.
func (*LoginSecurity) Get ¶ added in v0.50.0
func (h *LoginSecurity) Get(w http.ResponseWriter, r *http.Request)
Get returns the settings, the bounds the form should enforce, and the trusted-proxy list in force.
func (*LoginSecurity) Locks ¶ added in v0.50.0
func (h *LoginSecurity) Locks(w http.ResponseWriter, r *http.Request)
Locks lists the counters: who is being counted, who is locked, until when. ?locked=1 keeps only the locks in force; ?scope=account|ip narrows.
func (*LoginSecurity) Patch ¶ added in v0.50.0
func (h *LoginSecurity) Patch(w http.ResponseWriter, r *http.Request)
Patch validates every field it was given and, only when all are valid, writes them. It answers what Get does.
func (*LoginSecurity) Unlock ¶ added in v0.50.0
func (h *LoginSecurity) Unlock(w http.ResponseWriter, r *http.Request)
Unlock lifts one lock — and clears its counter and its escalation — or, with {"all":true}, every lock in force. An address that is being blocked is unlocked by the address (scope "ip"), an account by the identifier as typed (scope "account"; it is normalized the way the limiter does).
type Manager ¶
type Manager struct {
Store db.Store
StorageResolver func(int64) (storage.Driver, error)
// Index is consulted by `vfSearch` BEFORE falling back to SQL LIKE.
// nil is fine — search degrades to LIKE-only.
Index *search.Index
// Thumbs, when wired, generates a thumbnail asynchronously after a
// successful upload. nil is fine — uploads still succeed, callers
// just don't get an automatic preview in the grid view.
Thumbs ThumbPipeline
// ACL enforces per-user/per-item access control. nil disables
// enforcement (tests / list-only environments) → legacy all-access.
ACL *acl.Resolver
// ThumbRefresh draws in the background what a listing found missing or
// stale (hydrateThumbs). Nil: nothing is drawn from a listing.
ThumbRefresh ThumbRefresher
// FolderPreviews says whether folders show their pictures in a listing
// (thumb.FolderPreviewsSetting, cached by the pipeline). Nil: on.
FolderPreviews FolderPreviewSwitch
// ThumbSigner stamps the `thumb_url` this listing hands out, so a bare
// `<img src>` in an embed can fetch it with no header and no cookie. nil
// emits an unsigned URL, which authenticated clients still fetch fine.
ThumbSigner *thumb.Signer
// Staged, when wired, takes large whole-body uploads into filex's own
// staging area and lets the ops worker move them to the driver. nil (or a
// deployment with no staging directory configured) keeps the synchronous
// write — the feature degrades, it never blocks an upload.
Staged *StagedUpload
// Body resolves where a file's bytes actually are — the storage driver,
// or filex's staging area while a staged upload is still transferring.
// nil is fine and degrades to driver-only reads (filebody.Resolver).
Body *filebody.Resolver
// Changes answers `action=changes` (manager_changes.go). nil = 501, and a
// sync client falls back to walking its tree.
Changes *realtime.ChangeLog
// Quota enforces the per-user ceiling on the SYNCHRONOUS write paths.
// Large writes reach the staged path, which checks at `begin`; without
// this the small-file path had no ceiling at all, and a user could sail
// past their limit a few megabytes at a time. nil disables enforcement.
Quota *quota.Service
// AppDocs makes the rows apps add to the "New" menu (new_documents).
// Nil = only the built-in kinds.
AppDocs *wasmplugin.Registry
// Lazy answers what a listing needs from the catalogue's own state: is it
// complete, can it vouch for this folder, and (on a lazy storage) please
// catalogue what was just opened. See lazy_listing.go. nil = the catalogue
// is whatever the last scan left.
Lazy LazyCatalogue
// Ops runs a folder rename asked with `queued=1` as a job of the queue
// (vfRename). nil renames inside the request, as it always did.
Ops *ops.Service
// E2EPolicy is who may encrypt (e2e_policy_gate.go). The doors on this
// handler that CREATE a file — upload, New document, a draft's save, and
// through it the resumable upload and the public file request — ask it
// before an encrypted folder's key file or a `.fxe` comes into being.
// nil = the rule is not wired (a handler built by hand in a test).
E2EPolicy *e2epolicy.Service
}
Manager handles read-only browsing endpoints under /api/files/manager.
func NewManager ¶
NewManager constructs a Manager handler.
resolver may be nil for tests / list-only environments — the Read handler will return 503 in that case.
func (*Manager) AttachACL ¶
AttachACL wires the RBAC/ACL resolver so listings are filtered and reads are gated by the caller's grants. Optional — nil means no enforcement.
func (*Manager) AttachBody ¶
AttachBody wires the byte-source resolver, so every read surface on this handler serves a file that is still being transferred out of staging instead of asking a driver that does not have it yet.
func (*Manager) AttachChangeLog ¶
AttachChangeLog wires the log `action=changes` answers from. Unwired, the action answers 501, which clients read as "walk the tree as before".
func (*Manager) AttachLazy ¶ added in v0.44.2
func (h *Manager) AttachLazy(l LazyCatalogue)
AttachLazy wires the lazy catalogue. nil keeps the handler as it was.
func (*Manager) AttachOps ¶ added in v0.47.0
AttachOps wires the queue a rename asked with `queued=1` runs on.
func (*Manager) AttachSearchIndex ¶
AttachSearchIndex wires the Bleve index into the manager. Optional — without it, vfSearch falls back to SQL LIKE only.
func (*Manager) AttachStaged ¶
func (h *Manager) AttachStaged(s *StagedUpload)
AttachStaged wires the staged ingest path so every surface that writes through IngestFile — today the public drop link — answers as soon as the bytes are safe inside filex rather than after the driver write. One switch, every surface: see the note at the top of upload_staged_ingest.go.
func (*Manager) AttachThumbPipeline ¶
func (h *Manager) AttachThumbPipeline(p ThumbPipeline)
AttachThumbPipeline wires the pipeline so vfUpload can dispatch a generation job per uploaded file.
func (*Manager) CountDrafts ¶ added in v0.48.0
func (h *Manager) CountDrafts(w http.ResponseWriter, r *http.Request)
CountDrafts answers GET /api/files/drafts/count — the navigation panel's badge, asked after every change the explorer makes to a draft.
func (*Manager) CreateDraft ¶ added in v0.48.0
func (h *Manager) CreateDraft(w http.ResponseWriter, r *http.Request)
CreateDraft answers POST /api/files/drafts: New document, as a draft.
The request is action=newfile's (vfNewFileBody) and is judged by the same half (planNewDoc): the type, the name, the folder it is meant for — the caller must be able to write THERE, because that is where it will go — the storage's read-only switch and the caller's quota. What differs is where the bytes land: the caller's drafts area, and nothing at the destination. A name already taken there is not refused: whether the draft goes in beside it as `name (2).ext` is asked when it is saved (SaveDraft), which is when it matters.
func (*Manager) DiscardDraft ¶ added in v0.48.0
func (h *Manager) DiscardDraft(w http.ResponseWriter, r *http.Request)
DiscardDraft answers DELETE /api/files/drafts/{key}: the draft goes to the TRASH, like any deleted file, and the trash's retention deletes it for good (the owner's design, issue #71). Its row is kept: a restore from the trash puts the file back in its drafts folder, and it is a draft again. The trash shows it to its owner alone (Trash.trashListKeep).
func (*Manager) EnsureDir ¶
EnsureDir makes sure a directory exists on the driver AND has a node row, returning its node id. The file-drop handler calls it to materialise a per-submission subfolder before ingesting files into it, so the owner sees the folder (and its parent link) immediately without waiting for a sync. Idempotent: returns the existing node id when the dir is already known.
func (*Manager) GetDraft ¶ added in v0.48.0
func (h *Manager) GetDraft(w http.ResponseWriter, r *http.Request)
GetDraft answers GET /api/files/drafts/{key}.
func (*Manager) IngestFile ¶
func (h *Manager) IngestFile(ctx context.Context, st *model.Storage, destRel, filename string, src io.Reader, size int64) (*model.Node, error)
IngestFile writes one uploaded file into destRel/filename on the given storage and upserts + indexes + thumbnails its node. It is the shared ingest path behind the authenticated multipart upload (vfUpload's loop) and the public file-drop handler, so both surface identical mime sniffing, node caching and thumbnail dispatch. Parent dir nodes are looked up lazily — call EnsureDir first when writing into a freshly-created folder so the new file's node links to the right parent.
func (*Manager) List ¶
func (h *Manager) List(w http.ResponseWriter, r *http.Request)
List dispatches between two query shapes on the same path:
Native (admin SPA, trash, etc.): ?storage=<id>&parent=<id> Returns {nodes:[…model.Node]} from the DB cache.
Vuefinder/FileExplorer SFC: ?action=<verb>&path=<adapter://rel> (?q=<verb> is also accepted as a legacy alias.) Returns the {adapter, storages, dirname, read_only, files:[FileNode]} shape that @brftech/filex-core expects. Only `index`, `search`, `subfolders` are wired today — other actions return 501 so the UI can still render and warn rather than 404.
Keeping both behind one route avoids breaking the existing Explore page contract while letting the SFC mount unchanged.
func (*Manager) ListDrafts ¶ added in v0.48.0
func (h *Manager) ListDrafts(w http.ResponseWriter, r *http.Request)
ListDrafts answers GET /api/files/drafts.
func (*Manager) Mutate ¶
func (h *Manager) Mutate(w http.ResponseWriter, r *http.Request)
Mutate handles the POST verbs the FileExplorer SFC fires from its toolbar: newfolder, rename, move, delete, upload.
All bodies use the @brftech/filex-core wire format (adapter://path). On success each verb re-renders the parent dir via vfIndex so the SFC's reactive store updates without a follow-up GET.
Routes that hit this dispatcher live behind the auth middleware in routes.go (POST /api/files/manager?action=…). Reads stay on the GET dispatcher in manager.go to preserve cache semantics.
func (*Manager) NameTaken ¶ added in v0.47.0
NameTaken implements ops.RenameSync: the rule the explorer's rename is refused on (destinationTaken), asked again by the worker for a queued one.
func (*Manager) Read ¶
func (h *Manager) Read(w http.ResponseWriter, r *http.Request)
Read streams a file by node ID or by storage_id+path.
Query params:
?id=<node id> primary lookup (preferred) ?storage=<id>&path=<p> fallback when caller has the path but no id ?download=1 force attachment Content-Disposition
Auth: requires an authenticated user (route is mounted behind the auth middleware). Future RBAC checks slot in here once per-storage ACLs land.
func (*Manager) SaveDraft ¶ added in v0.48.0
func (h *Manager) SaveDraft(w http.ResponseWriter, r *http.Request)
SaveDraft answers POST /api/files/drafts/{key}/save: the draft goes where it is meant to go, and stops being a draft.
Nothing is ever replaced. When the name is free the file lands there; when something already has it the answer is 409 TARGET_TAKEN with `suggested`, the `name (2).ext` beside it (ops.UniqueDestNumbered — the numbering the New document dialog suggests), and nothing moves until the person agrees and the client asks again with `as`.
The file is MOVED, not copied: its catalogue row keeps its id, so an editor still open on it — the document server's session above all, which posts its final save by node id seconds after the editor closes — writes into the saved document rather than into a draft that is gone.
func (*Manager) ServeLinkedFile ¶ added in v0.45.0
func (h *Manager) ServeLinkedFile(w http.ResponseWriter, r *http.Request, s *model.Storage, rel string)
ServeLinkedFile streams one file for a credential-free download link (the one-file tickets of archive_download.go / download_link.go).
⚠ The link's OWNER is already on the request context. The ≥viewer check in streamBody is therefore the owner's reach NOW — at the drop — and not the reach they had when the link was minted; a grant revoked in between refuses the download. That is the property that lets the redeem carry no credential.
func (*Manager) Stat ¶
func (h *Manager) Stat(w http.ResponseWriter, r *http.Request)
Stat returns metadata for a single node.
Query: ?id=<id>
func (*Manager) SyncCopyAcross ¶
func (h *Manager) SyncCopyAcross(ctx context.Context, srcStorageID int64, src string, dstStorageID int64, dst string)
SyncCopy inserts a DB node for a freshly written copy, cloning the source node's type/size/mime. Idempotent: a node already at dst is left alone (a later background sync would reconcile anyway). SyncCopyAcross mirrors a copy whose ends live in two different storages — the cross-depo paste. The node is created under the DESTINATION storage, with the type/size/mime read from the source storage's node when there is one; there need not be (a folder nobody has browsed yet has no row), so the driver's own Stat is the fallback rather than a reason to skip the mirror. Skipping it is what leaves a pasted file invisible until the next scan.
func (*Manager) SyncHardDelete ¶
SyncHardDelete drops the rows of an item whose bytes are gone for good: the driver could not trash them and deleted them outright, they were already missing, or a move across storages took them away. Mirrors vfDelete's branches for the same three cases (dropGoneRows).
⚠⚠ Issue #104: it used to soft-delete the one row WHERE IT STOOD. That row had no bytes in `.filex-trash/`, so it was a trash entry nothing could restore (issue #74's shape, written by filex itself), and for a folder only the folder's row was touched: its contents stayed live under a deleted parent, still counted, still found by search, until a scan noticed.
func (*Manager) SyncMove ¶
SyncMove updates the moved node's path/parent. Delegates to the same helper vfMove uses.
func (*Manager) SyncRename ¶ added in v0.47.0
SyncRename implements ops.RenameSync: vfRename's catalogue side for a queued rename (finishRename).
func (*Manager) SyncSoftDelete ¶
SyncSoftDelete flags the node deleted and retags it to the trash path, preserving the original path in storage_key so trash.Service.Restore can move the bytes back. Mirrors vfDelete's soft-delete DB branch.
type Meta ¶
type Meta struct {
Store db.Store
// ACL is the RBAC resolver the folder listing consults for its per-row
// `perm`. Optional — nil means no enforcement and no `perm` on the wire,
// exactly as the folder listing behaves with the resolver unwired.
ACL *acl.Resolver
// ThumbSigner stamps the `thumb_url` these rows hand out, as the folder
// listing's does (thumb_url.go). Nil emits it unsigned.
ThumbSigner *thumb.Signer
// ThumbRefresh draws in the background what these rows found missing or
// stale (hydrateThumbs).
ThumbRefresh ThumbRefresher
}
Meta hosts the tags / star / recent endpoints.
func (*Meta) AttachACL ¶
AttachACL wires the RBAC/ACL resolver so every starred / recent / tag row carries the caller's effective level on it.
func (*Meta) AttachThumbSigner ¶
AttachThumbSigner wires the thumbnail URL stamp.
func (*Meta) GetTags ¶
func (h *Meta) GetTags(w http.ResponseWriter, r *http.Request)
GetTags lists the tags for one node (?node_id=) or the tags in use in a storage (?storage_id=), as the caller can see them. Exactly one of the two.
Both answers keep the pre-v0.43 `tags: []string` and add `items` (name + kind). The node answer also says `can_edit_team`, so the picker offers "team" only where saving it would succeed.
func (*Meta) ListAllTags ¶
func (h *Meta) ListAllTags(w http.ResponseWriter, r *http.Request)
ListAllTags lists every tag the caller can see, both kinds, alphabetical. Powers the navigation panel's Tags section and the Tagged files page.
func (*Meta) ListRecent ¶
func (h *Meta) ListRecent(w http.ResponseWriter, r *http.Request)
ListRecent returns nodes the current user opened recently (newest-first).
func (*Meta) ListStarred ¶
func (h *Meta) ListStarred(w http.ResponseWriter, r *http.Request)
ListStarred returns the user's starred nodes (newest-first by star time).
func (*Meta) SetRecent ¶
func (h *Meta) SetRecent(w http.ResponseWriter, r *http.Request)
SetRecent records that the current user opened a node.
func (*Meta) SetStar ¶
func (h *Meta) SetStar(w http.ResponseWriter, r *http.Request)
SetStar toggles the starred flag for the current user on a node.
func (*Meta) SetTags ¶
func (h *Meta) SetTags(w http.ResponseWriter, r *http.Request)
SetTags sets the tags on a file that the caller can see: their personal tags and their tenant's team tags (see tagOps.set).
func (*Meta) TaggedNodes ¶
func (h *Meta) TaggedNodes(w http.ResponseWriter, r *http.Request)
TaggedNodes lists the live files carrying a tag (?tag=…), newest-first, capped by ?limit=. ?kind=personal|team narrows to one kind; absent = both (a `#.tag~x` link from before v0.43 keeps working). Empty tag → 400.
type NFSExports ¶
type NFSExports struct {
Store db.Store
Auth *protocolauth.Resolver
// Enabled mirrors the FILEX_NFS kill switch.
Enabled bool
// Host and Port are what a mount line needs.
Host string
Port int
}
NFSExports is the self-service export handler.
func NewNFSExports ¶
func NewNFSExports(store db.Store, res *protocolauth.Resolver, enabled bool, host string, port int) *NFSExports
NewNFSExports constructs the handler.
func (*NFSExports) Create ¶
func (h *NFSExports) Create(w http.ResponseWriter, r *http.Request)
Create mints an export and returns its path exactly once.
func (*NFSExports) Delete ¶
func (h *NFSExports) Delete(w http.ResponseWriter, r *http.Request)
Delete revokes an export.
⚠ It takes effect on the mount's next request rather than by tearing the mount down: NFS has no session to end — the client holds a file handle and keeps sending RPCs, and there is no connection the server owns. So the live mount is MARKED revoked (protocolauth.Kick) and every operation on it starts refusing from that moment. The client sees an access error and its mount goes stale, which is the honest end state and worth saying in the UI.
func (*NFSExports) List ¶
func (h *NFSExports) List(w http.ResponseWriter, r *http.Request)
List returns the caller's own exports. The paths are NOT among them and cannot be: only their hashes are stored.
func (*NFSExports) SetState ¶
func (h *NFSExports) SetState(w http.ResponseWriter, r *http.Request)
SetState disables or re-enables an export.
type Notifications ¶
type Notifications struct {
Service notify.Service
// Store and ACL answer "may this member see the file a broadcast names" —
// with the same grants the explorer filters its listings by. With either
// one nil there is nothing to prove it with, and no such row reaches a
// member.
Store db.Store
ACL *acl.Resolver
}
Notifications wraps the notify.Service for the HTTP layer.
func NewNotifications ¶
NewNotifications constructs the handler.
func (*Notifications) AdminList ¶
func (h *Notifications) AdminList(w http.ResponseWriter, r *http.Request)
AdminList exposes the global view (broadcasts + every user's notifications) to admins.
GET /admin/api/notifications?unread=true&limit=50&offset=0
func (*Notifications) AdminTest ¶
func (h *Notifications) AdminTest(w http.ResponseWriter, r *http.Request)
AdminTest emits a manual event so the admin can verify the webhook URL works.
POST /admin/api/notifications/test
func (*Notifications) AdminUpdateWebhookConfig ¶
func (h *Notifications) AdminUpdateWebhookConfig(w http.ResponseWriter, r *http.Request)
AdminUpdateWebhookConfig overrides the URL/token at runtime. Pass empty url to disable webhook delivery without taking the in-app channel down. token may be left empty to keep the existing one (sentinel: empty in body means clear; supply "__keep__" to retain).
PATCH /admin/api/notifications/webhook-config
body: {url: "...", token: "..." or "__keep__" or ""}
func (*Notifications) AdminWebhookConfig ¶
func (h *Notifications) AdminWebhookConfig(w http.ResponseWriter, r *http.Request)
AdminWebhookConfig returns the current webhook URL + a token-set flag (the token itself is never exposed back to the UI).
GET /admin/api/notifications/webhook-config
func (*Notifications) GetSettings ¶
func (h *Notifications) GetSettings(w http.ResponseWriter, r *http.Request)
GetSettings returns the user's preferences.
GET /api/notifications/settings
func (*Notifications) List ¶
func (h *Notifications) List(w http.ResponseWriter, r *http.Request)
List paginates the current user's bell history (broadcasts + user-scoped). Admin-global view is exposed via /admin/api/notifications (passing nil userID to the service).
GET /api/notifications?unread=true&limit=50&offset=0
It is also what the browser notification and the desktop toast read (the web store diffs this list; the desktop polls `unread=true`), so what a reader may learn of a broadcast is decided here once for all three.
func (*Notifications) MarkAllRead ¶
func (h *Notifications) MarkAllRead(w http.ResponseWriter, r *http.Request)
MarkAllRead clears the user's unread queue.
POST /api/notifications/read-all
func (*Notifications) MarkRead ¶
func (h *Notifications) MarkRead(w http.ResponseWriter, r *http.Request)
MarkRead marks a single notification read.
POST /api/notifications/{id}/read
func (*Notifications) UnreadCount ¶
func (h *Notifications) UnreadCount(w http.ResponseWriter, r *http.Request)
UnreadCount returns the bell badge number for the current user.
GET /api/notifications/unread-count
The badge has to agree with the list, and a bare SQL COUNT cannot where the broadcasts get a per-row pass: it has no way to look inside meta_json, let alone at the caller's grants. So it is the list's own total (bellRead) — a badge saying 3 over a list of 1 is both a hint at how much is happening where the caller cannot look and a number they can never clear, because they cannot mark read what they cannot see.
func (*Notifications) UpdateSettings ¶
func (h *Notifications) UpdateSettings(w http.ResponseWriter, r *http.Request)
UpdateSettings replaces the user's preferences.
PATCH /api/notifications/settings
body: {in_app_enabled: bool, muted_events: ["replica_fail", ...]}
type OnlyOffice ¶
type OnlyOffice struct {
Service *onlyoffice.Service
Store db.Store
StorageResolver func(int64) (storage.Driver, error)
ACL *acl.Resolver
// Body resolves where the document's bytes are: the driver, or filex's
// staging area while a staged upload is still transferring. Nil-safe.
Body *filebody.Resolver
}
OnlyOffice exposes the editor config + fetch + callback endpoints.
func NewOnlyOffice ¶
func NewOnlyOffice(svc *onlyoffice.Service, store db.Store, resolver func(int64) (storage.Driver, error)) *OnlyOffice
NewOnlyOffice constructs the handler.
⚠ The handler is ALWAYS wired now, even when nothing is configured yet: what "configured" means is a question for the `external_services` row at request time, not for whatever env carried at boot. Every entry point asks Service.EnabledCtx, which is nil-safe, so a nil service still answers 503.
func (*OnlyOffice) AttachACL ¶
func (h *OnlyOffice) AttachACL(r *acl.Resolver)
AttachACL wires the RBAC resolver: opening a document needs ≥viewer; an editable config additionally needs ≥editor (else it's downgraded to view).
func (*OnlyOffice) AttachBody ¶
func (h *OnlyOffice) AttachBody(b *filebody.Resolver)
AttachBody wires the byte-source resolver so a document that is still being transferred opens in the editor.
func (*OnlyOffice) Callback ¶
func (h *OnlyOffice) Callback(w http.ResponseWriter, r *http.Request)
Callback receives save events from the OnlyOffice document server.
POST /api/files/onlyoffice/callback?node=<id>
Public — relies on the JWT in the body or Authorization header for integrity.
func (*OnlyOffice) Config ¶
func (h *OnlyOffice) Config(w http.ResponseWriter, r *http.Request)
Config returns the editor descriptor for an iframe to render.
Accepts both forms:
GET /api/files/onlyoffice/config?id=<node-id>&lang=tr
POST /api/files/onlyoffice/config { "path": "adapter://rel", "mode": "edit"|"view" }
The GET form is what the standalone Editor.vue route hands the SFC's PreviewModal when the embedder passes a numeric node id. The POST form is what the modal itself sends from inside the explore page — it has the adapter-qualified path handy but not the node id, so the handler must resolve path → node before continuing.
func (*OnlyOffice) Diagnose ¶ added in v0.50.0
func (h *OnlyOffice) Diagnose(w http.ResponseWriter, r *http.Request)
Diagnose answers the editor's question after "Download failed": did the document server ask filex for this document since it was opened, and what did filex answer? (onlyoffice.Service.Diagnose, issue #80.)
GET /api/files/onlyoffice/diagnose?path=<adapter://rel> GET /api/files/onlyoffice/diagnose?id=<node-id>
⚠ Only for a person who may open the document: the same tenant, root and viewer checks the editor configuration asks, with the same 404 for a document the caller cannot see. The answer names no path and no storage, only when, what status, and why.
It is answered from this process's memory; with several replicas each one knows only the requests it served (docs/ONLYOFFICE.md).
No MCP tool mirrors it on purpose: it explains a failure of the in-browser editor, which an agent never opens. An agent that needs the same facts reads the server log, where every refusal is written with its reason.
func (*OnlyOffice) Fetch ¶
func (h *OnlyOffice) Fetch(w http.ResponseWriter, r *http.Request)
Fetch streams document bytes back to the OnlyOffice document server.
Public: no session required, but the URL must be HMAC-signed via the onlyoffice service.
GET /api/files/onlyoffice/fetch?n=<id>&exp=<unix>&sig=<b64url>
Every refusal here is logged with its reason. The only person who ever sees this endpoint fail is an operator reading "Download failed" in the editor — a message from the document server that says nothing about which of the five things went wrong. Without a line naming the reason, the status code in the access log is all they have, and a 500 from a bad signature and a 500 from an unreachable bucket look identical (issue #17).
type OpenWith ¶ added in v0.50.0
OpenWith serves the account's "open with" choices.
func NewOpenWith ¶ added in v0.50.0
NewOpenWith constructs the handler.
func (*OpenWith) Clear ¶ added in v0.50.0
func (h *OpenWith) Clear(w http.ResponseWriter, r *http.Request)
Clear drops every choice. The account row keeps an empty set, so nothing is adopted again from a surface document that still carries an old copy.
func (*OpenWith) Forget ¶ added in v0.50.0
func (h *OpenWith) Forget(w http.ResponseWriter, r *http.Request)
Forget drops one choice: the kind opens with what the administrator put first again.
func (*OpenWith) Get ¶ added in v0.50.0
func (h *OpenWith) Get(w http.ResponseWriter, r *http.Request)
Get answers the caller's choices.
func (*OpenWith) Put ¶ added in v0.50.0
func (h *OpenWith) Put(w http.ResponseWriter, r *http.Request)
Put keeps one choice: the handler that opens the kind for this person.
⚠ Not checked against what is installed or on now: a choice is kept when its app is switched off and used again when it is back (lib/appViewer decides what opens a file, from what is on for the surface it runs on).
type Ops ¶
type Ops struct {
Service *ops.Service
Store db.Store // for path → storage_id resolution in the per-verb endpoints
ACL *acl.Resolver
// E2EPolicy is who may encrypt (e2e_policy_gate.go): a move under a name
// of the caller's choosing that lands on a key file's or a `.fxe`'s name
// asks it (refuseE2E), unless it is free: a folder, a `.fxe` that stays a
// `.fxe`, a key file that stays its own folder's. A COPY that makes a new
// encrypted item where it lands asks it too, whatever its name: a `.fxe`,
// a key file, a folder that holds either (refuseE2ECopy). nil: not wired,
// allowed.
E2EPolicy *e2epolicy.Service
// StorageResolver is how refuseE2E tells a file from a folder at a
// source. nil: every source counts as a file.
StorageResolver func(int64) (storage.Driver, error)
}
Ops handles async copy/move/delete tasks.
State is persisted in the pending_ops table — restart-safe so a crash doesn't lose in-flight work. The actual execution happens in the worker goroutine launched in server.New (see ops.Service.Run).
func (*Ops) AttachACL ¶
AttachACL wires the RBAC resolver so async copy/move/delete require ≥editor on their sources (and destination) at submit time — the async worker itself runs without a user, so authorization is a submit-time gate.
func (*Ops) Cancel ¶
func (o *Ops) Cancel(w http.ResponseWriter, r *http.Request)
Cancel ends a pending or running op the caller may see: their own, or any in reach for an administrator (opsViewer). A row already finished answers 409 FINISHED, and a running one that finishes what it starts (a rename, a restore, a purge) 409 NOT_CANCELLABLE; an unknown id, another tenant's or another person's, 404, in the same words Status uses so the id range cannot be probed.
func (*Ops) List ¶
func (o *Ops) List(w http.ResponseWriter, r *http.Request)
List returns ops filtered by ?status=… (e.g. "running"). Used by the SPA's PendingOpsTray which polls every 2 s. Empty status returns the most-recent rows across all statuses (capped at 200 service-side).
Response shape mirrors what the SPA's `opsApi.list` already understands: `{ "ops": [Op, …] }`, each row with its sources cut to a preview and counted (opListRow). The frontend's `normalizeOp` adapter then translates the backend's raw shape into the SPA's `PendingOp` contract.
func (*Ops) Status ¶
func (o *Ops) Status(w http.ResponseWriter, r *http.Request)
Status returns the live or final state of a submitted op.
func (*Ops) Submit ¶
func (o *Ops) Submit(w http.ResponseWriter, r *http.Request)
Submit queues a new op and returns the opID.
func (*Ops) SubmitCopy ¶
func (o *Ops) SubmitCopy(w http.ResponseWriter, r *http.Request)
func (*Ops) SubmitDelete ¶
func (o *Ops) SubmitDelete(w http.ResponseWriter, r *http.Request)
func (*Ops) SubmitMove ¶
func (o *Ops) SubmitMove(w http.ResponseWriter, r *http.Request)
type PermissionsAdmin ¶ added in v0.49.0
type PermissionsAdmin struct {
Store db.Store
ACL *acl.Resolver
// AppPermissions lists the installed apps' user permissions
// (wasmplugin.Registry.UserPermissions); nil when apps are off.
AppPermissions func() []wasmplugin.UserPermRow
// Notify tells administrators about a role a save left without a
// permission carved out of one it allows (permission_gaps.go); nil when
// notifications are off.
Notify notify.Service
}
PermissionsAdmin serves /api/admin/roles* and /api/admin/users/{id}/{exceptions,roles}.
func NewPermissionsAdmin ¶ added in v0.49.0
func NewPermissionsAdmin(store db.Store, resolver *acl.Resolver) *PermissionsAdmin
NewPermissionsAdmin constructs the handler.
func (*PermissionsAdmin) Catalogue ¶ added in v0.49.0
func (h *PermissionsAdmin) Catalogue(w http.ResponseWriter, _ *http.Request)
Catalogue lists every permission and preset.
GET /api/admin/roles/catalogue
func (*PermissionsAdmin) CreateRule ¶ added in v0.49.0
func (h *PermissionsAdmin) CreateRule(w http.ResponseWriter, r *http.Request)
CreateRule adds a rule. names / descriptions are the role in other interface languages ({"tr": "…"}); name stays required and is the fallback.
POST /api/admin/roles {name, description, names, descriptions, enabled, permissions, targets, effects, settings, conditions}
func (*PermissionsAdmin) DeleteRule ¶ added in v0.49.0
func (h *PermissionsAdmin) DeleteRule(w http.ResponseWriter, r *http.Request)
DeleteRule removes a role. A role people hold is deleted only when the caller says what those people become — ?to=user, ?to=viewer or ?to=<role id> — so deleting a role that takes things away never quietly hands its people the whole built-in User role. Without it: 409 with the count.
DELETE /api/admin/roles/{id}[?to=user|viewer|<id>]
func (*PermissionsAdmin) DismissGap ¶ added in v0.52.0
func (h *PermissionsAdmin) DismissGap(w http.ResponseWriter, r *http.Request)
DismissGap records that a role lacks the permission on purpose: it is not shown again while it stays that way.
POST /api/admin/roles/gaps/dismiss {"id": "role:12:files.encrypt"} → {"gaps": […]}
func (*PermissionsAdmin) GetDefaults ¶ added in v0.49.0
func (h *PermissionsAdmin) GetDefaults(w http.ResponseWriter, r *http.Request)
GetDefaults returns what a built-in role's accounts start from — the User role's, or with ?role=viewer the Viewer role's.
GET /api/admin/roles/builtin[?role=viewer]
func (*PermissionsAdmin) GetUserPermissions ¶ added in v0.49.0
func (h *PermissionsAdmin) GetUserPermissions(w http.ResponseWriter, r *http.Request)
GetUserPermissions returns an account's overrides and effective permissions.
GET /api/admin/users/{id}/exceptions
func (*PermissionsAdmin) GetUserRoles ¶ added in v0.49.0
func (h *PermissionsAdmin) GetUserRoles(w http.ResponseWriter, r *http.Request)
GetUserRoles returns the one custom role the account holds, or null.
GET /api/admin/users/{id}/roles → {"role_id": 3} | {"role_id": null}
func (*PermissionsAdmin) ListGaps ¶ added in v0.52.0
func (h *PermissionsAdmin) ListGaps(w http.ResponseWriter, r *http.Request)
ListGaps returns the roles the caller may edit that allow adding files but not encrypting, and nobody said were on purpose.
GET /api/admin/roles/gaps → {"gaps": [{id, key, from, role | rule_id, rule_name}]}
func (*PermissionsAdmin) ListOverrides ¶ added in v0.49.0
func (h *PermissionsAdmin) ListOverrides(w http.ResponseWriter, r *http.Request)
ListOverrides returns every account that has overrides, keyed by user id, so the Users list can mark "Custom" rows without a request per row. A tenant administrator sees its own tenant's accounts only.
GET /api/admin/roles/exceptions → {"overrides":{"12":{"files.delete":"deny"}}}
func (*PermissionsAdmin) ListRules ¶ added in v0.49.0
func (h *PermissionsAdmin) ListRules(w http.ResponseWriter, r *http.Request)
ListRules returns the rules the caller's tenant may see, in id order.
GET /api/admin/roles
func (*PermissionsAdmin) MyPermissions ¶ added in v0.49.0
func (h *PermissionsAdmin) MyPermissions(w http.ResponseWriter, r *http.Request)
MyPermissions returns the caller's own effective permissions — what the file manager reads to hide the buttons that would only answer 403.
GET /api/auth/me/permissions
func (*PermissionsAdmin) PreviewRole ¶ added in v0.50.0
func (h *PermissionsAdmin) PreviewRole(w http.ResponseWriter, r *http.Request)
PreviewRole answers what a custom role being edited comes to before it is saved: the built-in role its people would be on, "user" or "viewer" (perm.HolderRole). The role editor asks it for each app permission's "Default", so the rule lives here alone. The body is a role body as the editor has it — permissions, effects, conditions; the rest is ignored — possibly unfinished: nothing is checked or stored.
POST /api/admin/roles/preview {permissions, effects, conditions} → {holder_role}
func (*PermissionsAdmin) PutDefaults ¶ added in v0.49.0
func (h *PermissionsAdmin) PutDefaults(w http.ResponseWriter, r *http.Request)
PutDefaults replaces the defaults.
PUT /api/admin/roles/builtin {"permissions":["files.download",…]}
⚠ Supertenant-only in multi-tenant mode. Each built-in role is ONE instance-wide row (perm.SaveRoleBase), held by every account of every tenant that has no custom role — so a tenant admin writing it would narrow or widen every other tenant's people, up to making them all delegated administrators (admin.* with a session). A tenant's own roles are custom roles, which carry its provider_id (CreateRule). Reading stays open.
func (*PermissionsAdmin) PutUserPermissions ¶ added in v0.49.0
func (h *PermissionsAdmin) PutUserPermissions(w http.ResponseWriter, r *http.Request)
PutUserPermissions replaces an account's overrides ({} clears them).
PUT /api/admin/users/{id}/exceptions {"overrides":{"files.delete":"deny"}}
func (*PermissionsAdmin) PutUserRoles ¶ added in v0.49.0
func (h *PermissionsAdmin) PutUserRoles(w http.ResponseWriter, r *http.Request)
PutUserRoles sets the account's ONE role, in one call: a custom role (role_id), no custom role (role_id null), or a built-in role (role). A custom role also sets the built-in level it implies (perm.HolderRole: User when it can change or share anything, Viewer otherwise); a built-in role ends the custom one. An administrator given a custom role stops being one — never the last administrator. A delegated administrator never touches an administrator, never lifts an account above their own role, and gives only roles that allow nothing they do not hold (refuseAdminTarget and below).
PUT /api/admin/users/{id}/roles {"role_id": 3} | {"role_id": null} | {"role": "viewer"}
func (*PermissionsAdmin) RestoreGap ¶ added in v0.52.0
func (h *PermissionsAdmin) RestoreGap(w http.ResponseWriter, r *http.Request)
RestoreGap gives a role the permission it lacks: the built-in role's list the key, a custom role's folder part an Allow of it. Nothing else changes.
POST /api/admin/roles/gaps/restore {"id": "builtin:user:files.encrypt"} → {"gaps": […]}
func (*PermissionsAdmin) UpdateRule ¶ added in v0.49.0
func (h *PermissionsAdmin) UpdateRule(w http.ResponseWriter, r *http.Request)
UpdateRule replaces a rule's fields.
PUT /api/admin/roles/{id}
type PluginRequests ¶ added in v0.49.0
type PluginRequests struct {
Svc *pluginreq.Service
// Assoc and Apps draw an app request's File types group and write the
// approving administrator's choices (0.50, docs/APP-PLUGINS.md → Default
// apps): the same rows the install wizard shows, and for an upgrade only
// the kinds it adds. Nil (app plugins off): no group, nothing placed.
Assoc *assoc.Service
Apps *wasmplugin.Registry
}
PluginRequests is the handler set.
func NewPluginRequests ¶ added in v0.49.0
func NewPluginRequests(svc *pluginreq.Service) *PluginRequests
NewPluginRequests constructs the handler.
func (*PluginRequests) Approve ¶ added in v0.49.0
func (h *PluginRequests) Approve(w http.ResponseWriter, r *http.Request)
Approve installs what the request froze: session only.
func (*PluginRequests) Create ¶ added in v0.49.0
func (h *PluginRequests) Create(w http.ResponseWriter, r *http.Request)
Create leaves a request: POST /api/admin/plugin-requests. 201 with the new request, or 200 with the pending one for the same source.
func (*PluginRequests) Get ¶ added in v0.49.0
func (h *PluginRequests) Get(w http.ResponseWriter, r *http.Request)
Get answers one request with its frozen manifest and review.
func (*PluginRequests) List ¶ added in v0.49.0
func (h *PluginRequests) List(w http.ResponseWriter, r *http.Request)
List answers the requests: GET /api/admin/plugin-requests[?status=pending| approved|rejected|expired|superseded|all]. Pending by default.
func (*PluginRequests) Reject ¶ added in v0.49.0
func (h *PluginRequests) Reject(w http.ResponseWriter, r *http.Request)
Reject closes a request without installing anything: session only.
type Plugins ¶
type Plugins struct {
Manager *plugin.Manager
// MultiTenant is kept because the constructor is called with it from two
// places and the value is genuinely about this handler, but the gate no
// longer reads it: requireSupertenant decides from the request's tenant
// scope, which is attached only in multi-tenant mode, so the flag and the
// scope can never disagree.
MultiTenant bool
}
Plugins is the handler set. Manager may be nil when the subsystem is disabled by configuration; every route then answers 503 with the reason.
func NewPlugins ¶
NewPlugins constructs the handler.
func (*Plugins) CheckUpdates ¶ added in v0.48.0
func (h *Plugins) CheckUpdates(w http.ResponseWriter, r *http.Request)
CheckUpdates reads every binary plugin's source now and answers the list redrawn with the report. It installs nothing.
func (*Plugins) Install ¶
func (h *Plugins) Install(w http.ResponseWriter, r *http.Request)
Install accepts three shapes; the Content-Type decides which.
func (*Plugins) Logs ¶ added in v0.50.0
func (h *Plugins) Logs(w http.ResponseWriter, r *http.Request)
Logs answers a storage plugin's log - its starts and failures, and the storage sync's answers it could not make sense of (an entry it could not say exists or not, issue #104) - the same shape as an app plugin's (`?after=<seq>` → `{lines, next}`, internal/pluginlog). A line repeated comes back with the same `id` and a higher `count`.
type Protection ¶
Protection handles GET/PATCH /api/admin/protection.
func NewProtection ¶
func NewProtection(store db.Store) *Protection
NewProtection constructs the handler.
func (*Protection) Get ¶
func (h *Protection) Get(w http.ResponseWriter, r *http.Request)
Get returns the current protection settings + antivirus status.
func (*Protection) Patch ¶
func (h *Protection) Patch(w http.ResponseWriter, r *http.Request)
Patch updates one or both settings and echoes the fresh GET shape.
type Providers ¶
type Providers struct {
Store db.Store
MultiTenant bool
// DemoMode marks a public playground: every write is refused here too.
// api.DemoGuard already refuses writes under /api/admin and /api/ai/admin,
// but the admin MCP tools call this handler in-process and never pass that
// middleware, so the handler holds its own lock.
DemoMode bool
}
Providers handles /api/admin/providers — the tenant lifecycle API (docs/MULTI-TENANCY.md §8, §11): create/suspend/delete tenants, link storages, transfer the supertenant flag.
Guards enforced here (not scattered):
- only a supertenant admin may manage providers in multi-tenant mode;
- at most ONE supertenant — setting the flag on another provider is a TRANSFER (the old supertenant becomes a regular tenant atomically);
- the supertenant cannot be deleted, disabled, or directly un-flagged;
- deleting a tenant with users requires ?force=1 and then cascades its users and storage LINKS. Storage rows and file data are never touched — unlinking is reversible, deleting files is not;
- a tenant's REALM is given when it is created (the slug by default) and never changes: an update that names another realm is refused (realm_immutable), and the store never writes the column again.
func NewProviders ¶
NewProviders constructs the handler.
func (*Providers) Create ¶
func (h *Providers) Create(w http.ResponseWriter, r *http.Request)
Create provisions a tenant.
func (*Providers) Delete ¶
func (h *Providers) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a tenant. Users require ?force=1 (then cascade); storage LINKS are removed but storage rows + file data are never touched.
func (*Providers) Get ¶ added in v0.50.0
func (h *Providers) Get(w http.ResponseWriter, r *http.Request)
Get returns one provider (tenant) with its storage links and user count.
func (*Providers) LinkStorage ¶
func (h *Providers) LinkStorage(w http.ResponseWriter, r *http.Request)
LinkStorage links a storage to the tenant (POST {storage_id}).
func (*Providers) List ¶
func (h *Providers) List(w http.ResponseWriter, r *http.Request)
List returns every provider (tenant) with its storage links + user count.
func (*Providers) RealmSuggestion ¶ added in v0.50.0
func (h *Providers) RealmSuggestion(w http.ResponseWriter, r *http.Request)
RealmSuggestion answers GET /api/admin/providers/realm-suggestion?slug= with the realm the tenant screen offers for a tenant being created (tenant.SuggestRealm), the first variant of it that is valid, not reserved and not taken: {realm, base, available}. `realm` is "" when the slug holds nothing realm-shaped. Nothing is reserved by asking: the realm is settled by POST, and a second tenant created in between is refused there (`realm_taken`).
⚠ One rule for every client: the screen and the MCP tool ask here rather than each carrying its own transliteration.
func (*Providers) UnlinkStorage ¶
func (h *Providers) UnlinkStorage(w http.ResponseWriter, r *http.Request)
UnlinkStorage removes a storage link (never the storage itself).
type PublicAPI ¶
type PublicAPI struct {
// ACL, when wired, closes a link whose creator may no longer make it
// (link_creator.go). Nil in tests built by hand: every link answers.
ACL *acl.Resolver
Store db.Store
Service *share.Service
// Brand answers /api/public/branding. Nil-safe: unwired yields the stock
// filex identity, which is what an instance that never opened the
// Corporate identity page has anyway.
Brand *BrandingSource
// Apps answers the app-plugin half. Nil (or app plugins disabled) turns
// every /event and /file call into a 404 and leaves /s/{token} answering
// about the share alone.
Apps *AppPlugins
// Drop reuses the file-request ingest so the JSON upload path and the
// no-JS form path are the same code — limits, quota, notifications and
// all.
Drop *Drop
// DefaultLocale is the instance's fallback language, reported in
// /branding so the shell can pick one before a person has.
DefaultLocale string
}
PublicAPI serves /api/public/*.
func NewPublicAPI ¶
NewPublicAPI constructs the handler.
func (*PublicAPI) AttachACL ¶ added in v0.49.0
AttachACL wires the permission resolver for linkCreatorAllows.
func (*PublicAPI) AttachApps ¶
func (h *PublicAPI) AttachApps(a *AppPlugins)
AttachApps wires the app-plugin handler (nil = app pages unavailable).
func (*PublicAPI) AttachBranding ¶
func (h *PublicAPI) AttachBranding(b *BrandingSource)
AttachBranding wires the shared branding source.
func (*PublicAPI) AttachDrop ¶
AttachDrop wires the file-request ingest.
func (*PublicAPI) AttachLocale ¶
AttachLocale sets the instance's fallback language.
func (*PublicAPI) Branding ¶
func (h *PublicAPI) Branding(w http.ResponseWriter, r *http.Request)
Branding answers the public shell's identity. Unauthenticated and cacheable — it is fetched by every public page and changes when an administrator says so, not per visitor.
func (*PublicAPI) DropState ¶
func (h *PublicAPI) DropState(w http.ResponseWriter, r *http.Request)
DropState answers a file-request link's state.
func (*PublicAPI) DropUpload ¶
func (h *PublicAPI) DropUpload(w http.ResponseWriter, r *http.Request)
DropUpload receives the files. The body, the limits, the quota accounting and the owner's notification are the file-request handler's — there is one ingest path, and the JSON screen does not get a second one.
func (*PublicAPI) Entries ¶
func (h *PublicAPI) Entries(w http.ResponseWriter, r *http.Request)
Entries lists one directory of a shared FOLDER: GET /api/public/s/{token}/entries?path=<relative>
A folder share that can only be downloaded whole is not a folder share — the no-JS page has browsed since the beginning, and the shell needs the same thing as data. The path is relative to the share's root and cannot leave it; the PIN gate, the expiry and the visit ceiling are the share's own, checked before anything is read.
func (*PublicAPI) Event ¶
func (h *PublicAPI) Event(w http.ResponseWriter, r *http.Request)
Event runs the app's page_event for an unlocked visitor. `GET`-shaped opens are POSTs with `{"event":"open"}` — one endpoint, because the opening surface and every later one are the same call to the app.
func (*PublicAPI) File ¶
func (h *PublicAPI) File(w http.ResponseWriter, r *http.Request)
File serves a copy an app page exposed (`pub:N`), Range-capable.
func (*PublicAPI) PIN ¶
func (h *PublicAPI) PIN(w http.ResponseWriter, r *http.Request)
PIN answers the PIN and, on a hit, mints the unlock cookie.
func (*PublicAPI) Share ¶
func (h *PublicAPI) Share(w http.ResponseWriter, r *http.Request)
Share answers a share link's state.
func (*PublicAPI) UILocale ¶
func (h *PublicAPI) UILocale(w http.ResponseWriter, r *http.Request)
UILocale answers one added language's strings. Unauthenticated for the same reason /branding is — a public page is offered the language too — and cacheable for the same minute. 404 when no running app ships the tag, so a language that left with its app stops being served at once rather than lingering as a table nothing lists.
type PublicApp ¶
type PublicApp struct {
Plugin string `json:"plugin"`
Page string `json:"page"`
Title wire.Text `json:"title,omitempty"`
Files []wire.FileRef `json:"files"`
}
PublicApp describes the app surface a link carries, once it is unlocked.
type PublicBranding ¶
type PublicBranding struct {
Name string `json:"name"`
LogoURL string `json:"logo_url"`
Accent string `json:"accent"`
HidePoweredBy bool `json:"hide_powered_by"`
// Theme is the appearance a visitor gets before they choose one:
// "system" (follow the device), "light" or "dark". Settings key
// `branding.theme`; anything else is read as "system".
Theme string `json:"theme"`
// Locale is the instance's own default language, and Locales are the
// languages the public pages can be rendered in.
Locale string `json:"locale"`
Locales []string `json:"locales"`
// UILocales are the languages installed apps add to filex itself — one
// row each (tag, the app that brought it, whether it is right to left),
// and NO strings. The strings of one language are GET
// /api/public/ui-locales/{code}, fetched when somebody picks it.
//
// ⚠⚠ This field used to be `{tag: {key: text}}` — every string of every
// added language, on the one answer every public page and every panel
// start reads — while the browser read it as a LIST (`ui_locales?.length`)
// and so never saw a single string: an installed language joined the
// picker and then spoke English (measured 2026-09-21). It is a list now,
// the same PublicLocaleOption rows the browser types, and the wire fixture
// the web tests read is written by this package (public_wire_test.go).
UILocales []wasmplugin.UILocaleInfo `json:"ui_locales,omitempty"`
}
PublicBranding is what the public shell needs to look like this instance rather than like filex. It is the SAME record the admin "Corporate identity" page writes — whitelabel means whitelabel: a signature request from a renamed instance does not say "filex".
⚠ Deliberately NOT the same payload as /api/branding: that one also carries the SSO button label, which a stranger has no business receiving.
⚠ It no longer differs by the custom stylesheet, and the sentence that said so was left standing for a while after it stopped being true. Since v0.43.0 the sheet is on NEITHER payload: it is served from GET /api/me/custom-css behind authentication, and the field was REMOVED from /api/branding rather than blanked, so an old client fails loudly instead of quietly rendering nothing. A comment that describes a retired boundary is worse than no comment, because the next person reasons from it.
type PublicDrop ¶
type PublicDrop struct {
Kind string `json:"kind"` // always "drop"
NeedsPIN bool `json:"needs_pin"`
Unlocked bool `json:"unlocked"`
Expired bool `json:"expired"`
Revoked bool `json:"revoked"`
Locked bool `json:"locked"`
// Folder is the destination's NAME — never its path, and never a listing.
Folder string `json:"folder,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
UploadsLeft *int `json:"uploads_left"`
Limits dropLimits `json:"limits"`
}
PublicDrop is GET /api/public/d/{token}: everything the upload screen needs and nothing about what is already in the folder ("blind drop").
type PublicEntry ¶
type PublicEntry struct {
Name string `json:"name"`
Path string `json:"path"` // relative to the share's root
IsDir bool `json:"is_dir"`
Size int64 `json:"size,omitempty"`
Mime string `json:"mime,omitempty"`
}
PublicEntry is one row of a shared folder: what a visitor needs to decide whether to open it, and nothing about where it lives on the server.
type PublicNode ¶
type PublicNode struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mime string `json:"mime,omitempty"`
}
PublicNode is the little a visitor learns about the file behind a link: what it is called, how big it is and what kind of thing it is. Never its path, never its storage, never who owns it.
type PublicShare ¶
type PublicShare struct {
// entry the storage could not answer for (#104). The link is alive and
// may answer again; until then the page says so instead of offering a
// download that would fail. Told once the PIN gate is open, like the node.
Unavailable bool `json:"unavailable,omitempty"`
// Locked reports the PIN gate being shut after five wrong answers. It is
// NOT a reason the link is dead — it lifts by itself.
// Node is the file or folder behind the link, once unlocked. ⚠ Omitted
// for an app link: there the node is the ANCHOR the app's state and its
// follow-up job hang on, and the only bytes a visitor may have are the
// copies the app exposed. No storage driver is opened for an anonymous
// request, which is the property the v2 app pages had and this keeps.
}
PublicShare is GET /api/public/s/{token}.
`expired` and `revoked` are both reported because they are different sentences to a visitor, and they mean exactly this:
- expired — the clock ran out (`expires_at` has passed). An administrator's Revoke is expressed by moving the expiry to the moment it happened, so it lands here too; there is no second column and one that could disagree with this would be worse than one word doing both.
- revoked — the link is dead for a reason that is NOT the clock: its visit/download ceiling is spent, or the app that answers it has been stopped or removed.
A live link has both false. A caller that only wants "can I use this" reads `expired || revoked`.
type PublicUILocale ¶
PublicUILocale is GET /api/public/ui-locales/{code}: the strings of one language the installed apps add, merged, by filex's own keys.
type Queue ¶
type Queue struct {
Driver queue.Driver
// Store names what a job is about (see subjectOf). Nil leaves the
// payload as the only description.
Store db.Store
}
Queue exposes admin endpoints over the persistent queue driver.
Only admin role hits these — wired under /api/admin/queue/... in routes.go behind auth.Middleware + auth.RequireAdmin.
func NewQueue ¶
NewQueue constructs the handler. driver may be nil when the bootstrap disabled the queue (config.Queue.Enabled = false) — handlers respond 503 in that case so the UI can render a graceful warning.
func (*Queue) AttachStore ¶
AttachStore wires the store the list names its jobs' subjects from.
func (*Queue) Cancel ¶
func (h *Queue) Cancel(w http.ResponseWriter, r *http.Request)
Cancel marks a pending op cancelled.
DELETE /api/admin/queue/{id}
func (*Queue) Get ¶
func (h *Queue) Get(w http.ResponseWriter, r *http.Request)
Get returns a single op.
GET /api/admin/queue/{id}
func (*Queue) List ¶
func (h *Queue) List(w http.ResponseWriter, r *http.Request)
List paginates ops with optional status filter.
GET /api/admin/queue?status=pending&limit=50&offset=0
type QueueStats ¶
QueueStats is the one question the dashboard asks the job queue: how many jobs are waiting or running. queue.Driver satisfies it.
type Quota ¶
type Quota struct {
Service *quota.Service
// Store answers the tenancy question the quota service cannot: the
// admin routes take a raw user id, and in multi-tenant mode a tenant
// admin must not read or clamp another tenant's user. quota.Service
// knows about bytes, not about who owns the account.
Store db.Store
// ACL answers the other tenancy question, the per-user one: which
// storages this caller may see at all. Wired by AttachACL (quota_storages.go);
// nil means no RBAC enforcement, as everywhere else.
ACL *acl.Resolver
// Lazy says when a storage's catalogue — and so its usage figure — covers
// only part of it (docs/LAZY-CATALOGUE.md). nil: a figure is a figure.
Lazy LazyCatalogue
// contains filtered or unexported fields
}
Quota wires quota HTTP routes.
func (*Quota) AdminGet ¶
func (h *Quota) AdminGet(w http.ResponseWriter, r *http.Request)
AdminGet returns the target user's current quota + usage snapshot. (The admin routes bind {user_id}; reading "id" here was a long-standing mismatch that made these endpoints always 400 — fixed alongside.)
func (*Quota) AdminRecompute ¶
func (h *Quota) AdminRecompute(w http.ResponseWriter, r *http.Request)
AdminRecompute rescans nodes owned by id and rewrites usage_bytes.
func (*Quota) AdminSet ¶
func (h *Quota) AdminSet(w http.ResponseWriter, r *http.Request)
AdminSet writes a new quota_bytes for the target user.
func (*Quota) AttachACL ¶
AttachACL wires the RBAC resolver. nil (ACL unwired) means no grant filtering, matching every other file handler — but see StorageUsage: an unwired resolver is the single-tenant "no RBAC configured" install, where every enabled storage is already everybody's.
func (*Quota) AttachLazy ¶ added in v0.44.2
func (h *Quota) AttachLazy(l LazyCatalogue)
AttachLazy wires the catalogue-coverage source.
func (*Quota) Me ¶
func (h *Quota) Me(w http.ResponseWriter, r *http.Request)
Me returns the caller's quota snapshot.
func (*Quota) StorageUsage ¶
func (h *Quota) StorageUsage(w http.ResponseWriter, r *http.Request)
StorageUsage answers per-storage usage for the signed-in caller.
⚠⚠ The filter is the SAME one the explorer's own root listing applies (handlers/manager.go → listVuefinder): list the enabled storages, then drop every one whose `acl.Set` is not `StorageVisible()`. It has to be the same one, because the guarantee is not "this endpoint is careful" — it is "the set of drives you can measure is exactly the set of drives you can open". Two independently-written filters would be two chances for one of them to answer for a drive the other hides.
Two dimensions are already closed before this code runs and are named here so the next reader does not add a second, weaker copy of them:
- Tenancy. h.Store is the tenant-scoped store (server.go → tenantstore), and ListEnabledStorages is one of the methods it confines. A tenant admin never sees another provider's storages in the list at all.
- Root confinement. A token carrying `root:<adapter>://<rel>` (or a trusted proxy's X-Filex-Root) is not looking at a storage, it is looking at one folder inside one — so a whole-storage total is not its number. Rather than invent a subtree total nothing else in the product reports, such a caller is answered with the storage it is confined to ONLY when the confinement is the storage root, and an empty list otherwise.
⚠ What this figure is NOT: it is the drive's total, not the caller's share of it, and item-level grants do not narrow it. Somebody granted one folder on an RBAC storage learns how full that storage is. That is deliberate — the card says "this drive is X full", which is a property of the drive, and the alternative (per-grant subtotals) would make the same card mean a different thing for every reader. The line that must not move is the storage one: a drive you cannot open is not reported at all.
type Replica ¶
type Replica struct {
Store db.Store
Service *replica.Service
Cron *replica.CronScheduler
Reloader *replica.RulesReloader
}
Replica wires the admin endpoints over the replica subsystem.
func NewReplica ¶
func NewReplica(store db.Store, svc *replica.Service, cron *replica.CronScheduler, reloader *replica.RulesReloader) *Replica
NewReplica constructs a handler. Components may be nil when replica is disabled — endpoints return 503 in that case.
func (*Replica) CountFailures ¶
func (h *Replica) CountFailures(w http.ResponseWriter, r *http.Request)
CountFailures returns the unresolved failure count.
GET /admin/api/replica/failures/count
func (*Replica) CreateRule ¶
func (h *Replica) CreateRule(w http.ResponseWriter, r *http.Request)
CreateRule inserts a new rule.
POST /admin/api/replica/rules
func (*Replica) DeleteRule ¶
func (h *Replica) DeleteRule(w http.ResponseWriter, r *http.Request)
DeleteRule removes a rule.
DELETE /admin/api/replica/rules/{id}
func (*Replica) FixAll ¶
func (h *Replica) FixAll(w http.ResponseWriter, r *http.Request)
FixAll enqueues retry ops for every unresolved failure.
POST /admin/api/replica/fix
func (*Replica) FixOne ¶
func (h *Replica) FixOne(w http.ResponseWriter, r *http.Request)
FixOne enqueues a single retry.
POST /admin/api/replica/fix-one
body: {path: "...", op: "write|delete|move|copy"}
func (*Replica) GetReport ¶
func (h *Replica) GetReport(w http.ResponseWriter, r *http.Request)
GetReport returns the latest singleton status report. nil → 204.
GET /admin/api/replica/report
func (*Replica) GetSettings ¶
func (h *Replica) GetSettings(w http.ResponseWriter, r *http.Request)
GetSettings returns the singleton replica_settings row.
GET /admin/api/replica/settings
func (*Replica) ListFailures ¶
func (h *Replica) ListFailures(w http.ResponseWriter, r *http.Request)
ListFailures paginates the failures table.
GET /admin/api/replica/failures?unresolved=true&limit=50&offset=0
func (*Replica) ListRules ¶
func (h *Replica) ListRules(w http.ResponseWriter, r *http.Request)
ListRules paginates rules.
GET /admin/api/replica/rules
func (*Replica) RunReportNow ¶
func (h *Replica) RunReportNow(w http.ResponseWriter, r *http.Request)
RunReportNow triggers GenerateReport synchronously.
POST /admin/api/replica/report/run-now
func (*Replica) UpdateRule ¶
func (h *Replica) UpdateRule(w http.ResponseWriter, r *http.Request)
UpdateRule replaces a rule by id.
PATCH /admin/api/replica/rules/{id}
func (*Replica) UpdateSettings ¶
func (h *Replica) UpdateSettings(w http.ResponseWriter, r *http.Request)
UpdateSettings rewrites the singleton replica_settings row and reloads the cron schedule + rules engine.
PATCH /admin/api/replica/settings
body: {report_cron, report_enabled, default_mode}
type ReplicationTargets ¶
func NewReplicationTargets ¶
func NewReplicationTargets(store db.Store) *ReplicationTargets
func (*ReplicationTargets) Create ¶
func (h *ReplicationTargets) Create(w http.ResponseWriter, r *http.Request)
func (*ReplicationTargets) Delete ¶
func (h *ReplicationTargets) Delete(w http.ResponseWriter, r *http.Request)
func (*ReplicationTargets) Get ¶
func (h *ReplicationTargets) Get(w http.ResponseWriter, r *http.Request)
func (*ReplicationTargets) List ¶
func (h *ReplicationTargets) List(w http.ResponseWriter, r *http.Request)
func (*ReplicationTargets) Update ¶
func (h *ReplicationTargets) Update(w http.ResponseWriter, r *http.Request)
type Response ¶
type Response struct {
Storages []StorageSummary `json:"storages"`
TotalUsers int64 `json:"total_users"`
ActiveSessions int64 `json:"active_sessions"`
QueueDepth int `json:"queue_depth"`
TotalFiles int64 `json:"total_files"`
TotalBytes int64 `json:"total_bytes"`
RecentActivity []ActivityRow `json:"recent_activity"`
Capabilities CapabilitiesShort `json:"capabilities"`
}
Response is the shape returned to the admin UI.
type S3Keys ¶
type S3Keys struct {
Store db.Store
Auth *protocolauth.Resolver
// Endpoint is the S3 URL to hand back with a freshly minted key, so the
// UI can render a working `aws configure` / rclone block instead of a
// placeholder the user has to fill in. Empty = the UI falls back to the
// page origin.
//
// ⚠ It is the ENDPOINT, not the application URL: with a dedicated host
// they differ, and a client pointed at the application root reaches the web
// app. See s3api.EndpointURL.
Endpoint string
// Enabled mirrors the FILEX_S3 kill switch, so the UI can say "your
// operator has turned this off" instead of handing out a key for an
// endpoint that answers 404.
Enabled bool
// PathStyle is true when clients must be told to force path-style
// addressing (no dedicated domain, so `bucket.host` does not resolve).
PathStyle bool
}
S3Keys is the self-service access-key handler.
func NewS3Keys ¶
func NewS3Keys(store db.Store, res *protocolauth.Resolver, cfg config.S3Config, publicURL string) *S3Keys
NewS3Keys constructs the handler.
func (*S3Keys) Create ¶
func (h *S3Keys) Create(w http.ResponseWriter, r *http.Request)
Create mints a key and returns the secret exactly once.
func (*S3Keys) Delete ¶
func (h *S3Keys) Delete(w http.ResponseWriter, r *http.Request)
Delete revokes a key permanently.
type SSHKeys ¶
type SSHKeys struct {
Store db.Store
// Auth is the shared credential resolver. Held only so revoking a key can
// reach an SFTP session that key already opened — without it, deleting a
// key means "cannot log in again", not "is logged out".
Auth *protocolauth.Resolver
// Enabled mirrors the FILEX_SFTP kill switch, so the UI can say the
// endpoint is off instead of collecting keys for a port that is closed.
Enabled bool
// Addr and Host are what a client needs to connect, echoed back so the
// instructions name this deployment rather than a placeholder.
Host string
Port int
// FTPS is the other non-HTTP endpoint's shape.
FTPS FTPSFacts
}
SSHKeys is the self-service SSH key handler.
It also reports the FTPS endpoint, because the two are one screen for the user: "how do I reach this server from a program that is not a browser?". Splitting the facts across two endpoints would mean the guide had to make two calls to answer one question.
func NewSSHKeys ¶
func NewSSHKeys(store db.Store, res *protocolauth.Resolver, enabled bool, host string, port int, ftps FTPSFacts) *SSHKeys
NewSSHKeys constructs the handler.
func (*SSHKeys) Create ¶
func (h *SSHKeys) Create(w http.ResponseWriter, r *http.Request)
Create registers a key.
func (*SSHKeys) Delete ¶
func (h *SSHKeys) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a key.
type SanitizedCSS ¶
type SanitizedCSS struct {
// Hoisted holds the top-level `@font-face` and `@keyframes` blocks, which
// are emitted OUTSIDE the scope wrapper.
//
// ⚠ They have to be hoisted or they stop working: both at-rules define a
// NAME rather than matching elements, and their behaviour inside `@scope`
// is at best inconsistent across engines. Hoisting is safe precisely
// because the url() pass has already run — a hoisted `@font-face` can only
// reference a `data:` URI, so moving it out of the scope cannot give it a
// network request it did not already lack.
Hoisted string
// Scoped holds everything else, to be wrapped in the `@scope` rule.
Scoped string
// Removed names what the pass took out, so the admin screen can say so
// instead of silently serving something other than what was typed.
Removed []string
}
SanitizedCSS is the result of a guard pass over an operator stylesheet.
func SanitizeCustomCSS ¶
func SanitizeCustomCSS(raw string) (SanitizedCSS, error)
SanitizeCustomCSS runs the guard pass over an operator stylesheet.
It is a small CSS-aware scanner rather than a set of regexes, and it has to be: a regex for `url\(` cannot tell the one in a comment from the one in a declaration, and a regex that counts braces cannot tell the `}` inside a quoted string from the one that closes a rule. Both differences are load-bearing — the brace count is what stops a sheet from ending the `@scope` wrapper early and escaping the guard, which is the single most important thing this function does.
The pass:
- comments are COPIED THROUGH but never scanned inside (an operator's notes are theirs, and nothing in a comment is fetched or matched)
- quoted strings are copied verbatim, and braces inside them do not count
- `@import` is removed, at-rule and all
- `url(…)` and `image-set(…)` that are not `data:` or `#fragment` are made inert
- top-level `@font-face` / `@keyframes` are hoisted out of the scope
- the sheet is refused if its braces do not balance, if it contains `</style`, or if it is over the byte cap
func (SanitizedCSS) Empty ¶
func (s SanitizedCSS) Empty() bool
Empty reports whether there is nothing to serve.
func (SanitizedCSS) Render ¶
func (s SanitizedCSS) Render() string
Render produces the text injected into the browser's <style> element.
type SaveText ¶
type SaveText struct {
Store db.Store
StorageResolver func(int64) (storage.Driver, error)
Versions VersionSnapshotter
ACL *acl.Resolver
// Index keeps the saved text searchable. Optional; nil skips indexing.
Index *search.Index
// E2EPolicy is who may encrypt (e2e_policy_gate.go): a save that creates
// an encrypted folder's key file asks it. nil = not wired.
E2EPolicy *e2epolicy.Service
// Thumbs draws the saved file again. ⚠ Before 0.50 nothing did: an SVG
// edited in the built-in editor kept the picture of its first version.
Thumbs ThumbPipeline
}
SaveText handles plain-text edits from the SFC's code/markdown viewer.
func NewSaveText ¶
NewSaveText constructs the handler.
func (*SaveText) AttachACL ¶
AttachACL wires the RBAC resolver so save-text requires ≥editor on the file.
func (*SaveText) AttachSearchIndex ¶
AttachSearchIndex wires the search index. ⚠ Without it an edit saved from the built-in editor never reaches Bleve: the file keeps whatever text it had when it was last indexed, so a content search finds the OLD words and misses the new ones — and a name search only appears to work because the search endpoint falls back to a SQL LIKE over node rows.
func (*SaveText) AttachThumbs ¶ added in v0.50.0
func (h *SaveText) AttachThumbs(p ThumbPipeline)
AttachThumbs wires the thumbnail pipeline (see Thumbs).
func (*SaveText) AttachVersions ¶
func (h *SaveText) AttachVersions(v VersionSnapshotter)
AttachVersions wires the versioning service so save-text snapshots the previous content before writing. Without it edits silently overwrite history (the SFC's "Sürüm geçmişi" / Versions page would show no entries even after multiple saves).
func (*SaveText) Save ¶
func (h *SaveText) Save(w http.ResponseWriter, r *http.Request)
Save writes `content` as the new body of the addressed file.
Body shape mirrors brf-mono's `FilesController::saveText`. We do NOT version on save — the SFC's preview re-renders against the new bytes immediately, and a future versions endpoint can snapshot the previous payload before the write if requested.
type Search ¶
Search handles /api/files/search.
func (*Search) AttachACL ¶
AttachACL wires the RBAC resolver so search results are filtered to the paths the caller may see (prevents cross-user enumeration via search).
func (*Search) Search ¶
func (h *Search) Search(w http.ResponseWriter, r *http.Request)
Search returns up to N matching nodes.
Strategy: try Bleve first; on miss/empty, fall back to SQL LIKE on the `nodes.name` column.
Accepts both POST {query, storage_id, limit} (canonical) and GET ?q=…&storage_id=…&limit=… (admin SPA's toolbar search). The GET form lets the SFC degrade gracefully when the embedder hasn't wired the POST flow.
type SearchAdmin ¶
SearchAdmin holds the Bleve admin actions.
There is deliberately no rebuild lock here. The index owns that guard, because filex also rebuilds on its own when it finds an index written by an older document schema, and a handler-local flag could only ever see the rebuilds this handler started — the two would happily run at once and race to swap their replacement index in.
func NewSearchAdmin ¶
func NewSearchAdmin(idx *search.Index, store db.Store) *SearchAdmin
NewSearchAdmin constructs the handler.
func (*SearchAdmin) Rebuild ¶
func (h *SearchAdmin) Rebuild(w http.ResponseWriter, r *http.Request)
Rebuild reindexes every node row into a replacement index and swaps it in when it is complete. Search keeps answering from the existing index for the whole rebuild, and extracted text is carried across, so this is no longer the destructive operation it was before v0.30.
?content=1 additionally re-enqueues content extraction for every eligible node once the new index is live — for an operator who has just added an extractor or raised FILEX_SEARCH_CONTENT_MAX and wants the text derived again rather than copied.
func (*SearchAdmin) Stats ¶
func (h *SearchAdmin) Stats(w http.ResponseWriter, r *http.Request)
Stats returns index document counts and size.
type SelfTokens ¶
type SelfTokens struct {
// contains filtered or unexported fields
}
SelfTokens is the self-service API-token surface at /api/tokens. Any authenticated user (including non-admin user/viewer accounts) may mint tokens bound to THEMSELVES — capped so a token can never exceed the creator's account-role ceiling nor their own file grants. Admins get the richer /api/admin/ai-tokens surface; this is the safe capped subset for everyone.
func NewSelfTokens ¶
func NewSelfTokens(store db.Store, resolver *acl.Resolver, pauth *protocolauth.Resolver) *SelfTokens
NewSelfTokens constructs the self-service token handler.
func (*SelfTokens) Create ¶
func (h *SelfTokens) Create(w http.ResponseWriter, r *http.Request)
Create mints a token bound to the caller with capped scopes.
func (*SelfTokens) Delete ¶
func (h *SelfTokens) Delete(w http.ResponseWriter, r *http.Request)
Delete revokes one of the caller's own tokens (ownership enforced).
func (*SelfTokens) List ¶
func (h *SelfTokens) List(w http.ResponseWriter, r *http.Request)
List returns the caller's own tokens (never any secret).
func (*SelfTokens) Update ¶
func (h *SelfTokens) Update(w http.ResponseWriter, r *http.Request)
Update edits one of the caller's own tokens (label / usernames; ownership enforced — the credential and scopes stay immutable here).
PATCH /api/tokens/{id}
type Settings ¶
type Settings struct {
Store db.Store
Mailer *mailer.Service
// Branding is invalidated on branding.* writes so public pages pick the
// new identity up immediately (wiring:e1). Nil-safe.
Branding *BrandingSource
// Appearance is invalidated on ui.default_theme writes, so an operator who
// sets the instance default sees it take effect on the next page load
// rather than up to fifteen seconds later (tema:v1). Nil-safe.
//
// ⚠ Two caches, not one: branding is keyed per HOST and overlaid per
// tenant, while the appearance payload is a single instance-wide answer.
Appearance *AppearanceSource
// LoginGuard holds the sign-in security settings (`login.*`) in memory; a
// write of one of them here makes it read them again, so the generic
// settings API cannot leave the limiter deciding with an old value.
// Nil-safe.
LoginGuard *loginguard.Guard
// DemoMode marks a public playground: the addresses in the sign-in
// security settings (the allow-list, the trusted proxies) are masked in
// what this page reads back (demo_redact.go maskSettingsForDemo).
DemoMode bool
}
Settings handles /api/admin/settings.
func NewSettings ¶
NewSettings constructs a Settings handler.
func (*Settings) AttachAppearance ¶
func (h *Settings) AttachAppearance(a *AppearanceSource)
AttachAppearance wires the shared theme source (tema:v1).
func (*Settings) AttachBranding ¶
func (h *Settings) AttachBranding(b *BrandingSource)
AttachBranding wires the shared branding source (wiring:e1).
func (*Settings) AttachMailer ¶
AttachMailer wires the mailer so the SMTP "Test" button can verify / send.
func (*Settings) List ¶
func (h *Settings) List(w http.ResponseWriter, r *http.Request)
List returns all key/value pairs.
func (*Settings) SMTPTest ¶
func (h *Settings) SMTPTest(w http.ResponseWriter, r *http.Request)
SMTPTest verifies the SMTP config (auth handshake) and, when a `to` address is given, sends a real test message end-to-end.
POST /api/admin/settings/smtp-test { "to": "you@example.com" } (to optional)
func (*Settings) Set ¶
func (h *Settings) Set(w http.ResponseWriter, r *http.Request)
Set upserts a single setting.
Body: {"value":"…"}
func (*Settings) Update ¶
func (h *Settings) Update(w http.ResponseWriter, r *http.Request)
Update upserts multiple settings in a single request — the collection-level counterpart of Set. The admin Settings page PATCHes the whole form here; the old API only had GET / + PUT /{key}, so the page's save 405'd.
Body: a flat JSON object {key: value, …}. Non-string values (numbers, bools) are JSON-encoded to text so they round-trip through the string-valued settings store. Returns the redacted settings map after the write.
type Share ¶
type Share struct {
// signature) so downloads don't re-zip the whole folder every time. A nil
// or disabled cache falls back to streaming a fresh zip. See serveFolderZip.
Zip *sharezip.Cache
// from settings (wiring:e1). Nil-safe: unwired = stock filex chrome.
Branding *BrandingSource
// unwired, that page falls back to streaming the ORIGINALS — which is what
// it always did, and what made a folder of photos crawl on open.
Thumbs *thumb.Pipeline
// `default_locale`). The visitor's request wins over it — see publicLocale.
DefaultLocale string
// staging area while a staged upload is still transferring. Nil-safe.
Body *filebody.Resolver
Tenants tenanturl.Resolver
// pages in this file are the no-JS answer behind it. nil (frontend not
// bundled) means every visitor gets the Go pages. See public_shell.go for
// which request gets which.
Shell http.Handler
// an app link falls back to "this needs a browser with JavaScript".
Apps *AppPlugins
// catalogue had not seen yet (catalogueOnDemand). nil-safe.
Index *search.Index
}
Share handles share creation and the public viewer endpoints.
func NewShare ¶
func NewShare(svc *share.Service, store db.Store, resolver func(int64) (storage.Driver, error), publicURL string, zipCache *sharezip.Cache) *Share
NewShare constructs a Share handler. zipCache enables the folder-share ZIP cache (pass a disabled/nil cache to stream fresh on every folder download).
func (*Share) AttachACL ¶
AttachACL wires the RBAC resolver so minting a public share link requires ≥editor on the target node (sharing grants outside access — a write action).
func (*Share) AttachApps ¶
func (h *Share) AttachApps(a *AppPlugins)
AttachApps wires the app-plugin handler for the no-JS app page.
func (*Share) AttachBody ¶
AttachBody wires the byte-source resolver so a share link serves a file that is still being transferred to the backend.
func (*Share) AttachBranding ¶
func (h *Share) AttachBranding(b *BrandingSource)
AttachBranding wires the shared branding source (wiring:e1).
func (*Share) AttachIndex ¶
AttachIndex wires the search index for on-demand cataloguing.
func (*Share) AttachLocale ¶
AttachLocale sets the fallback language for the public pages — used when the visitor's browser asks for neither of the two we ship.
func (*Share) AttachShell ¶
AttachShell wires the SPA document served to JavaScript browsers.
func (*Share) AttachTenants ¶
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*Share) AttachThumbs ¶
AttachThumbs wires the thumbnail pipeline used by the public folder page.
func (*Share) HandleBrowseFile ¶
func (h *Share) HandleBrowseFile(w http.ResponseWriter, r *http.Request)
HandleBrowseFile streams a single file UNDER a shared folder: GET /s/{token}/f/{rel...}. Media endpoint → plain-text errors (the browse page is the human-facing surface).
func (*Share) HandleCreate ¶
func (h *Share) HandleCreate(w http.ResponseWriter, r *http.Request)
HandleCreate mints a new share token.
The SFC's `useFileApi.createShare` posts:
{ path: "<adapter>://<rel>", password: true|false, expires_at: …, max_downloads: … }
and reads `body.share.url` / `body.share.password_pin` afterwards. The legacy embed.js posts `{ node_id, pin, expires_in, … }` and reads the flat fields. We support both.
func (*Share) HandleDelete ¶
func (h *Share) HandleDelete(w http.ResponseWriter, r *http.Request)
HandleDelete revokes a share.
func (*Share) HandleDownload ¶
func (h *Share) HandleDownload(w http.ResponseWriter, r *http.Request)
func (*Share) HandleList ¶
func (h *Share) HandleList(w http.ResponseWriter, r *http.Request)
HandleList returns the current caller's active share links for one item, so the permissions modal's "Existing links" section can list (and revoke) them.
GET /api/files/share?path=<adapter://rel> (or ?node_id=<n>)
Non-admins only see links they created; admins see every link on the item. A path with no indexed node (or no links yet) returns an empty list rather than an error so the modal shows "none" instead of failing. The `uuid` field carries the numeric share id (what DELETE /share/{id} expects), while `url` is built from the token — matching the ShareInfo shape the SFC consumes.
func (*Share) HandleMetadata ¶
func (h *Share) HandleMetadata(w http.ResponseWriter, r *http.Request)
HandleMetadata returns metadata for a share token (no PIN check).
Used by the embed.js viewer to decide whether to render a PIN prompt.
type ShareX ¶
type ShareX struct {
// contains filtered or unexported fields
}
ShareX is the token-authenticated endpoint that lets ShareX (the Windows screenshot / upload tool) push a file, image, or text capture into filex and get back a public, browser-viewable link in one round trip.
It is a thin wrapper over the same aiOps core the /api/ai surface uses, so an upload is stored AND indexed (a plain driver write into an un-indexed folder never creates the DB cache row a share lookup needs), then a public /s/<token> share is minted for it. The reply is the exact JSON ShareX's custom-uploader URL parser expects:
POST /api/sharex/upload (multipart/form-data)
file → the captured bytes (ShareX default FileFormName)
folder → optional target directory (default: "sharex")
→ 200 {"url":"<PublicURL>/s/<token>?inline=1"}
The link carries ?inline=1 so images/text render in the browser instead of forcing a download (see Share.HandleDownload). Every capture is stored under a random-prefixed filename so a fresh, collision-free share is minted each time (re-using a name would silently repoint the previous share at the new bytes).
func NewShareX ¶
func NewShareX(store db.Store, resolver func(int64) (storage.Driver, error), shareSvc *share.Service, publicURL string) *ShareX
NewShareX constructs the ShareX upload handler. publicURL is the base used to build the returned /s/<token> link (empty yields a relative link). shareSvc must be non-nil for sharing to work — it is the same *share.Service the rest of the app uses.
func (*ShareX) AttachACL ¶
AttachACL wires the RBAC resolver so the write + share both enforce the bound user's grants (≥editor on the target), mirroring the AI surface.
func (*ShareX) AttachBody ¶
AttachBody wires the byte-source resolver (staging-aware reads).
func (*ShareX) AttachE2EPolicy ¶ added in v0.51.0
AttachE2EPolicy wires the rule into ShareX captures.
func (*ShareX) AttachSearchIndex ¶
AttachSearchIndex wires the search index into the ops core — a ShareX capture is a write like any other and must be searchable immediately.
func (*ShareX) AttachStaged ¶
func (h *ShareX) AttachStaged(s *StagedUpload)
AttachStaged routes captures above the chunk threshold through filex's staging area — a long screen recording is answered as soon as filex holds it instead of after the driver write (nil = synchronous writes).
func (*ShareX) AttachTenants ¶
AttachTenants wires the shared origin resolver (internal/tenanturl).
func (*ShareX) AttachThumbs ¶
AttachThumbs wires the thumbnail pipeline so captures get grid thumbnails like manager uploads (nil = thumbnails skipped).
type Shared ¶
type Shared struct {
// unsigned URL, which authenticated clients still fetch fine.
ThumbSigner *thumb.Signer
// or stale (hydrateThumbs).
ThumbRefresh ThumbRefresher
}
Shared serves the shared-with-me listing. Mounted inside the authenticated /api/files group, so the session/token middleware and confine.Middleware are already in force.
func (*Shared) AttachThumbSigner ¶
AttachThumbSigner wires the thumbnail URL stamp.
func (*Shared) SharedWithMe ¶
func (h *Shared) SharedWithMe(w http.ResponseWriter, r *http.Request)
SharedWithMe lists the items the caller holds a grant on, newest grant first, paginated with ?limit= (default 100, max 500) and ?offset=.
Four decisions worth stating, because each of them is a place where the obvious implementation is wrong:
A grant on a FOLDER lists the folder itself, not its contents. That is what the explorer needs: the row is a `dir` whose `path` is adapter-qualified, so opening it navigates into the folder through the ordinary listing endpoint (which applies the same grants again). Listing the contents here would flatten the tree and make the row unopenable.
"Does not own" is expressed the way filex actually models ownership. There is no per-node owner: `nodes.owner_id` exists for quota accounting only and is nil for everything a sync discovered. What separates "shared with me" from "mine" is where the access comes from — on an RBAC-off storage every authenticated user already reaches everything by role, so a grant row there is inert and the items are not "shared", they are just the user's files. Hence: RBAC-enabled storages only.
A grant with an empty path_prefix is a whole-storage grant. It is not listed as an item (its name would be empty); it is what makes the storage appear in `storages` below — the "shared drive" that the panel shows in its storage list rather than as a file row.
Tenancy is filtered explicitly, not assumed. tenantstore wraps exactly three methods, and per-grant reads are not among them, so an unfiltered listing here would hand a tenant the names and paths of another tenant's shared folders — the same cross-tenant leak the search handler filters out at handlers/search.go. Filtering only in ListEnabledStorages would be filtering in a place the test harness does not even wrap.
type SharesAdmin ¶
type SharesAdmin struct {
// resolver the share dialog's links use (handlers.Share). Zero value yields
// relative "/s/<token>" links, which every existing test constructing this
// handler by hand gets.
Tenants tenanturl.Resolver
// the app wakes and finds out (appLinkEnded). nil: app plugins are off.
Apps *wasmplugin.Registry
}
SharesAdmin handles /api/admin/shares.
func NewSharesAdmin ¶
func NewSharesAdmin(store db.Store) *SharesAdmin
NewSharesAdmin constructs the handler.
func (*SharesAdmin) AttachApps ¶
func (h *SharesAdmin) AttachApps(reg *wasmplugin.Registry)
AttachApps wires the app-plugin registry (nil = app plugins disabled).
func (*SharesAdmin) AttachTenants ¶
func (h *SharesAdmin) AttachTenants(rv tenanturl.Resolver)
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*SharesAdmin) Delete ¶
func (h *SharesAdmin) Delete(w http.ResponseWriter, r *http.Request)
Delete hard-deletes a share row.
func (*SharesAdmin) List ¶
func (h *SharesAdmin) List(w http.ResponseWriter, r *http.Request)
List returns all shares with optional creator/active filters. Rows carry `plugin_name` / `page_id` when an app plugin opened the link (00046), so the Shares table can show a "plugin / page" column without a lookup per row.
func (*SharesAdmin) ListAppPluginShares ¶
func (h *SharesAdmin) ListAppPluginShares(w http.ResponseWriter, r *http.Request)
ListAppPluginShares is the same rows, narrowed to the links APPS opened, so an app's own admin panel can draw its table (`?plugin=sign`). It is the same query, the same tenant filter and the same envelope as List — there is no second listing to keep in step.
⚠ `?plugin=` takes the app's NAME, not its row id: a panel drawn for an app knows what the app is called and should not have to look its id up first.
func (*SharesAdmin) Revoke ¶
func (h *SharesAdmin) Revoke(w http.ResponseWriter, r *http.Request)
Revoke soft-revokes by setting expiration to now (keeps audit trail).
type SharesMine ¶
type SharesMine struct {
// PIN endpoint answers "cannot be shown" rather than panicking, which is
// also what an instance with no secret key gets.
Service *share.Service
// resolver the share dialog and the admin listing use, never
// window.location (issue #32).
Tenants tenanturl.Resolver
// app plugins are off: every row then lists as a plain share.
Apps *wasmplugin.Registry
}
SharesMine handles the caller's own shares.
func NewSharesMine ¶
func NewSharesMine(store db.Store, svc *share.Service) *SharesMine
NewSharesMine constructs the handler.
func (*SharesMine) AttachApps ¶
func (h *SharesMine) AttachApps(reg *wasmplugin.Registry)
AttachApps wires the app-plugin registry.
func (*SharesMine) AttachTenants ¶
func (h *SharesMine) AttachTenants(rv tenanturl.Resolver)
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*SharesMine) List ¶
func (h *SharesMine) List(w http.ResponseWriter, r *http.Request)
List returns the shares the CALLER created, newest first.
⚠ The creator filter is applied in the STORE (`created_by = me`), not by dropping rows after the fact. A post-filter would have to paginate over everybody's links to fill one person's page, which means the total would be wrong and, worse, that the query the database ran was the admin one.
func (*SharesMine) Pin ¶
func (h *SharesMine) Pin(w http.ResponseWriter, r *http.Request)
Pin hands back ONE link's PIN, in plain, to the person who created it or to an administrator — and to nobody else.
Owner's decision, 2026-09-20: *"paylaşımın sahibi ve admin alabilir şifreyi… Admin değilse göremez, kopyalayamaz."*
The shape of the answer ¶
An authorized caller always gets 200, with a body that either carries the PIN or says in one machine-readable word why it cannot:
{"pin":"834595"}
{"pin":null,"reason":"no_pin"} this link has no PIN
{"pin":null,"reason":"not_recoverable"} minted before 00049 / key rotated
{"pin":null,"reason":"no_secret_key"} this instance has no FILEX_SECRET_KEY
The three refusals are 200 rather than an error status because they are not failures of the request: the caller is allowed, the link exists, and the honest answer is a sentence the screen must show. An error status here would be indistinguishable — to a client's interceptor — from "you may not".
Why every read is audited ¶
A PIN is the second factor on a public link. Reading one is not like reading a row of a table, and the audit row is what makes "the owner and an admin" a statement somebody can CHECK afterwards rather than a claim about code. The row is written whatever the answer was, because an attempt that found nothing is still an attempt.
type StagedUpload ¶
type StagedUpload struct {
Store db.Store
Manager *Manager
Area *staging.Area
Ops *ops.Service
Quota *quota.Service
ACL *acl.Resolver
// ChunkSize is the default part size handed to clients that do not ask.
ChunkSize int64
// TTL is how long an idle staging directory survives the sweeper.
TTL time.Duration
}
StagedUpload is the HTTP surface of the staged upload protocol plus the transfer worker that ops calls back into.
func NewStagedUpload ¶
func NewStagedUpload(store db.Store, mgr *Manager, area *staging.Area, opsSvc *ops.Service, q *quota.Service, chunkSize int64, ttl time.Duration) *StagedUpload
NewStagedUpload constructs the handler. mgr carries the post-write hooks (search index, thumbnails, parent lookup, realtime) so the committed bytes go through exactly the same door as a plain upload rather than a parallel one.
func (*StagedUpload) Abort ¶
func (h *StagedUpload) Abort(w http.ResponseWriter, r *http.Request)
Abort drops the staging directory and the row.
func (*StagedUpload) AttachACL ¶
func (h *StagedUpload) AttachACL(r *acl.Resolver)
AttachACL wires the RBAC resolver. Begin gates on the destination directory through the manager's resolveAdapterDir; every later call re-checks the stored key, because an upload id is not a capability.
func (*StagedUpload) Begin ¶
func (h *StagedUpload) Begin(w http.ResponseWriter, r *http.Request)
Begin reserves a staging directory for a new upload.
func (*StagedUpload) Commit ¶
func (h *StagedUpload) Commit(w http.ResponseWriter, r *http.Request)
Commit verifies the staged bytes, publishes the node immediately as `staged` and hands the actual transfer to a background op.
func (*StagedUpload) CommitUpload ¶
func (h *StagedUpload) CommitUpload(ctx context.Context, uploadID string) error
CommitUpload streams one staged upload into the driver and fires every post-write hook the synchronous path fires. Called by the ops worker with a server-lifetime context — never a request context.
On success: transfer_state → "stored", staging deleted, row deleted. On failure: the row goes to "failed" with the message, the staging directory is KEPT (so a retry costs no bytes) and the node stays "staged".
func (*StagedUpload) IngestStream ¶
func (h *StagedUpload) IngestStream( ctx context.Context, storageID int64, storageKey string, src io.Reader, size int64, userID int64, declaredMime string, ) (*model.Node, error)
IngestStream stages src (exactly size bytes) for storageKey on storageID, publishes the node immediately with transfer_state="staged" and hands the driver write to the ops worker. The returned node is listable before a single byte has reached the backend — that is the point.
A short or over-long body is refused and the staging directory removed: the same rule as a chunk that arrives short, for the same reason.
declaredMime is advisory. As on every other write path the bytes decide, sniffed from staging once they are all there.
func (*StagedUpload) Put ¶
func (h *StagedUpload) Put(w http.ResponseWriter, r *http.Request)
Put stores one chunk. The chunk's position comes from Content-Range, and the part number is derived from it against the grid `begin` handed out — so parts may arrive out of order and a retried chunk simply overwrites itself.
func (*StagedUpload) RecoverUnstored ¶
func (h *StagedUpload) RecoverUnstored(ctx context.Context) int
RecoverUnstored settles the rows a staged upload left "staged" or "failed" although their bytes are on the storage, and reports how many it settled.
transfer() now makes the "stored" flip a condition of releasing staging, but a process that died between the driver write and the flip, or ran a version that released staging regardless, can leave such a row with no session behind it: listed, and unreadable. Nothing else settles it on a storage nobody scans (sync_mode `ondemand`), so this runs once at boot, from RunSweeper, after the first sweep.
A row is settled only on the evidence the storage sync uses: no session references it (a session owns its bytes — in flight, or retryable), and the object at its key has the committed size and is not older than the commit (model.TransferLanded). Anything short of that is left exactly as it is and logged: an error a person can read beats a wrong file served as the right one.
func (*StagedUpload) RunSweeper ¶
func (h *StagedUpload) RunSweeper(ctx context.Context, interval time.Duration)
RunSweeper sweeps once at boot and then on every tick until ctx is done.
func (*StagedUpload) ShouldStage ¶
func (h *StagedUpload) ShouldStage(size int64) bool
ShouldStage is the single decision every whole-body surface asks.
func (*StagedUpload) StagedReady ¶
func (h *StagedUpload) StagedReady() bool
StagedReady reports whether IngestStream can run at all.
func (*StagedUpload) StagedThreshold ¶
func (h *StagedUpload) StagedThreshold() int64
StagedThreshold is the size above which a whole-body upload is staged rather than written straight to the driver. It is the same chunk size the chunked protocol hands out, so "large" means the same thing on every surface.
func (*StagedUpload) Status ¶
func (h *StagedUpload) Status(w http.ResponseWriter, r *http.Request)
Status is the resume oracle: a client that lost its state asks here and continues from `offset`.
func (*StagedUpload) Sweep ¶
func (h *StagedUpload) Sweep(ctx context.Context) (int, int)
Sweep removes staging that has seen no activity for longer than TTL, and the rows that describe it. Returns (rows, orphan directories) removed.
`committing` rows are never swept: their bytes are being read right now. `failed` rows are, once they too have been idle for a full TTL — otherwise a permanently failing upload keeps its bytes forever.
type StorageDrivers ¶
type StorageDrivers struct{}
StorageDrivers serves the driver descriptor catalogue.
func NewStorageDrivers ¶
func NewStorageDrivers() *StorageDrivers
NewStorageDrivers constructs the handler.
func (*StorageDrivers) List ¶
func (h *StorageDrivers) List(w http.ResponseWriter, r *http.Request)
List returns every registered driver's descriptor, driver-name sorted.
A driver a storage may be catalogued lazily on (model.LazyDrivers) also carries `lazy_fields`, the settings of sync_mode `lazy`; the storage form offers that mode only where they are (docs/LAZY-CATALOGUE.md).
type StorageSummary ¶
type StorageSummary struct {
ID int64 `json:"id"`
Name string `json:"name"`
Driver string `json:"driver"`
MountPath string `json:"mount_path"`
Enabled bool `json:"enabled"`
TotalFiles int64 `json:"total_files"`
TotalBytes int64 `json:"total_bytes"`
LastSyncAt any `json:"last_sync_at,omitempty"`
LastSyncStatus string `json:"last_sync_status,omitempty"`
State string `json:"state"` // ok | stale | error | running
}
StorageSummary is a per-storage row in the dashboard response.
type StorageUsageRow ¶
type StorageUsageRow struct {
Name string `json:"name"`
UsedBytes int64 `json:"used_bytes"`
FileCount int64 `json:"file_count"`
// Coverage is set when the figure counts only part of the drive: its
// first scan is still running, or it is catalogued lazily and not all of
// it has been (docs/LAZY-CATALOGUE.md). The card then draws it as a lower
// bound and says why.
Coverage *syncpkg.CatalogueCoverage `json:"coverage,omitempty"`
}
StorageUsageRow is one drive's answer.
type Storages ¶
type Storages struct {
Store db.Store
Worker *syncpkg.Worker
// Plugins and StorageResolver are set when the plugin subsystem is on.
// They exist for ONE reason: a storage saved on a plugin driver is probed
// against its own configuration first (see plugin_gate.go). Nil means the
// subsystem is off and nothing is probed.
Plugins *plugin.Manager
StorageResolver func(int64) (storage.Driver, error)
// ForgetStorage drops the cached driver the resolver built for a storage.
// Set by the server; nil where nothing caches.
ForgetStorage func(int64)
// DemoMode marks a public playground, where "admin" is whoever read the
// credentials off the landing page. See denyOnDemo.
DemoMode bool
}
Storages handles /api/admin/storages.
func NewStorages ¶
NewStorages constructs a Storages handler.
func (*Storages) Create ¶
func (h *Storages) Create(w http.ResponseWriter, r *http.Request)
Create adds a new storage.
func (*Storages) Delete ¶
func (h *Storages) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a storage and its descendant nodes (cascade).
func (*Storages) Get ¶
func (h *Storages) Get(w http.ResponseWriter, r *http.Request)
Get returns a single storage by id. The admin Storages list page fetches /api/admin/storages/{id} when the user clicks a row to open the edit view; without this route chi returned 405.
func (*Storages) List ¶
func (h *Storages) List(w http.ResponseWriter, r *http.Request)
List returns all configured storages. Each entry carries a `stats` blob with the file count + total byte sum so the admin Storages list page can render real "12 files, 4.2 MB" labels instead of static placeholders.
Accepts `?role=primary` / `?role=replica` so the Depolar page can hide replica targets (operators never write to them directly) and the Replikasyon page can list replica candidates separately.
func (*Storages) SetOrder ¶ added in v0.46.0
func (h *Storages) SetOrder(w http.ResponseWriter, r *http.Request)
SetOrder writes the order storages are listed in (issue #57):
PUT /api/admin/storages/order {"ids": [3, 1, 7]}
The list is the WHOLE order as the admin sees it, first = top: the storages it names get positions 1..n and every other storage the caller may administer is un-placed (NULL: after the placed ones, by id). An empty list is "back to the default order". Answers {"ok": true, "ids": [...]}.
"May administer" is what h.Store.ListStorages returns for this request. h.Store is the tenant-scoped store, so for a tenant admin another tenant's storage is not in it: naming one is refused with exactly the words an id that never existed gets, and the other tenant's positions are never written (tenantown.go on why the two must be indistinguishable).
Nothing here reaches outward, so a public demo lets it through.
func (*Storages) TriggerSync ¶
func (h *Storages) TriggerSync(w http.ResponseWriter, r *http.Request)
TriggerSync forces an immediate sync run for a storage — or, with ?path=<folder>, a rescan of that one catalogued folder (rescanFolder).
type StoragesAdmin ¶
type StoragesAdmin struct {
Store db.Store
// Plugins is set when the plugin subsystem is on; Test then probes a
// plugin driver's real capabilities instead of only listing its root.
Plugins *plugin.Manager
}
StoragesAdmin holds extra admin actions on storages.
func NewStoragesAdmin ¶
func NewStoragesAdmin(store db.Store) *StoragesAdmin
NewStoragesAdmin constructs the handler.
func (*StoragesAdmin) Discover ¶
func (h *StoragesAdmin) Discover(w http.ResponseWriter, r *http.Request)
Discover lists the folders directly under the given driver+config's root without saving anything, so the operator can mount several of them as separate storages in one go.
Issue #31: a bucket holding N top-level folders meant filling in the storage form N times, once per folder, because the bucket ROOT is never mounted (storage.ErrRootPathForbidden — filex would otherwise take ownership of everything at the root of a shared namespace). This is how "the whole bucket" is offered instead: the same credentials once, every folder listed, each one ticked becomes its own storage with its own sidebar entry. The probe itself may look at the root — looking is not mounting — and the rows the caller then creates go through Create and its root guard like any other.
POST /api/admin/storages/discover {driver, config}
→ {ok, root_key, folders:[{name, root}]}
Folders whose name starts with "." (filex's own `.filex-trash`, a `.git`, an editor's scratch) are left out: they are nobody's document root.
func (*StoragesAdmin) Drift ¶
func (h *StoragesAdmin) Drift(w http.ResponseWriter, r *http.Request)
Drift returns recent unresolved sync conflicts for a storage.
func (*StoragesAdmin) SyncRuns ¶
func (h *StoragesAdmin) SyncRuns(w http.ResponseWriter, r *http.Request)
SyncRuns returns the recent sync_runs for a single storage.
func (*StoragesAdmin) Test ¶
func (h *StoragesAdmin) Test(w http.ResponseWriter, r *http.Request)
Test connects to the given driver+config without saving and lists the root.
type SyncAdmin ¶
SyncAdmin handles /api/admin/sync-runs.
func NewSyncAdmin ¶
NewSyncAdmin constructs the handler.
type TLSHooks ¶ added in v0.50.0
type TLSHooks struct {
Store db.Store
Box *secretbox.Box
// PublicURL is the platform's own address (FILEX_PUBLIC_URL), always
// certified.
PublicURL string
}
TLSHooks answers the reverse proxy's two questions about certificates (FILEX_TLS_MODE=proxy, docs/TENANT-ADMIN.md, "TLS"):
GET /api/tls/ask?domain=<name> 200: certify it; 404: do not GET /api/tls/certificate?server_name=<name> the tenant's own certificate (PEM), or 204
The first is Caddy's on-demand TLS `ask`: a stranger pointing a name at the platform gets no certificate, because the platform serves only its own address, enabled tenants' addresses, their platform subdomains and their ACTIVE own domains. The second is Caddy's `get_certificate http`: a tenant that brought a certificate for its domain has it served.
⚠ Both answer the proxy ITSELF only (clientip.ProxyItself: a trusted peer, no forwarded-for header): the same proxy forwards every stranger's request to filex, and those must not learn the platform's customers' domains, let alone read a private key. Anyone else gets 404, the answer an unknown name gets.
func (*TLSHooks) Ask ¶ added in v0.50.0
func (h *TLSHooks) Ask(w http.ResponseWriter, r *http.Request)
Ask answers whether the proxy may certify a name.
func (*TLSHooks) Certificate ¶ added in v0.50.0
func (h *TLSHooks) Certificate(w http.ResponseWriter, r *http.Request)
Certificate hands the proxy a tenant's own certificate for a name (the chain, then the key, PEM), or 204 when the tenant brought none: the proxy then issues its own.
type TenantSelf ¶ added in v0.50.0
type TenantSelf struct {
Store db.Store
Live *authsetup.Live
Domains *tenantdomain.Service
MultiTenant bool
DemoMode bool
// TLSMode is FILEX_TLS_MODE, told to the screen (who issues the
// certificate of an own domain that brought none).
TLSMode string
}
TenantSelf handles /api/admin/tenant.
func NewTenantSelf ¶ added in v0.50.0
func NewTenantSelf(store db.Store, live *authsetup.Live, domains *tenantdomain.Service, multiTenant bool) *TenantSelf
NewTenantSelf constructs the handler.
func (*TenantSelf) AddDomain ¶ added in v0.50.0
func (h *TenantSelf) AddDomain(w http.ResponseWriter, r *http.Request)
AddDomain records an own domain for the tenant ({domain}), pending until its CNAME is seen; it is looked at once at once.
func (*TenantSelf) CheckDomain ¶ added in v0.50.0
func (h *TenantSelf) CheckDomain(w http.ResponseWriter, r *http.Request)
CheckDomain looks at a domain's CNAME now.
func (*TenantSelf) CreateProvider ¶ added in v0.50.0
func (h *TenantSelf) CreateProvider(w http.ResponseWriter, r *http.Request)
CreateProvider adds the tenant's own OIDC or LDAP: {driver, label?, slug?, enabled?, config?, confirm_failed_test?}.
func (*TenantSelf) DeleteCertificate ¶ added in v0.50.0
func (h *TenantSelf) DeleteCertificate(w http.ResponseWriter, r *http.Request)
DeleteCertificate removes it: the installation's way issues one again.
func (*TenantSelf) DeleteDomain ¶ added in v0.50.0
func (h *TenantSelf) DeleteDomain(w http.ResponseWriter, r *http.Request)
DeleteDomain removes an own domain from the tenant.
func (*TenantSelf) DeleteProvider ¶ added in v0.50.0
func (h *TenantSelf) DeleteProvider(w http.ResponseWriter, r *http.Request)
DeleteProvider removes one of the tenant's own providers.
func (*TenantSelf) Get ¶ added in v0.50.0
func (h *TenantSelf) Get(w http.ResponseWriter, r *http.Request)
Get answers the tenant's overview.
func (*TenantSelf) SetCertificate ¶ added in v0.50.0
func (h *TenantSelf) SetCertificate(w http.ResponseWriter, r *http.Request)
SetCertificate stores the tenant's own certificate for a domain: {cert_pem, key_pem}. The key is sealed and never sent back.
func (*TenantSelf) SetInsecure ¶ added in v0.50.0
func (h *TenantSelf) SetInsecure(w http.ResponseWriter, r *http.Request)
SetInsecure is the platform operator's per-tenant switch: {allow: bool}. A tenant's administrator is refused (403).
func (*TenantSelf) TestProvider ¶ added in v0.50.0
func (h *TenantSelf) TestProvider(w http.ResponseWriter, r *http.Request)
TestProvider tests one of the tenant's own providers on the form as it stands, through the same guard as its sign-ins.
func (*TenantSelf) UpdateProvider ¶ added in v0.50.0
func (h *TenantSelf) UpdateProvider(w http.ResponseWriter, r *http.Request)
UpdateProvider changes one of the tenant's own providers.
type ThemeExport ¶
type ThemeExport struct {
Filex int `json:"filex_theme"`
Key string `json:"key"`
Name string `json:"name"`
Light map[string]string `json:"light"`
Dark map[string]string `json:"dark"`
}
ThemeExport is the JSON form of a theme — what "Export" downloads and what "Import" reads.
⚠ It carries the KEY, so importing the same file twice replaces rather than duplicates. Carrying a theme to another instance and then carrying a fix over is the ordinary case, and a second "Acme (1)" would be the wrong answer to it; the admin screen names the key it is about to overwrite.
func ExportTheme ¶
func ExportTheme(t *model.CustomTheme) ThemeExport
ExportTheme renders a stored theme as its portable document.
type ThemeWire ¶
type ThemeWire struct {
ID string `json:"id"`
Name string `json:"name"`
Light map[string]string `json:"light"`
Dark map[string]string `json:"dark"`
}
ThemeWire is one palette as the browser receives it — the same shape as a built-in `ThemeDef`, with the id already prefixed.
type Themes ¶
type Themes struct {
Store db.Store
Appearance *AppearanceSource
}
Themes handles /api/admin/themes.
func NewThemes ¶
func NewThemes(store db.Store, src *AppearanceSource) *Themes
NewThemes constructs the admin handler.
func (*Themes) Delete ¶
func (h *Themes) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a theme.
⚠ It does NOT touch anybody's stored preference — see ResolveThemeID. The one thing it does clean is the instance DEFAULT, and only because that is a choice an operator made deliberately: leaving `ui.default_theme` pointing at a deleted theme would resolve correctly but read as a bug in the admin screen, which would show a default nobody can find in the list.
type Thumb ¶
type Thumb struct {
Store db.Store
Pipeline *thumb.Pipeline
// ACL gates the authenticated path. Nil = unwired (tests) and, per the
// aclAllowID contract, allows — the tenancy and confinement checks above it
// still run.
ACL *acl.Resolver
// Signer verifies the URL stamp. Nil is the fail-CLOSED state: unsigned and
// signed URLs alike then need an authenticated caller.
Signer *thumb.Signer
}
Thumb serves cached thumbnail JPEGs.
⚠⚠ Every request is authorized, by one of exactly two proofs:
- a live signature on the URL (`?exp=…&sig=…`), stamped by a listing that had already cleared tenancy + ACL for that node. This is what lets a bare `<img src>` — which carries no Authorization header, and no cookie either when it sits in a third-party embed, because the session cookie is SameSite=Lax — render at all; or
- an authenticated caller (session cookie, bearer/API token) who passes the same tenancy, root-confinement and ACL checks the file's own listing applies. This is the path every in-repo consumer actually takes: the SPA, the desktop app and the embedded explorer all fetch thumbnails through useThumbs, i.e. `fetch()` with credentials and auth headers.
Neither proof → 401. It used to be neither proof → 200: `checkSig` returned true when `sig` was absent, `manager.go` emitted `thumb_url` with no signature, and the signing key was never seeded, so the whole scheme was inert and anyone could walk the dense node-id range and collect the rendered first page of every file on the instance. That was true of single-tenant installs too.
⚠ The public folder-share page does NOT come through here: it serves the same cached artefact via `/s/{token}/f/<path>?thumb=1`, scoped to the share token (see share_browse.go). An anonymous share viewer is therefore unaffected by this gate, and must stay that way.
func (*Thumb) AttachSigner ¶
AttachSigner wires the URL signature verifier.
type ThumbPipeline ¶
ThumbPipeline is the narrow surface manager_mutate needs to fire a thumbnail job after upload. Kept as an interface so the package doesn't have to import `internal/thumb` (which would create an import cycle through the storage resolver wiring).
type ThumbRefresher ¶ added in v0.50.0
ThumbRefresher is what a listing hands a missing or stale thumbnail to (*thumb.Refresher). Consider must never block: it runs inside the listing.
type ThumbRepair ¶ added in v0.50.0
type ThumbRepair struct {
Store db.Store
Ops *ops.Service
// Wait is how long POST watches a fresh run before answering; 0 = 3 s.
// A small folder is done by then and the answer is its result.
Wait time.Duration
// Limits drops the pipeline's cached settings after a change. Nil: the
// new ones apply within the cache's few seconds.
Limits ThumbSettingsForgetter
}
ThumbRepair serves Admin → Tools → Thumbnail repair:
POST /api/admin/tools/thumbnails/repair {path, mode}
GET /api/admin/tools/thumbnails/repair the tenant's latest run
`path` is a qualified path: "photos://2024/summer" (a folder and everything in it), "photos://2024/cover.jpg" (one file), "photos://" (the storage), or "" (every storage the caller reaches). `mode` is "fix" (missing, failed, skipped, stale) or "rebuild" (everything).
The run is an ops job (ops/thumb_repair.go): in the operations tray, cancellable through POST /api/files/ops/{id}/cancel, its tenant's alone, resumed after a restart. The walk it runs is the CLI's (`filex thumb backfill`, server.BackfillThumbs).
⚠ Administrators only, and a tenant administrator only over their tenant's storages: a named storage outside the scope is 404, and "every storage" is the tenant's reach (trash.Reach), never the instance.
func NewThumbRepair ¶ added in v0.50.0
func NewThumbRepair(store db.Store, o *ops.Service) *ThumbRepair
NewThumbRepair constructs the handler.
func (*ThumbRepair) Generators ¶ added in v0.50.0
func (h *ThumbRepair) Generators(w http.ResponseWriter, r *http.Request)
Generators answers GET .../thumbnails/generators: the ready thumbnails in the caller's reach, counted by who drew them, the most first.
func (*ThumbRepair) Problems ¶ added in v0.50.0
func (h *ThumbRepair) Problems(w http.ResponseWriter, r *http.Request)
Problems lists the files whose thumbnail failed or was skipped, with the reason (GET .../thumbnails/problems), inside the caller's reach.
func (*ThumbRepair) Settings ¶ added in v0.50.0
func (h *ThumbRepair) Settings(w http.ResponseWriter, r *http.Request)
Settings answers the thumbnail settings in force (GET .../thumbnails/settings).
func (*ThumbRepair) Start ¶ added in v0.50.0
func (h *ThumbRepair) Start(w http.ResponseWriter, r *http.Request)
Start queues a repair (see the type comment) and watches it for a moment: 200 with the result when it ended within Wait, 202 with its progress when it goes on. 409 BUSY while this tenant already has one going (with that run as `job`); 400 for an unreadable request; 404 for a storage or path the caller cannot see.
func (*ThumbRepair) Status ¶ added in v0.50.0
func (h *ThumbRepair) Status(w http.ResponseWriter, r *http.Request)
Status answers the tenant's latest repair: queued, running, or how it ended. `{"running": false}` alone when it has asked for none.
func (*ThumbRepair) UpdateSettings ¶ added in v0.50.0
func (h *ThumbRepair) UpdateSettings(w http.ResponseWriter, r *http.Request)
UpdateSettings changes them (PATCH .../thumbnails/settings). The instance's setting: a tenant administrator is refused. A value out of range is 400, never clamped (dbsetting.IntSpec.Validate).
type ThumbSettingsForgetter ¶ added in v0.50.0
type ThumbSettingsForgetter interface{ ForgetSettings() }
ThumbSettingsForgetter drops the pipeline's cached settings after a change (*thumb.Pipeline).
type TicketUpload ¶
type TicketUpload struct {
// contains filtered or unexported fields
}
TicketUpload serves PUT/POST /u/{ticket}: the credential-free half of the ticket flow. It is deliberately the thinnest possible surface — one write to one pinned path — and it never lists, reads or reveals anything about the destination.
func NewTicketUpload ¶
func NewTicketUpload(store db.Store, resolver func(int64) (storage.Driver, error), tickets *uploadTicketStore) *TicketUpload
NewTicketUpload builds the public redeem handler over the same ops core the authorized surfaces use, so a ticket upload gets identical staging, thumbnailing, write-hook and DB-cache behaviour.
func (*TicketUpload) AttachACL ¶
func (h *TicketUpload) AttachACL(r *acl.Resolver)
AttachACL wires the RBAC resolver so the write runs under the minter's grants.
func (*TicketUpload) AttachBody ¶
func (h *TicketUpload) AttachBody(b *filebody.Resolver)
AttachBody wires the byte-source resolver.
func (*TicketUpload) AttachE2EPolicy ¶ added in v0.51.0
func (h *TicketUpload) AttachE2EPolicy(s *e2epolicy.Service)
AttachE2EPolicy wires the rule into the ticket redeem (/u/{ticket}): the write runs as the minter, and is judged as theirs.
func (*TicketUpload) AttachSearchIndex ¶
func (h *TicketUpload) AttachSearchIndex(idx *search.Index)
AttachSearchIndex wires the search index. ⚠ Easy to forget, and the symptom is quiet: a ticket upload without it lands a row and a change frame — so the folder listing and every open explorer look right — while the file is findable by name only through the search endpoint's SQL LIKE fallback, and not at all by its content.
func (*TicketUpload) AttachStaged ¶
func (h *TicketUpload) AttachStaged(s *StagedUpload)
AttachStaged routes big transfers through the staging area — the whole point of tickets is big files, so this is the common path, not the exception.
func (*TicketUpload) AttachThumbs ¶
func (h *TicketUpload) AttachThumbs(p *thumb.Pipeline)
AttachThumbs mirrors the manager-upload thumbnail dispatch.
func (*TicketUpload) Upload ¶
func (h *TicketUpload) Upload(w http.ResponseWriter, r *http.Request)
Upload handles PUT /u/{ticket} (raw body, what `curl -T` sends) and POST /u/{ticket} (multipart `file`, what a browser or `curl -F` sends).
type Trash ¶
type Trash struct {
Service *trash.Service
Store db.Store
ACL *acl.Resolver
// Index puts a restored file back into search. Optional; nil skips it.
Index *search.Index
// Ops is the queue "empty the trash now" runs on (ops.SubmitTrashEmpty).
// Without it AdminEmpty answers 503, like every queued operation.
Ops *ops.Service
// EmptyWait is how long AdminEmpty waits for the purge it started before
// answering 202 with the run's progress instead. Zero is defaultEmptyWait.
EmptyWait time.Duration
}
Trash wires trash retention HTTP routes.
func (*Trash) AdminEmpty ¶
func (h *Trash) AdminEmpty(w http.ResponseWriter, r *http.Request)
AdminEmpty starts "empty the trash now" and answers when it is over or when EmptyWait has passed, whichever comes first: 200 with the final counts, or 202 with the run's progress so far — which GET on the same path (EmptyStatus) keeps reporting until the run ends. 409 BUSY while the caller's tenant already has one going (with that run as `job`); 400 for a request it cannot read.
`older_than_days` and `storage_id` arrive in the query or the JSON body (the admin SPA's trashApi.empty posts {storage_id, older_than_days}). 0/missing days wipes everything soft-deleted when it is asked; 0/missing storage is every storage the caller can reach.
⚠⚠ The purge used to run inside this request, and a large trash cannot be purged inside any request. 61,844 files needed the better part of an hour; nginx answered 504 at sixty seconds, the request's context was cancelled, and the purge died mid-batch — while the admin page, whose HTTP client had given up at thirty, showed nothing at all. The run is an ops job now (ops.SubmitTrashEmpty): in the operations centre and the admin tray, cancellable like every op, its tenant's alone, resumed after a restart — and this request only watches it for a moment.
func (*Trash) AttachACL ¶
AttachACL wires the RBAC resolver so the trash list is filtered to nodes the caller may see and restore requires ≥editor on the node's original path.
func (*Trash) AttachSearchIndex ¶
AttachSearchIndex wires the search index. ⚠ Deleting a file removes its document from the index (correctly). Restoring it never put the document back, so a restored file was in the listing, on the storage, and unfindable — and looked findable anyway, because a name search falls back to a SQL LIKE over node rows.
func (*Trash) EmptyStatus ¶
func (h *Trash) EmptyStatus(w http.ResponseWriter, r *http.Request)
EmptyStatus reports the caller's tenant's latest "empty the trash now": queued, running, or how it ended. `{"running": false}` alone when it has asked for none.
func (*Trash) List ¶
func (h *Trash) List(w http.ResponseWriter, r *http.Request)
List returns soft-deleted nodes for the admin trash view.
Query: ?storage_id=…&limit=…&offset=…
⚠⚠ The rows a caller may see are judged one by one after the store has read them (trashListKeep), so the store's pagination and its count describe rows the caller may not see. `total` used to be the page's own length after the filters (and the offset stayed in the store's coordinates): a member shown 12 of the first 25 rows was told there were 12 in all, the client drew no pager, and everything after the first page was unreachable (lesson #413). The walk below reads the caller's trash in store batches, counts every entry the caller may see, and cuts the page at offset/limit in the caller's own coordinates.
func (*Trash) Purge ¶
func (h *Trash) Purge(w http.ResponseWriter, r *http.Request)
Purge hard-deletes a single trashed node by id.
DELETE /api/admin/trash/{id}
func (*Trash) PurgeNode ¶ added in v0.47.0
PurgeNode implements ops.Purger: Purge's work for a queued purge. The entry was judged when it was queued (Purge: ownsNode) on storageID, and one of any other storage is refused (queuedEntry).
An entry that is no longer there at all is DONE, not failed: what a purge is for has happened. That is the shape of a purge requeued at boot after the process died past HardDeleteNode, and of two administrators purging the same entry — both of which ended `failed` with a bare "sql: no rows in result set" on the second run.
func (*Trash) Restore ¶
func (h *Trash) Restore(w http.ResponseWriter, r *http.Request)
Restore lifts the deleted_at flag on a soft-deleted node.
Asked with `queued=1` it takes a batch (`node_ids`), judges every entry the way it judges one, and queues them as a job of the operations queue instead: 202 `{ops}`, one job per storage (restoreQueued).
func (*Trash) RestoreNode ¶ added in v0.47.0
RestoreNode implements ops.Restorer: Restore's work for a queued restore. The bytes and the row come back, and the node is re-indexed, scanned and announced (announceRestore). The entry was judged when it was queued (RestoreQueued) on storageID, and one of any other storage is refused (queuedEntry). A place that is taken is an error in the words Restore answers with, and nothing of that entry moves.
type Update ¶
type Update struct {
// contains filtered or unexported fields
}
Update is the admin-facing surface of the release checker:
GET /api/admin/update → what we know (cached, no network) POST /api/admin/update/check → check now POST /api/admin/update/apply → install the pending release (binary installs)
The split matters: reading the status must never block on a slow or absent network, and applying must never be a side effect of looking.
func NewUpdate ¶
NewUpdate builds the handler. A nil service means the feature is compiled in but disabled by config; the endpoints then answer with a quiet "disabled" status rather than 404, so the UI can explain the state.
func (*Update) Apply ¶
func (h *Update) Apply(w http.ResponseWriter, r *http.Request)
Apply installs the pending release. Refused with 409 when the install cannot replace itself (container, package manager) — a permanent condition the UI turns into instructions, and 409 keeps it distinct from a transient 5xx.
func (*Update) Check ¶
func (h *Update) Check(w http.ResponseWriter, r *http.Request)
Check forces a fetch. Used by the "check now" button; a failure is reported in the payload (check_error) rather than as an HTTP error, because "I could not reach the update server" is information, not a server fault.
type Upload ¶
type Upload struct {
Store db.Store
StorageResolver func(int64) (storage.Driver, error)
Thumbs *thumb.Pipeline
ACL *acl.Resolver
// E2EPolicy is who may encrypt (e2e_policy_gate.go): Init asks it before
// a multipart upload creates a key file or a `.fxe`. nil = not wired.
E2EPolicy *e2epolicy.Service
}
Upload handles browser to S3 multipart uploads.
Flow:
- Init — caller posts {storage_id, path, size}. Backend computes part count, calls MultipartUploader.InitMultipart, persists a chunked_uploads row and returns the per-part presigned URLs.
- Browser uploads each part directly to S3 via the presigned URLs and captures each part's ETag from the response header.
- Finalize — caller posts {upload_id, parts:[{part_number, etag}]}. Backend asks S3 to assemble the parts via CompleteMultipartUpload, then upserts a node row and dispatches a thumbnail job.
- Abort — caller cancels mid-flight; AbortMultipartUpload is called and the chunked_uploads row deleted.
func NewUpload ¶
func NewUpload(store db.Store, resolver func(int64) (storage.Driver, error), pipeline *thumb.Pipeline) *Upload
NewUpload constructs an Upload handler.
pipeline may be nil; in that case node insertion still happens but no thumbnail job is dispatched.
func (*Upload) Abort ¶
func (u *Upload) Abort(w http.ResponseWriter, r *http.Request)
Abort cancels an in-progress upload.
func (*Upload) AttachACL ¶
AttachACL wires the RBAC resolver so chunked uploads require ≥editor on the destination at init time (finalize/abort continue an already-authorized id).
type Usage ¶
Usage serves /api/admin/usage.
Configuration is written through the ordinary settings endpoint (the keys are ordinary settings rows, and `allowSettingWrite` already reserves every non-branding key to the supertenant); this handler only reads.
func (*Usage) Report ¶
func (h *Usage) Report(w http.ResponseWriter, r *http.Request)
Report answers GET /api/admin/usage.
?days=30 the last N days ending today (default 30, max 400) ?from=&to= an explicit range, YYYY-MM-DD, inclusive
⚠ The default range ENDS TODAY even though today's report does not exist yet: a page that silently shifted the window back a day would show a total that does not match the dates on its own axis. The absent day is a note in the payload instead.
type UserPrefs ¶
UserPrefs wires the per-user, per-surface preference routes.
func NewUserPrefs ¶
NewUserPrefs constructs the handler.
func (*UserPrefs) Get ¶
func (h *UserPrefs) Get(w http.ResponseWriter, r *http.Request)
Get returns the caller's document for one surface. A caller with nothing stored gets an empty object rather than a 404: "nothing chosen yet" is the ordinary state of every new account, and a 404 would make the client treat its own first day as a failure.
type Users ¶
type Users struct {
Store db.Store
// ACL resolves per-user permissions, for the delegated administrator's
// lines (refuseGain, refuseTakeover). Unset, a delegated administrator is
// refused those changes; a full administrator never needs it.
ACL *acl.Resolver
// Mailer and Tenants send a new account its invitation (send_invite):
// the address to sign in at and a first password. No mailer, or a send
// that fails: the password comes back once for the administrator.
Mailer *mailer.Service
Tenants tenanturl.Resolver
// DirectoryFor names the LDAP directory that owns an address among the
// ones that sign people in to a tenant (0: the platform's own;
// authsetup.Live.DirectoryFor); nil on a harness with none.
DirectoryFor func(email string, tenant int64) (name, label string, ok bool)
}
Users handles /api/admin/users.
func (*Users) Create ¶
func (h *Users) Create(w http.ResponseWriter, r *http.Request)
Create makes a new user.
func (*Users) Delete ¶
func (h *Users) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a user. The last remaining admin can never be deleted — not even by itself — so the instance can't be locked out of its own admin surface. A non-existent id is reported as 404 rather than a silent 200.
func (*Users) Get ¶
func (h *Users) Get(w http.ResponseWriter, r *http.Request)
Get returns a single user by id. The admin UI's UserEdit page hits this when the row is clicked; without it chi returned 405 (only PATCH/DELETE were wired).
type UsersAdmin ¶
type UsersAdmin struct {
Store db.Store
// ACL resolves per-user permissions for the delegated administrator's
// takeover line (refuseTakeover). Unset, a delegated administrator is
// refused; a full administrator never needs it.
ACL *acl.Resolver
}
UsersAdmin holds admin-only user actions.
func NewUsersAdmin ¶
func NewUsersAdmin(store db.Store) *UsersAdmin
NewUsersAdmin constructs the handler.
func (*UsersAdmin) ResetPassword ¶
func (h *UsersAdmin) ResetPassword(w http.ResponseWriter, r *http.Request)
ResetPassword generates a fresh random password for the user, persists the hash, returns the cleartext password ONCE in the response (admin must save).
type VersionSnapshotter ¶
type VersionSnapshotter interface {
Snapshot(ctx context.Context, nodeID int64) (*model.NodeVersion, error)
}
VersionSnapshotter is the narrow surface save-text needs to capture a snapshot before a destructive write.
type Versions ¶
type Versions struct {
Store db.Store
Service *versioning.Service
// Index keeps the restored content searchable. Optional; nil skips it.
Index *search.Index
// Thumbs draws the restored content. ⚠ Before 0.50 a restore kept the
// picture of the version it replaced.
Thumbs ThumbPipeline
// ACL is the RBAC resolver. Nil = unwired (tests) and, per the aclAllowID
// contract, allows — the existence, tenancy and confinement checks above it
// still run.
ACL *acl.Resolver
}
Versions wraps version-history HTTP routes.
func NewVersions ¶
func NewVersions(store db.Store, svc *versioning.Service) *Versions
NewVersions constructs the handler.
func (*Versions) AttachACL ¶
AttachACL wires the RBAC resolver. ⚠ This handler shipped without one: it was the only file surface in routes.go with no AttachACL call, which is precisely why nobody noticed that a read-only account could roll files back.
func (*Versions) AttachSearchIndex ¶
AttachSearchIndex wires the search index. ⚠ Restoring a version rewrites the file's BYTES at an unchanged path, and nothing else ever revisits a document whose path did not change — so without this the index keeps the text of the version that was just rolled back. Measured: after restoring v1, a content search for a phrase only v2 ever contained still returned the file, and a phrase that IS in the restored file did not.
func (*Versions) HardDelete ¶
func (h *Versions) HardDelete(w http.ResponseWriter, r *http.Request)
HardDelete erases a version row + its storage object (admin only).
func (*Versions) List ¶
func (h *Versions) List(w http.ResponseWriter, r *http.Request)
List returns the version timeline for a node.
type ViewPrefs ¶
ViewPrefs wires the per-user view-preference routes.
func NewViewPrefs ¶
NewViewPrefs constructs the handler.
func (*ViewPrefs) Get ¶
func (h *ViewPrefs) Get(w http.ResponseWriter, r *http.Request)
Get returns the caller's document. A caller with no stored document gets an empty object rather than a 404: "nothing arranged yet" is the ordinary state of every new account, and a 404 would make the client treat its own first day as a failure.
type WS ¶
type WS struct {
Store db.Store
ACL *acl.Resolver
Hub *realtime.Hub
Tickets *realtime.TicketStore // ticket auth for embedded/cross-origin clients
PublicURL string // used to advertise the wss:// URL in ticket responses
// Tenants resolves which origin the advertised wss:// URL is built on.
// In multi-tenant mode a tenant's browser must be told to open ITS OWN
// host — the operator's would fail the origin check and, on a separate
// certificate, not connect at all.
Tenants tenanturl.Resolver
// PublicURLSet reports whether PublicURL was chosen by the operator. False
// means it is the built-in guess, and wsURL then takes the origin from the
// request instead of announcing an address nothing is listening on.
PublicURLSet bool
// MultiTenant mirrors the config switch, and it is needed here for the same
// reason auth.TenantResolver takes it: a TICKETED upgrade authenticates
// after every middleware has run, so this handler is the only place that
// can attach the ticket user's tenant scope — and it must attach one ONLY
// when the resolver would have. auth.ScopeForUser fails closed
// (tenant.DenyAll) for a user with no provider, which is the normal shape
// on a single-tenant install; attaching that unconditionally would take
// every embedded client on every single-tenant install off the air.
MultiTenant bool
// Lazy, when wired, is told about every accepted recursive watch: a
// desktop sync pair on a lazily catalogued storage needs its whole subtree
// in the catalogue, because the upload precondition it sends is judged
// against catalogue rows (docs/LAZY-CATALOGUE.md). nil changes nothing.
Lazy LazyCatalogue
}
WS is the live-collaboration WebSocket handler. It upgrades GET /api/ws to a WebSocket, then relays the connected user's intents (subscribe to a folder, focus a file, ping) into the realtime Hub and streams the Hub's change + presence frames back to the browser.
The route MUST be mounted in the AUTHENTICATED group so auth.UserFrom(ctx) resolves the caller (the browser's session cookie authenticates the upgrade; it is same-origin, so no bearer token is involved for the native panel).
func NewWS ¶
func NewWS(store db.Store, resolver *acl.Resolver, hub *realtime.Hub, tickets *realtime.TicketStore, publicURL string) *WS
NewWS constructs the WebSocket handler. A nil hub makes Handle reply 503 so the route can be registered unconditionally.
func (*WS) AttachLazy ¶ added in v0.44.2
func (h *WS) AttachLazy(l LazyCatalogue)
AttachLazy wires the lazy catalogue.
func (*WS) AttachMultiTenant ¶
AttachMultiTenant records whether the instance runs in multi-tenant mode. Kept out of NewWS for the same reason as AttachPublicURLConfigured: the existing tests construct a WS directly and must keep the single-tenant (zero-value) behaviour without knowing this exists.
func (*WS) AttachPublicURLConfigured ¶
AttachPublicURLConfigured records whether PublicURL was chosen rather than defaulted. ⚠ Kept out of NewWS deliberately: every test constructs a WS with an explicit URL, and a constructor that guessed at this would have them all disagree with the running server about the one thing being fixed.
func (*WS) AttachTenants ¶
AttachTenants wires the shared per-request origin resolver (internal/tenanturl).
func (*WS) Handle ¶
func (h *WS) Handle(w http.ResponseWriter, r *http.Request)
Handle upgrades the request and runs the per-connection read loop + write pump until the socket closes.
func (*WS) Ticket ¶
func (h *WS) Ticket(w http.ResponseWriter, r *http.Request)
Ticket mints a short-lived, single-use WebSocket auth ticket for the caller, bound to their identity + confinement. Embedded consumers fetch this through the host's HTTP proxy (which injects the real token) and then open `wss://.../api/ws?ticket=<t>` directly — the durable token never reaches the browser. Same-origin callers (the native panel) can use it too.
POST /api/files/ws-ticket → {"ticket": "...", "ws_url": "wss://host/api/ws"}
type WebhooksAdmin ¶
WebhooksAdmin is the admin CRUD surface for webhook v2 targets (multi-destination, event-filtered, HMAC-signed deliveries). The legacy single global webhook keeps its own endpoints under /api/admin/notifications/webhook-config.
func NewWebhooksAdmin ¶
func NewWebhooksAdmin(store db.Store, svc notify.Service) *WebhooksAdmin
NewWebhooksAdmin constructs the handler.
func (*WebhooksAdmin) Create ¶
func (h *WebhooksAdmin) Create(w http.ResponseWriter, r *http.Request)
Create adds a new target.
POST /api/admin/webhooks
body: {name, url, secret?, events?: ["file.uploaded",...], enabled?}
func (*WebhooksAdmin) Delete ¶
func (h *WebhooksAdmin) Delete(w http.ResponseWriter, r *http.Request)
Delete removes a target.
DELETE /api/admin/webhooks/{id}
func (*WebhooksAdmin) List ¶
func (h *WebhooksAdmin) List(w http.ResponseWriter, r *http.Request)
List returns every target (enabled or not), secrets masked, plus the in-memory last-delivery status per target.
GET /api/admin/webhooks
func (*WebhooksAdmin) Test ¶
func (h *WebhooksAdmin) Test(w http.ResponseWriter, r *http.Request)
Test fires a sample payload at one target synchronously (single attempt) and returns the outcome so the admin sees pass/fail with the concrete error inline.
POST /api/admin/webhooks/{id}/test
func (*WebhooksAdmin) Update ¶
func (h *WebhooksAdmin) Update(w http.ResponseWriter, r *http.Request)
Update patches a target.
PATCH /api/admin/webhooks/{id}
Source Files
¶
- account_rules.go
- acl_guard.go
- ai.go
- ai_admin.go
- ai_admin_tenants.go
- ai_doors.go
- ai_doors_people.go
- ai_e2e.go
- ai_mcp.go
- ai_mcp_doors.go
- ai_mcp_people.go
- ai_ops.go
- ai_tags.go
- ai_tokens_admin.go
- antivirus_emit.go
- app_plugins.go
- app_plugins_admin.go
- app_plugins_badges.go
- app_plugins_words.go
- app_store_intent.go
- app_ui.go
- app_ui_call.go
- app_ui_chunks.go
- app_ui_newdoc.go
- apptoken_gate.go
- archive.go
- archive_admin.go
- archive_download.go
- archive_external.go
- archive_progress.go
- audit.go
- audit_targets.go
- auth.go
- auth_methods.go
- auth_providers.go
- auth_self.go
- body_length.go
- branding.go
- branding_contrast.go
- cache_wait.go
- capabilities.go
- catalogue_on_demand.go
- cloud.go
- comments.go
- confine_guard.go
- custom_css.go
- custom_css_serve.go
- dashboard.go
- demo_redact.go
- desktop_auth.go
- download_link.go
- drafts.go
- drop.go
- duplicates.go
- e2e.go
- e2e_convert.go
- e2e_password.go
- e2e_policy_admin.go
- e2e_policy_files.go
- e2e_policy_gate.go
- e2e_rows.go
- entry_unavailable.go
- external_admin.go
- file_types_admin.go
- filebody_errors.go
- grants.go
- groups_admin.go
- image_ref.go
- lazy_listing.go
- link_creator.go
- link_policy.go
- login_security.go
- mail_templates.go
- manager.go
- manager_changes.go
- manager_mutate.go
- manager_newdoc.go
- manager_opsync.go
- meta.go
- mutation_context.go
- name_taken.go
- nfsexports.go
- notifications.go
- notify_emit.go
- onlyoffice.go
- onlyoffice_offer.go
- openwith.go
- ops.go
- pack_source_rule.go
- pathguard.go
- perm_guard.go
- permission_gaps.go
- permissions_admin.go
- person_names.go
- plugin_gate.go
- plugin_requests.go
- plugin_session_gate.go
- plugins.go
- protection.go
- providers.go
- public_api.go
- public_cache.go
- public_i18n.go
- public_link_rule.go
- public_shell.go
- queue.go
- quota.go
- quota_storages.go
- range_stream.go
- realtime_emit.go
- replica.go
- replication_targets.go
- s3keys.go
- save_text.go
- search.go
- search_admin.go
- session_gate.go
- settings.go
- share.go
- share_app_page.go
- share_browse.go
- shared.go
- shares_admin.go
- shares_mine.go
- sharex.go
- sshkeys.go
- storage_drivers.go
- storages.go
- storages_admin.go
- supertenant.go
- surface_conditions.go
- sync_admin.go
- tags.go
- tenant_self.go
- tenantown.go
- themes.go
- thumb.go
- thumb_hydrate.go
- thumb_url.go
- tls_hooks.go
- token_ceiling.go
- tokens_self.go
- tools_thumbs.go
- trash.go
- update.go
- upload.go
- upload_expect.go
- upload_staged.go
- upload_staged_ingest.go
- upload_ticket.go
- usage.go
- userprefs.go
- users.go
- users_admin.go
- versions.go
- viewprefs.go
- webhooks_admin.go
- write_gate.go
- ws.go