hfs
Version:
HTTP File Server
257 lines (255 loc) • 11.5 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.showHelp = exports.configFile = exports.saveConfigAsap = exports.currentVersion = exports.versionToScalar = void 0;
exports.defineConfig = defineConfig;
exports.configKeyExists = configKeyExists;
exports.getConfig = getConfig;
exports.getWholeConfig = getWholeConfig;
exports.setConfig = setConfig;
const const_1 = require("./const");
const watchLoad_1 = require("./watchLoad");
const yaml_1 = __importDefault(require("yaml"));
const lodash_1 = __importDefault(require("lodash"));
const cross_1 = require("./cross");
const debounceAsync_1 = require("./debounceAsync");
const fs_1 = require("fs");
const path_1 = require("path");
const events_1 = __importDefault(require("./events"));
const promises_1 = require("fs/promises");
const immer_1 = __importStar(require("immer"));
const argv_1 = require("./argv");
(0, immer_1.setAutoFreeze)(false); // we still want to mess with objects later (eg: account.belongs)
// keep definition of config properties
const configProps = {};
let started = false; // this will tell the difference for subscribeConfig()s that are called before or after config is loaded
let state = {}; // current state of config properties
const filePath = (0, cross_1.with_)(argv_1.argv.config || process.env.HFS_CONFIG, p => {
if (!p)
return const_1.CONFIG_FILE;
p = (0, path_1.resolve)(const_1.ORIGINAL_CWD, p);
try {
if ((0, fs_1.statSync)(p).isDirectory()) // try to detect if path points to a folder, in which case we add the standard filename
return (0, path_1.join)(p, const_1.CONFIG_FILE);
}
catch (_a) { }
return p;
});
// takes a semver like 1.2.3-alpha1, but alpha and beta numbers must share the number progression
exports.versionToScalar = lodash_1.default.memoize((ver) => {
// this regexp is supposed to be resistant to optional leading "v" and an optional custom name after a space
const res = /^v?(\d+)\.(\d+)\.(\d+)(?:-\D+([0-9.]+))?/.exec(ver);
if (!res)
return NaN;
const [, a, b, c, beta] = res.map(Number);
const officialScalar = c + b * 1E3 + a * 1E6; // gives 3 digits for each number
const betaScalar = 1 / (1 + beta || Infinity); // beta tends to 0, while non-beta is 0. +1 to make it work even in case of alpha0
return officialScalar - betaScalar;
});
class Version extends String {
constructor(v) {
super(v);
this.scalar = (0, exports.versionToScalar)(v);
}
olderThan(otherVersion) {
return this.scalar < (0, exports.versionToScalar)(otherVersion);
}
}
const CONFIG_CHANGE_EVENT_PREFIX = 'config.';
exports.currentVersion = new Version(const_1.VERSION);
const configVersion = defineConfig('version', const_1.VERSION, v => new Version(v));
function defineConfig(k, defaultValue, compiler) {
configProps[k] = { defaultValue };
let compiled = compiler === null || compiler === void 0 ? void 0 : compiler(defaultValue, { k, version: exports.currentVersion, defaultValue });
const ret = {
key() {
return k;
},
get() {
return getConfig(k);
},
sub(cb) {
if (started) // initial event already passed, we'll make the first call
cb(getConfig(k), { k, was: defaultValue, defaultValue, version: configVersion.compiled() });
return events_1.default.on(CONFIG_CHANGE_EVENT_PREFIX + k, (v, was, version) => {
if (stack.includes(cb))
return; // avoid infinite loop in case a subscriber changes the value
stack.push(cb);
try {
return cb(v, { k, was, version, defaultValue });
}
finally {
stack.pop();
}
}, { warnAfter: 1000 }); // e.g. each plugin watch enable_plugins
},
set(v) {
if (typeof v === 'function')
this.set((0, immer_1.default)(this.get(), v));
else
setConfig1(k, v);
},
compiled: () => (compiler ? compiled : (0, cross_1.throw_)("missing compiler")),
};
if (compiler)
ret.sub((...args) => compiled = compiler(...args));
return ret;
}
function configKeyExists(k) {
return configProps.hasOwnProperty(k);
}
const stack = [];
function getConfig(k) {
var _a, _b;
return (_a = state[k]) !== null && _a !== void 0 ? _a : lodash_1.default.cloneDeep((_b = configProps[k]) === null || _b === void 0 ? void 0 : _b.defaultValue); // clone to avoid changing
}
function getWholeConfig({ omit, only }) {
const defs = (0, cross_1.newObj)(configProps, x => x.defaultValue);
let copy = lodash_1.default.defaults({}, state, defs);
if (omit === null || omit === void 0 ? void 0 : omit.length)
copy = lodash_1.default.omit(copy, omit);
if (only)
copy = lodash_1.default.pick(copy, only);
return lodash_1.default.cloneDeep(copy);
}
// pass a value to `save` to force saving decision, or leave undefined for auto. Passing false will also reset previously loaded configs.
function setConfig(newCfg, save) {
const version = lodash_1.default.isString(newCfg.version) ? new Version(newCfg.version) : undefined;
const considerEnvs = !process.env['HFS_ENV_BOOTSTRAP'] || !started && lodash_1.default.isEmpty(newCfg);
// first time we consider also CLI args
const argCfg = !started && lodash_1.default.pickBy((0, cross_1.newObj)(configProps, (x, k) => { var _a; return (_a = argv_1.argv[k]) !== null && _a !== void 0 ? _a : (0, cross_1.tryJson)(considerEnvs ? process.env['HFS_' + k.toUpperCase().replaceAll('-', '_')] : '', lodash_1.default.identity); }), x => x !== undefined);
if (!lodash_1.default.isEmpty(argCfg)) {
(0, exports.saveConfigAsap)(); // don't set `save` argument, as it would interfere below at check `save===false`
Object.assign(newCfg, argCfg);
}
for (const k in newCfg)
apply(k, newCfg[k]);
if (save) {
(0, exports.saveConfigAsap)();
return;
}
if (started) {
if (save === false) // false is used when loading whole config, and in such case we should not leave previous values untreated. Also, we need this only after we already `started`.
for (const k of Object.keys(state))
if (!newCfg.hasOwnProperty(k))
apply(k, undefined);
return;
}
// first time we emit also for the default values
for (const k of Object.keys(configProps))
if (!newCfg.hasOwnProperty(k))
apply(k, newCfg[k], true);
started = true;
events_1.default.emit('configReady', startedWithoutConfig);
if (version !== const_1.VERSION) // be sure to save version
(0, exports.saveConfigAsap)();
function apply(k, newV, isDefault = false) {
return setConfig1(k, newV, save === undefined, argCfg && k in argCfg || isDefault ? exports.currentVersion : version);
}
}
function setConfig1(k, newV, saveChanges = true, valueVersion) {
var _a;
if (lodash_1.default.isPlainObject(newV))
newV = lodash_1.default.pickBy(newV, x => x !== undefined);
const def = (_a = configProps[k]) === null || _a === void 0 ? void 0 : _a.defaultValue;
if (same(newV !== null && newV !== void 0 ? newV : null, def !== null && def !== void 0 ? def : null))
newV = undefined;
if (started && same(newV, state[k]))
return; // no change
const was = getConfig(k); // include cloned default, if necessary
state[k] = newV;
events_1.default.emit(CONFIG_CHANGE_EVENT_PREFIX + k, getConfig(k), was, valueVersion);
if (saveChanges)
(0, exports.saveConfigAsap)();
function same(a, b) {
return a === b || JSON.stringify(a) === JSON.stringify(b);
}
}
const saveDebounced = (0, debounceAsync_1.debounceAsync)(async () => {
while (!started)
await (0, cross_1.wait)(100);
// keep backup
const bak = filePath + '.bak';
const aWeekAgo = Date.now() - cross_1.DAY * 7;
if (await (0, promises_1.stat)(bak).then(x => aWeekAgo > x.mtimeMs, () => true))
await (0, promises_1.copyFile)(filePath, bak).catch(() => { }); // ignore errors
await exports.configFile.save(stringify({ ...state, version: const_1.VERSION }))
.catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)));
});
const saveConfigAsap = () => void saveDebounced();
exports.saveConfigAsap = saveConfigAsap;
function stringify(obj) {
return yaml_1.default.stringify(obj, { lineWidth: 1000 });
}
let startedWithoutConfig = false;
console.log("config", filePath);
exports.configFile = (0, watchLoad_1.watchLoad)(filePath, text => {
startedWithoutConfig = !text;
try {
setConfig(yaml_1.default.parse(text, { uniqueKeys: false }) || {}, false);
}
catch (e) {
console.error("Error in", filePath, ':', e.message || String(e));
}
}, {
failedOnFirstAttempt() {
startedWithoutConfig = true;
console.log("No config file, using defaults");
setTimeout(() => // this is called synchronously, but we need to call setConfig after first tick, when all configs are defined
setConfig({}, false));
}
});
exports.showHelp = argv_1.argv.help;
events_1.default.on('configReady', () => {
if (!exports.showHelp)
return;
console.log(`HELP
You can pass any configuration in the form: --name value
Most common configurations:
--create-admin <password>
--port <port>
--cert <path>
--private_key <path>
--consoleFile <path>
For a description of each configuration, please refer to https://rejetto.com/hfs-config
Other options:
--debug will print extra information
`);
process.exit(0);
});
;