hfs
Version:
HTTP File Server
192 lines (191 loc) • 10.4 kB
JavaScript
// 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, '<') : 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);
}
;