UNPKG

hfs

Version:
192 lines (191 loc) 10.4 kB
"use strict"; // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.logGui = void 0; exports.serveGuiFiles = serveGuiFiles; const promises_1 = __importDefault(require("fs/promises")); const const_1 = require("./const"); const serveFile_1 = require("./serveFile"); const plugins_1 = require("./plugins"); const api_auth_1 = require("./api.auth"); const apiMiddleware_1 = require("./apiMiddleware"); const path_1 = require("path"); const misc_1 = require("./misc"); const adminApis_1 = require("./adminApis"); const customHtml_1 = require("./customHtml"); const lodash_1 = __importDefault(require("lodash")); const config_1 = require("./config"); const lang_1 = require("./lang"); const upload_1 = require("./upload"); const icons_1 = require("./icons"); const size1024 = (0, config_1.defineConfig)(misc_1.CFG.size_1024, false, x => misc_1.formatBytes.k = x ? 1024 : 1000); // we both configure formatBytes, and also provide a compiled version (number instead of boolean) const splitUploads = (0, config_1.defineConfig)(misc_1.CFG.split_uploads, 0); exports.logGui = (0, config_1.defineConfig)(misc_1.CFG.log_gui, false); lodash_1.default.each(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.defineConfig)(k, v)); // define default values function serveStatic(uri) { const folder = (const_1.DEV ? 'dist/' : '') + uri.slice(2, -1); // we know folder is very similar to uri let cache = {}; customHtml_1.customHtml.emitter.on('change', () => cache = {}); // reset cache at every change return async (ctx) => { if (!exports.logGui.get()) ctx.state.dontLog = true; if (ctx.method === 'OPTIONS') { ctx.status = const_1.HTTP_NO_CONTENT; ctx.set({ Allow: 'OPTIONS, GET' }); return; } if (ctx.method !== 'GET') return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED; const serveApp = shouldServeApp(ctx); const fullPath = (0, path_1.join)(__dirname, '..', folder, serveApp ? '/index.html' : ctx.path); const content = await (0, misc_1.parseFileContent)(fullPath, raw => serveApp || !raw.length ? raw : adjustBundlerLinks(ctx, uri, raw)) .catch(() => null); if (content === null) return ctx.status = const_1.HTTP_NOT_FOUND; if (!serveApp) return (0, serveFile_1.serveFile)(ctx, fullPath, const_1.MIME_AUTO, content); // we don't cache the index as it's small and may prevent plugins change to apply ctx.body = await treatIndex(ctx, uri, String(content)); }; } function shouldServeApp(ctx) { var _a; return (_a = ctx.state).serveApp || (_a.serveApp = ctx.path.endsWith('/') && !ctx.headers.upgrade); // skip websockets } function adjustBundlerLinks(ctx, uri, data) { const ext = (0, path_1.extname)(ctx.path); return ext && !ext.match(/\.(css|html|js|ts|scss)/) ? data : String(data).replace(/((?:import[ (]| from )['"])\//g, `$1${ctx.state.revProxyPath}${uri}`); } const getFaviconTimestamp = (0, misc_1.debounceAsync)(async () => { const f = adminApis_1.favicon.get(); return !f ? 0 : promises_1.default.stat(f).then(x => (x === null || x === void 0 ? void 0 : x.mtimeMs) || 0, () => 0); }, { retain: 5000 }); async function treatIndex(ctx, filesUri, body) { const session = await (0, api_auth_1.refresh_session)({}, ctx); ctx.set('etag', ''); ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate'); ctx.type = 'html'; const isFrontend = filesUri === const_1.FRONTEND_URI ? ' ' : ''; // as a string will allow neater code later const pub = ctx.state.revProxyPath + const_1.PLUGINS_PUB_URI; // expose plugins' configs that are declared with 'frontend' attribute const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => { var _a, _b; let configs = (0, misc_1.newObj)((0, plugins_1.getPluginConfigFields)(name), (v, k, skip) => { var _a, _b, _c; return !v.frontend ? skip() : adjustValueByConfig((_b = (_a = plugins_1.pluginsConfig.get()) === null || _a === void 0 ? void 0 : _a[name]) === null || _b === void 0 ? void 0 : _b[k], (_c = pl.getData().config) === null || _c === void 0 ? void 0 : _c[k]); }); configs = ((_b = (_a = (0, plugins_1.getPluginInfo)(name)).onFrontendConfig) === null || _b === void 0 ? void 0 : _b.call(_a, configs)) || configs; return !lodash_1.default.isEmpty(configs) && [name, configs]; }))); const timestamp = await getFaviconTimestamp(); const lang = await (0, lang_1.getLangData)(ctx); return body .replace(/((?:src|href) *= *['"])\/?(?!([a-z]+:\/)?\/)(?!\?)/g, '$1' + ctx.state.revProxyPath + filesUri) .replace(/<(\/)?(head|body)>/g, (all, isClose, name) => { const isHead = name === 'head'; const isBody = !isHead; const isOpen = !isClose; if (isHead && isOpen) return all + ` <script> HFS = ${JSON.stringify({ VERSION: const_1.VERSION, API_VERSION: const_1.API_VERSION, SPECIAL_URI: const_1.SPECIAL_URI, PLUGINS_PUB_URI: const_1.PLUGINS_PUB_URI, FRONTEND_URI: const_1.FRONTEND_URI, session: session instanceof apiMiddleware_1.ApiError ? null : session, plugins, loadScripts: Object.fromEntries((0, plugins_1.mapPlugins)((p, id) => { var _a; return [id, (_a = p.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => f.includes('//') ? f : pub + id + '/' + f)]; })), prefixUrl: ctx.state.revProxyPath, dontOverwriteUploading: upload_1.dontOverwriteUploading.get(), splitUploads: splitUploads.get(), kb: size1024.compiled(), forceTheme: (0, plugins_1.mapPlugins)(p => lodash_1.default.isString(p.isTheme) ? p.isTheme : undefined).find(Boolean), customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']), // exclude the sections we already apply in this phase ...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)), icons: Object.assign({}, ...(0, plugins_1.mapPlugins)(p => iconsToObj(p.icons, p.id + '/')), iconsToObj(icons_1.customizedIcons)), // name-to-uri lang }, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/} document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0]) </script> ${isFrontend && ` <title>${adminApis_1.title.get()}</title> <link rel="shortcut icon" href="/favicon.ico?${timestamp}" /> ${(0, customHtml_1.getSection)('htmlHead')}`} `; function iconsToObj(icons, pre = '') { return icons && (0, misc_1.objSameKeys)(icons, (v, k) => const_1.ICONS_URI + pre + k); } if (isBody && isOpen) return `${all} ${isFrontend && (0, customHtml_1.getSection)('top')} <style> :root { ${lodash_1.default.map(plugins, (configs, pluginName) => // make plugin configs accessible via css lodash_1.default.map(configs, (v, k) => { v = serializeCss(v); return typeof v === 'string' && `\n--${pluginName}-${k}: ${v};`; }).filter(Boolean).join('')).join('')} } ${isFrontend && (0, customHtml_1.getSection)('style')} </style> ${isFrontend && (0, plugins_1.mapPlugins)((plug, id) => { var _a; return (_a = plug.frontend_css) === null || _a === void 0 ? void 0 : _a.map(f => `<link rel='stylesheet' type='text/css' href='${f.includes('//') ? f : pub + id + '/' + f}' plugin=${JSON.stringify(id)}/>`); }) .flat().filter(Boolean).join('\n')} `; if (isBody && isClose) return (0, customHtml_1.getSection)('bottom') + all; return all; // unchanged }); function adjustValueByConfig(v, cfg) { v !== null && v !== void 0 ? v : (v = cfg.defaultValue); const { type } = cfg; if (v && type === 'vfs_path') { v = (0, misc_1.enforceStarting)('/', v); const { root } = ctx.state; if (root) if (v.startsWith(root)) v = v.slice(root.length - 1); else return; if (ctx.state.revProxyPath) v = ctx.state.revProxyPath + v; } else if (type === 'array' && Array.isArray(v)) v = v.map(x => (0, misc_1.objSameKeys)(x, (xv, xk) => adjustValueByConfig(xv, cfg.fields[xk]))); return v; } } function serializeCss(v) { var _a; return typeof v === 'string' && /^#[0-9a-fA-F]{3,8}|rgba?\(.+\)$/.test(v) ? v // colors : (0, misc_1.isPrimitive)(v) ? (_a = JSON.stringify(v)) === null || _a === void 0 ? void 0 : _a.replace(/</g, '&lt;') : undefined; } function serveProxied(port, uri) { if (!port) return; console.debug('proxied on port', port); let proxy; import('koa-better-http-proxy').then(lib => // dynamic import to avoid having this in final distribution proxy = lib.default('127.0.0.1:' + port, { proxyReqPathResolver: (ctx) => shouldServeApp(ctx) ? '/' : ctx.path, userResDecorator(res, data, ctx) { return shouldServeApp(ctx) ? treatIndex(ctx, uri, String(data)) : adjustBundlerLinks(ctx, uri, data); } })); return function (ctx, next) { if (!exports.logGui.get()) ctx.state.dontLog = true; return proxy(ctx, next); }; } function serveGuiFiles(proxyPort, uri) { return serveProxied(proxyPort, uri) || serveStatic(uri); }