/** * appBase — the path prefix the filex web app is served under. * * filex can run under a sub-path behind a reverse proxy * (`https://example.com/filex/`, FILEX_BASE_PATH on the server). The web app * is built once and runs at the root and under any prefix: the server tells * the document which one it is in by adding * * * * to the index.html it serves (backend/internal/api/spa_shell.go). At the * root there is no tag, and everything here answers `''` — the address the * app always used. * * ⚠⚠ The ONE reader of that tag. The admin/drive app, the explorer and every * default below ask here; a second reader is how one of them ends up still * talking to the host's root. * * ⚠ It is the base of the DOCUMENT, not of the API. An explorer embedded in * another application has no such tag (the host page is not filex), so its * defaults stay what they were and it keeps using the `apiBase` its host * configured. The desktop app loads its own pages and has none either. */ /** The meta tag's name. Mirrors spa_shell.go. */ export const APP_BASE_META = 'filex-base'; /** * What a base may look like: `/seg[/seg…]` with the URL's unreserved * characters, and no `.`/`..` segment. The same rule the server validates * FILEX_BASE_PATH with (backend/internal/basepath.Normalize), repeated here so * a tag that did not come from filex cannot aim the app somewhere else. */ const BASE_RE = /^(\/[A-Za-z0-9_~.-]+)+$/; /** Reads the base from a document; `''` when it has none, or a bad one. */ export function readAppBase(doc?: Pick | null): string { const d = doc ?? (typeof document !== 'undefined' ? document : null); if (!d) return ''; let raw = ''; try { raw = d.querySelector(`meta[name="${APP_BASE_META}"]`)?.getAttribute('content')?.trim() ?? ''; } catch { return ''; } if (!raw || !BASE_RE.test(raw)) return ''; if (raw.split('/').some((seg) => seg === '.' || seg === '..')) return ''; return raw; } /** The base this document was served under: `''` at the root, else `/filex`. */ export function appBase(): string { return readAppBase(); } /** * An application path (`/files/edit`, `/api/files/archive/list`) as the * browser has to ask for it from this document: with the base in front. */ export function withAppBase(path: string): string { return appBase() + path; } /** * A path relative to the SERVER ROOT, joined onto an API base, keeping the * base's own path. * * The server hands out some addresses relative to its root — a thumbnail's * `thumb_url`, a selection archive's `/z/` — because it cannot know * where its reader sees it: the admin app at `/filex`, the desktop app at * `https://example.com/filex`, an embed through its host's proxy at * `https://host.example/files-proxy`. The reader knows, and its API base * (`apiBase`, the server root WITHOUT `/api`) is that answer. * * ⚠ Not `new URL(path, apiBase)`: a root-relative path resolved that way drops * the base's path, which is precisely the prefix a sub-path deployment or a * proxying host lives under. * * An absolute `path` is returned unchanged; an empty base leaves the path * relative to the page's own origin (same-origin at the root, as before). */ export function underApiBase(apiBase: string | null | undefined, path: string): string { if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) return path; const base = String(apiBase ?? '').replace(/\/+$/, ''); if (!base || !path.startsWith('/')) return path; return base + path; } /** * The API root a module-level default talks to: the one its caller gave, else * this document's base path — `''` at the root, so an unconfigured caller * behaves exactly as before, and `/filex` for the admin app under a sub-path. * * ⚠ For the modules that fetch on their own at boot (account preferences, * the language list, the public branding) and are configured with no base by * the admin app: an unset base used to mean "the host's root". */ export function apiRootOr(base: string | null | undefined): string { return String(base ?? appBase()).replace(/\/+$/, ''); }