hfs 0.57.26 → 0.57.27-beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/{index-CHMhmNYS.js → index-DSkhXlWX.js} +103 -103
- package/admin/assets/{sha512-Dsv49IAs.js → sha512-CoddZKeD.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{index-legacy-BI8icwFp.js → index-legacy-C4Gclsbz.js} +4 -4
- package/frontend/assets/{sha512-legacy-D82alHBq.js → sha512-legacy-C-FMk44u.js} +1 -1
- package/frontend/index.html +1 -1
- package/package.json +4 -3
- package/src/QuickZipStream.js +11 -8
- package/src/SendList.js +10 -8
- package/src/ThrottledStream.js +8 -6
- package/src/acme.js +5 -7
- package/src/adminApis.js +6 -7
- package/src/api.accounts.js +4 -6
- package/src/api.auth.js +7 -11
- package/src/api.get_file_list.js +11 -16
- package/src/api.lang.js +1 -1
- package/src/api.monitor.js +2 -3
- package/src/api.net.js +1 -2
- package/src/api.plugins.js +8 -10
- package/src/api.vfs.js +14 -19
- package/src/apiMiddleware.js +4 -4
- package/src/auth.js +6 -9
- package/src/basicWeb.js +1 -2
- package/src/block.js +1 -1
- package/src/commands.js +9 -9
- package/src/comments.js +4 -5
- package/src/config.js +8 -9
- package/src/connections.js +11 -6
- package/src/consoleLog.js +6 -6
- package/src/const.js +4 -4
- package/src/cross.js +19 -21
- package/src/customHtml.js +3 -4
- package/src/ddns.js +4 -4
- package/src/debounceAsync.js +3 -3
- package/src/errorPages.js +2 -3
- package/src/events.js +7 -10
- package/src/fileAttr.js +6 -6
- package/src/first.js +13 -8
- package/src/frontEndApis.js +5 -5
- package/src/geo.js +3 -5
- package/src/github.js +14 -17
- package/src/i18n.js +3 -4
- package/src/icons.js +1 -1
- package/src/index.js +2 -3
- package/src/lang.js +5 -6
- package/src/listen.js +25 -28
- package/src/log.js +18 -17
- package/src/middlewares.js +12 -15
- package/src/misc.js +9 -11
- package/src/nat.js +14 -16
- package/src/outboundProxy.js +7 -7
- package/src/perm.js +7 -12
- package/src/persistence.js +1 -1
- package/src/plugins.js +62 -73
- package/src/roots.js +1 -2
- package/src/selfCheck.js +5 -4
- package/src/serveFile.js +6 -7
- package/src/serveGuiAndSharedFiles.js +7 -9
- package/src/serveGuiFiles.js +11 -20
- package/src/stat.js +40 -0
- package/src/statWorker.js +11 -0
- package/src/throttler.js +5 -7
- package/src/update.js +7 -7
- package/src/upload.js +11 -13
- package/src/util-files.js +42 -32
- package/src/util-http.js +80 -29
- package/src/util-os.js +2 -4
- package/src/vfs.js +77 -68
- package/src/walkDir.js +9 -9
- package/src/watchLoad.js +4 -4
- package/src/zip.js +5 -6
package/src/cross.js
CHANGED
|
@@ -101,7 +101,7 @@ __exportStar(require("./cross-const"), exports);
|
|
|
101
101
|
exports.WEBSITE = 'https://rejetto.com/hfs/';
|
|
102
102
|
exports.REPO_URL = `https://github.com/${cross_const_1.HFS_REPO}/`;
|
|
103
103
|
exports.WIKI_URL = exports.REPO_URL + 'wiki/';
|
|
104
|
-
exports.MINUTE =
|
|
104
|
+
exports.MINUTE = 60_000;
|
|
105
105
|
exports.HOUR = 60 * exports.MINUTE;
|
|
106
106
|
exports.DAY = 24 * exports.HOUR;
|
|
107
107
|
exports.MAX_TILE_SIZE = 10;
|
|
@@ -146,10 +146,9 @@ function isWhoObject(v) {
|
|
|
146
146
|
}
|
|
147
147
|
const MULTIPLIERS = ['', 'K', 'M', 'G', 'T'];
|
|
148
148
|
function formatBytes(n, { post = 'B', k = 0, digits = NaN, sep = ' ' } = {}) {
|
|
149
|
-
var _a;
|
|
150
149
|
if (isNaN(Number(n)) || n < 0)
|
|
151
150
|
return '';
|
|
152
|
-
k
|
|
151
|
+
k ||= formatBytes.k ?? 1024; // default value
|
|
153
152
|
const i = n && Math.min(MULTIPLIERS.length - 1, Math.floor(Math.log2(n) / Math.log2(k)));
|
|
154
153
|
n /= k ** i;
|
|
155
154
|
const nAsString = i && !isNaN(digits) ? n.toFixed(digits)
|
|
@@ -176,7 +175,11 @@ function wait(ms, val) {
|
|
|
176
175
|
}
|
|
177
176
|
// throws after ms
|
|
178
177
|
function haveTimeout(ms, job, error) {
|
|
179
|
-
|
|
178
|
+
let h;
|
|
179
|
+
return Promise.race([
|
|
180
|
+
job.finally(() => clearTimeout(h)), // don't leave pending timeout if the job is done first
|
|
181
|
+
new Promise((_resolve, reject) => h = setTimeout(() => reject(error || Error('timeout')), ms))
|
|
182
|
+
]);
|
|
180
183
|
}
|
|
181
184
|
function objSameKeys(src, newValue) {
|
|
182
185
|
return Object.fromEntries(Object.entries(src).map(([k, v]) => [k, newValue(v, k)]));
|
|
@@ -227,7 +230,7 @@ function try_(cb, onException) {
|
|
|
227
230
|
return cb();
|
|
228
231
|
}
|
|
229
232
|
catch (e) {
|
|
230
|
-
return onException
|
|
233
|
+
return onException?.(e);
|
|
231
234
|
}
|
|
232
235
|
}
|
|
233
236
|
function with_(par, cb) {
|
|
@@ -253,8 +256,7 @@ function pendingPromise() {
|
|
|
253
256
|
return Object.assign(ret, takeOut);
|
|
254
257
|
}
|
|
255
258
|
function basename(path) {
|
|
256
|
-
|
|
257
|
-
return ((_a = path.match(/([^\\/]+)[\\/]*$/)) === null || _a === void 0 ? void 0 : _a[1]) || '';
|
|
259
|
+
return path.match(/([^\\/]+)[\\/]*$/)?.[1] || '';
|
|
258
260
|
}
|
|
259
261
|
function dirname(path) {
|
|
260
262
|
return path.slice(0, Math.max(0, path.lastIndexOf('/', path.length - 1)));
|
|
@@ -263,8 +265,8 @@ function tryJson(s, except) {
|
|
|
263
265
|
try {
|
|
264
266
|
return s && JSON.parse(s);
|
|
265
267
|
}
|
|
266
|
-
catch
|
|
267
|
-
return except
|
|
268
|
+
catch {
|
|
269
|
+
return except?.(s);
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
function swap(obj, k1, k2) {
|
|
@@ -377,7 +379,7 @@ function repeat(everyMs, cb) {
|
|
|
377
379
|
try {
|
|
378
380
|
await cb(stopIt);
|
|
379
381
|
} // you can use stopIt passed as a parameter or the returned value, whatever makes you happy
|
|
380
|
-
catch
|
|
382
|
+
catch { }
|
|
381
383
|
await wait(everyMs);
|
|
382
384
|
}
|
|
383
385
|
})();
|
|
@@ -418,17 +420,15 @@ function isTimestampString(v) {
|
|
|
418
420
|
return typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z*$/.test(v);
|
|
419
421
|
}
|
|
420
422
|
function isEqualLax(a, b, overrideRule) {
|
|
421
|
-
|
|
422
|
-
return (_a = overrideRule === null || overrideRule === void 0 ? void 0 : overrideRule(a, b)) !== null && _a !== void 0 ? _a : (a == b || a && b && typeof a === 'object' && typeof b === 'object'
|
|
423
|
+
return overrideRule?.(a, b) ?? (a == b || a && b && typeof a === 'object' && typeof b === 'object'
|
|
423
424
|
&& Object.entries(a).every(([k, v]) => isEqualLax(v, b[k], overrideRule))
|
|
424
425
|
&& Object.entries(b).every(([k, v]) => k in a /*already checked*/ || isEqualLax(v, a[k], overrideRule)));
|
|
425
426
|
}
|
|
426
427
|
function xlate(input, table) {
|
|
427
|
-
|
|
428
|
-
return (_a = table[input]) !== null && _a !== void 0 ? _a : input;
|
|
428
|
+
return table[input] ?? input;
|
|
429
429
|
}
|
|
430
430
|
function normalizeHost(host) {
|
|
431
|
-
return host[0] === '[' ? host.slice(1, host.indexOf(']')) : host
|
|
431
|
+
return host[0] === '[' ? host.slice(1, host.indexOf(']')) : host?.split(':')[0];
|
|
432
432
|
}
|
|
433
433
|
function isIpLocalHost(ip) {
|
|
434
434
|
return ip === '::1' || ip.endsWith('127.0.0.1');
|
|
@@ -480,9 +480,8 @@ function makeMatcher(mask, emptyMaskReturns = false) {
|
|
|
480
480
|
}
|
|
481
481
|
// this is caching all matchers, so don't use it with frequently changing masks. Benchmarks revealed that _.memoize make it slower than not using it, while this simple cache can speed up to 30x
|
|
482
482
|
function matches(s, mask, emptyMaskReturns = false) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return (cache[_b = mask + (emptyMaskReturns ? '1' : '0')] || (cache[_b] = makeMatcher(mask, emptyMaskReturns)))(s);
|
|
483
|
+
const cache = matches.cache ||= {};
|
|
484
|
+
return (cache[mask + (emptyMaskReturns ? '1' : '0')] ||= makeMatcher(mask, emptyMaskReturns))(s);
|
|
486
485
|
}
|
|
487
486
|
// if delimiter is specified, it is prefixed to symbols. If it contains a space, the part after the space is considered as suffix.
|
|
488
487
|
function replace(s, symbols, delimiter = '') {
|
|
@@ -513,7 +512,7 @@ function safeDecodeURIComponent(s) {
|
|
|
513
512
|
try {
|
|
514
513
|
return decodeURIComponent(s);
|
|
515
514
|
}
|
|
516
|
-
catch
|
|
515
|
+
catch {
|
|
517
516
|
return s;
|
|
518
517
|
}
|
|
519
518
|
}
|
|
@@ -543,9 +542,8 @@ function toMutable(value) {
|
|
|
543
542
|
return Array.isArray(value) ? value.slice() : { ...value };
|
|
544
543
|
}
|
|
545
544
|
function shortenAgent(agent) {
|
|
546
|
-
var _a;
|
|
547
545
|
return lodash_1.default.findKey(BROWSERS, re => re.test(agent))
|
|
548
|
-
||
|
|
546
|
+
|| /^[^/(]+ ?/.exec(agent)?.[0]
|
|
549
547
|
|| agent;
|
|
550
548
|
}
|
|
551
549
|
const BROWSERS = {
|
package/src/customHtml.js
CHANGED
|
@@ -22,7 +22,6 @@ exports.disableCustomHtml = (0, config_1.defineConfig)(misc_1.CFG.disable_custom
|
|
|
22
22
|
function watchLoadCustomHtml(folder = '') {
|
|
23
23
|
const sections = new Map();
|
|
24
24
|
const res = (0, watchLoad_1.watchLoad)((0, misc_1.prefix)('', folder, '/') + FILE, data => {
|
|
25
|
-
var _a;
|
|
26
25
|
const re = /^\[([^\]]+)] *$/gm;
|
|
27
26
|
sections.clear();
|
|
28
27
|
if (!data)
|
|
@@ -31,10 +30,10 @@ function watchLoadCustomHtml(folder = '') {
|
|
|
31
30
|
do {
|
|
32
31
|
let last = re.lastIndex;
|
|
33
32
|
const match = re.exec(data);
|
|
34
|
-
const content = data.slice(last, !match ? undefined : re.lastIndex - (
|
|
33
|
+
const content = data.slice(last, !match ? undefined : re.lastIndex - (match?.[0]?.length || 0)).trim();
|
|
35
34
|
if (content)
|
|
36
35
|
sections.set(name, content);
|
|
37
|
-
name = match
|
|
36
|
+
name = match?.[1];
|
|
38
37
|
} while (name);
|
|
39
38
|
});
|
|
40
39
|
return Object.assign(res, { sections });
|
|
@@ -50,7 +49,7 @@ function getAllSections() {
|
|
|
50
49
|
return Object.fromEntries(all.map(x => [x, getSection(x)]));
|
|
51
50
|
}
|
|
52
51
|
async function saveCustomHtml(sections) {
|
|
53
|
-
const text = Object.entries(sections).filter(([k, v]) => v
|
|
52
|
+
const text = Object.entries(sections).filter(([k, v]) => v?.trim()).map(([k, v]) => `[${k}]\n${v}\n\n`).join('');
|
|
54
53
|
await (0, promises_1.writeFile)(FILE, text);
|
|
55
54
|
exports.customHtml.sections.clear();
|
|
56
55
|
for (const [k, v] of Object.entries(sections))
|
package/src/ddns.js
CHANGED
|
@@ -22,8 +22,8 @@ let stopFetching;
|
|
|
22
22
|
let lastIPs;
|
|
23
23
|
let lastMap;
|
|
24
24
|
events_1.default.onListeners(EVENT, cbs => {
|
|
25
|
-
stopFetching
|
|
26
|
-
if (!
|
|
25
|
+
stopFetching?.();
|
|
26
|
+
if (!cbs?.size)
|
|
27
27
|
return;
|
|
28
28
|
stopFetching = (0, cross_1.repeat)(cross_1.HOUR, async () => {
|
|
29
29
|
const IPs = await (0, nat_1.getPublicIps)();
|
|
@@ -41,7 +41,7 @@ events_1.default.onListeners(EVENT, cbs => {
|
|
|
41
41
|
let stopListening;
|
|
42
42
|
let last;
|
|
43
43
|
dynamicDnsUrl.sub(v => {
|
|
44
|
-
stopListening
|
|
44
|
+
stopListening?.();
|
|
45
45
|
if (!v)
|
|
46
46
|
return;
|
|
47
47
|
stopListening = events_1.default.on(EVENT, async () => {
|
|
@@ -60,7 +60,7 @@ dynamicDnsUrl.sub(v => {
|
|
|
60
60
|
}));
|
|
61
61
|
last = lodash_1.default.find(all, 'error') || all[0]; // the system is designed for just one result, and we give precedence to errors
|
|
62
62
|
events_1.default.emit('dynamicDnsError', last);
|
|
63
|
-
console.log('dynamic dns update',
|
|
63
|
+
console.log('dynamic dns update', last?.error || 'ok');
|
|
64
64
|
}, { callNow: true });
|
|
65
65
|
});
|
|
66
66
|
async function get_dynamic_dns_error() {
|
package/src/debounceAsync.js
CHANGED
|
@@ -19,7 +19,7 @@ callback, options = {}) {
|
|
|
19
19
|
const interceptingWrapper = (...args) => latestDebouncer = debouncer(...args);
|
|
20
20
|
return Object.assign(interceptingWrapper, {
|
|
21
21
|
clearRetain: () => latestCallback = undefined,
|
|
22
|
-
flush: () => runningCallback
|
|
22
|
+
flush: () => runningCallback ?? exec(),
|
|
23
23
|
isWorking: () => runningCallback,
|
|
24
24
|
...cancelable && {
|
|
25
25
|
cancel() {
|
|
@@ -32,10 +32,10 @@ callback, options = {}) {
|
|
|
32
32
|
if (reuseRunning && runningCallback)
|
|
33
33
|
return runningCallback;
|
|
34
34
|
const now = Date.now();
|
|
35
|
-
if (latestCallback && now - latestTimestamp < (latestHasFailed ? retainFailure
|
|
35
|
+
if (latestCallback && now - latestTimestamp < (latestHasFailed ? retainFailure ?? retain : retain))
|
|
36
36
|
return await latestCallback;
|
|
37
37
|
whoIsWaiting = args;
|
|
38
|
-
waitingSince
|
|
38
|
+
waitingSince ||= now;
|
|
39
39
|
const waitingCap = maxWait - (now - (waitingSince || started));
|
|
40
40
|
const waitFor = Math.min(waitingCap, leading ? wait - (now - started) : wait);
|
|
41
41
|
if (waitFor > 0)
|
package/src/errorPages.js
CHANGED
|
@@ -11,7 +11,6 @@ function getErrorSections() {
|
|
|
11
11
|
}
|
|
12
12
|
// to be used with errors whose recipient is possibly human
|
|
13
13
|
async function sendErrorPage(ctx, code = ctx.status) {
|
|
14
|
-
var _a, _b;
|
|
15
14
|
ctx.type = 'text';
|
|
16
15
|
ctx.set('content-disposition', ''); // reset ctx.attachment (or forceDownload)
|
|
17
16
|
ctx.status = code;
|
|
@@ -21,8 +20,8 @@ async function sendErrorPage(ctx, code = ctx.status) {
|
|
|
21
20
|
const lang = await (0, lang_1.getLangData)(ctx);
|
|
22
21
|
if (!lang)
|
|
23
22
|
return;
|
|
24
|
-
const trans =
|
|
25
|
-
ctx.body =
|
|
23
|
+
const trans = Object.values(lang)[0]?.translate;
|
|
24
|
+
ctx.body = trans?.[msg] ?? msg;
|
|
26
25
|
const errorPage = (0, customHtml_1.getSection)(ctx.status === cross_1.HTTP_UNAUTHORIZED ? 'unauthorized' : String(ctx.status));
|
|
27
26
|
if (!errorPage)
|
|
28
27
|
return;
|
package/src/events.js
CHANGED
|
@@ -4,11 +4,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.BetterEventEmitter = void 0;
|
|
5
5
|
const LISTENERS_SUFFIX = '\0listeners';
|
|
6
6
|
class BetterEventEmitter {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.stop = this.preventDefault; // legacy pre-0.54 (introduced in 0.53)
|
|
11
|
-
}
|
|
7
|
+
listeners = new Map();
|
|
8
|
+
preventDefault = Symbol();
|
|
9
|
+
stop = this.preventDefault; // legacy pre-0.54 (introduced in 0.53)
|
|
12
10
|
on(event, listener, { warnAfter = 10, callNow = false } = {}) {
|
|
13
11
|
if (typeof event === 'string')
|
|
14
12
|
event = [event];
|
|
@@ -25,7 +23,7 @@ class BetterEventEmitter {
|
|
|
25
23
|
try {
|
|
26
24
|
listener();
|
|
27
25
|
}
|
|
28
|
-
catch
|
|
26
|
+
catch { }
|
|
29
27
|
return () => {
|
|
30
28
|
for (const e of event) {
|
|
31
29
|
const cbs = this.listeners.get(e);
|
|
@@ -47,7 +45,7 @@ class BetterEventEmitter {
|
|
|
47
45
|
off = this.on(event, function (...args) {
|
|
48
46
|
off();
|
|
49
47
|
resolve(args.slice(0, -1)); // remove the extra argument at the end of our emit()
|
|
50
|
-
return listener
|
|
48
|
+
return listener?.(...args);
|
|
51
49
|
});
|
|
52
50
|
});
|
|
53
51
|
return Object.assign(off, { then: pro.then.bind(pro) });
|
|
@@ -60,12 +58,11 @@ class BetterEventEmitter {
|
|
|
60
58
|
};
|
|
61
59
|
}
|
|
62
60
|
anyListener(event) {
|
|
63
|
-
|
|
64
|
-
return Boolean((_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.size);
|
|
61
|
+
return Boolean(this.listeners.get(event)?.size);
|
|
65
62
|
}
|
|
66
63
|
emit(event, ...args) {
|
|
67
64
|
let cbs = this.listeners.get(event);
|
|
68
|
-
if (!
|
|
65
|
+
if (!cbs?.size)
|
|
69
66
|
return;
|
|
70
67
|
const output = [];
|
|
71
68
|
let prevented = false;
|
package/src/fileAttr.js
CHANGED
|
@@ -24,10 +24,9 @@ if ((0, fs_1.existsSync)(FN))
|
|
|
24
24
|
const FILE_ATTR_PREFIX = 'user.hfs.'; // user. prefix to be linux compatible
|
|
25
25
|
/* @param v must be JSON-able or undefined */
|
|
26
26
|
async function storeFileAttr(path, k, v) {
|
|
27
|
-
var _a, _b;
|
|
28
27
|
const s = await (0, util_files_1.statWithTimeout)(path).catch(() => null);
|
|
29
28
|
// since we don't have fsx.remove, we simulate it with an empty string
|
|
30
|
-
if (s && await
|
|
29
|
+
if (s && await fsx?.set(path, FILE_ATTR_PREFIX + k, v === undefined ? '' : JSON.stringify(v)).then(() => 1, () => 0)) {
|
|
31
30
|
if (const_1.IS_WINDOWS)
|
|
32
31
|
(0, promises_2.utimes)(path, s.atime, s.mtime); // restore timestamps, necessary only on Windows
|
|
33
32
|
return true;
|
|
@@ -39,14 +38,15 @@ async function storeFileAttr(path, k, v) {
|
|
|
39
38
|
else
|
|
40
39
|
await fileAttrDb.open(FN);
|
|
41
40
|
// pipe should be a safe separator
|
|
42
|
-
return
|
|
41
|
+
return await fileAttrDb.put(`${path}|${k}`, v)?.catch((e) => {
|
|
43
42
|
console.error("couldn't store metadata on", path, String(e.message || e));
|
|
44
43
|
return false;
|
|
45
|
-
})
|
|
44
|
+
}) ?? true; // if put is undefined, the value was already there
|
|
46
45
|
}
|
|
47
46
|
async function loadFileAttr(path, k) {
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
return await fsx?.get(path, FILE_ATTR_PREFIX + k)
|
|
48
|
+
.then((x) => x === '' ? undefined : (0, cross_1.tryJson)(String(x)), () => fileAttrDb.isOpen() ? fileAttrDb.get(`${path}|${k}`) : null)
|
|
49
|
+
?? undefined; // normalize, as we get null instead of undefined on windows
|
|
50
50
|
}
|
|
51
51
|
async function purgeFileAttr() {
|
|
52
52
|
let n = 0;
|
package/src/first.js
CHANGED
|
@@ -10,17 +10,22 @@ function onProcessExit(cb) {
|
|
|
10
10
|
}
|
|
11
11
|
exports.quitting = false;
|
|
12
12
|
onProcessExit(() => exports.quitting = true);
|
|
13
|
-
onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP']
|
|
14
|
-
console.log('quitting');
|
|
13
|
+
onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP']).then(([signal]) => Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() => {
|
|
14
|
+
console.log('quitting', signal || '');
|
|
15
15
|
process.exit(0);
|
|
16
16
|
}));
|
|
17
|
-
function onFirstEvent(emitter, events
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
function onFirstEvent(emitter, events) {
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
let already = false;
|
|
20
|
+
for (const e of events)
|
|
21
|
+
emitter.on(e, listener);
|
|
22
|
+
function listener(...args) {
|
|
21
23
|
if (already)
|
|
22
24
|
return;
|
|
23
25
|
already = true;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
for (const e of events)
|
|
27
|
+
emitter.removeListener(e, listener);
|
|
28
|
+
resolve(args);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
26
31
|
}
|
package/src/frontEndApis.js
CHANGED
|
@@ -70,7 +70,7 @@ exports.frontEndApis = {
|
|
|
70
70
|
});
|
|
71
71
|
},
|
|
72
72
|
async get_file_details({ uris }, ctx) {
|
|
73
|
-
if (typeof
|
|
73
|
+
if (typeof uris?.[0] !== 'string')
|
|
74
74
|
return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad uris');
|
|
75
75
|
const isAdmin = (0, adminApis_1.ctxAdminAccess)(ctx);
|
|
76
76
|
return {
|
|
@@ -114,7 +114,7 @@ exports.frontEndApis = {
|
|
|
114
114
|
const node = await (0, vfs_1.urlToNode)(uri, ctx);
|
|
115
115
|
if (!node)
|
|
116
116
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
|
|
117
|
-
if ((0, vfs_1.isRoot)(node) || dest.includes('/') || (0, util_files_1.
|
|
117
|
+
if ((0, vfs_1.isRoot)(node) || dest.includes('/') || (0, util_files_1.hasDirTraversal)(dest))
|
|
118
118
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN);
|
|
119
119
|
if (!(0, vfs_1.hasPermission)(node, 'can_delete', ctx))
|
|
120
120
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
|
|
@@ -147,12 +147,12 @@ exports.frontEndApis = {
|
|
|
147
147
|
if (typeof from1 !== 'string')
|
|
148
148
|
return const_1.HTTP_BAD_REQUEST;
|
|
149
149
|
const srcNode = await (0, vfs_1.urlToNode)(from1, ctx);
|
|
150
|
-
const src = srcNode
|
|
150
|
+
const src = srcNode?.source;
|
|
151
151
|
if (!src)
|
|
152
152
|
return const_1.HTTP_NOT_FOUND;
|
|
153
153
|
const dest = (0, path_1.join)(destNode.source, (0, path_1.basename)(src));
|
|
154
154
|
if (lodash_1.default.isFunction(override))
|
|
155
|
-
return override
|
|
155
|
+
return override?.(srcNode, dest);
|
|
156
156
|
return (0, vfs_1.statusCodeForMissingPerm)(srcNode, 'can_delete', ctx)
|
|
157
157
|
|| (0, promises_1.rename)(src, dest).catch(async (e) => {
|
|
158
158
|
if (e.code !== 'EXDEV')
|
|
@@ -200,7 +200,7 @@ exports.frontEndApis = {
|
|
|
200
200
|
let bytes = 0;
|
|
201
201
|
let files = 0;
|
|
202
202
|
for await (const n of (0, vfs_1.walkNode)(folder, { ctx, onlyFiles: true, depth: Infinity })) {
|
|
203
|
-
bytes += await (0, vfs_1.nodeStats)(n).then(x =>
|
|
203
|
+
bytes += await (0, vfs_1.nodeStats)(n).then(x => x?.size || 0, () => 0);
|
|
204
204
|
files++;
|
|
205
205
|
partialFolderSize[id] = { bytes, files };
|
|
206
206
|
}
|
package/src/geo.js
CHANGED
|
@@ -20,10 +20,9 @@ setInterval(checkFiles, misc_1.DAY); // keep updated at run-time
|
|
|
20
20
|
// benchmark: memoize can make this 44x faster
|
|
21
21
|
exports.ip2country = lodash_1.default.memoize((ip) => ip2location.getCountryShortAsync(ip).then(v => v === '-' ? '' : v, () => ''));
|
|
22
22
|
const geoFilter = async (ctx, next) => {
|
|
23
|
-
var _a;
|
|
24
23
|
if (enabled.get() && !(0, misc_1.isLocalHost)(ctx) && !(0, misc_1.isIpLan)(ctx.ip)) {
|
|
25
24
|
const { connection } = ctx.state;
|
|
26
|
-
const country =
|
|
25
|
+
const country = connection.country ??= await (0, exports.ip2country)(ctx.ip);
|
|
27
26
|
if (country)
|
|
28
27
|
(0, connections_1.updateConnection)(connection, { country });
|
|
29
28
|
if (!ctx.state.skipFilters && allow.get() !== null)
|
|
@@ -37,7 +36,6 @@ function isOpen() {
|
|
|
37
36
|
return Boolean(ip2location.getPackageVersion());
|
|
38
37
|
}
|
|
39
38
|
async function checkFiles() {
|
|
40
|
-
var _a, _b;
|
|
41
39
|
if (!enabled.get())
|
|
42
40
|
return;
|
|
43
41
|
const ZIP_FILE = 'IP2LOCATION-LITE-DB1.IPV6.BIN';
|
|
@@ -57,11 +55,11 @@ async function checkFiles() {
|
|
|
57
55
|
ip2location.close();
|
|
58
56
|
await (0, promises_1.unlink)(LOCAL_FILE).catch(() => { });
|
|
59
57
|
await (0, promises_1.rename)(TEMP, LOCAL_FILE);
|
|
60
|
-
|
|
58
|
+
exports.ip2country.cache.clear?.();
|
|
61
59
|
console.log(`${name} download completed`);
|
|
62
60
|
}
|
|
63
61
|
catch (e) {
|
|
64
|
-
console.error(`Failed to download ${name}${mtime ? ", falling back on old data" : ''}:`,
|
|
62
|
+
console.error(`Failed to download ${name}${mtime ? ", falling back on old data" : ''}:`, e?.message || String(e));
|
|
65
63
|
}
|
|
66
64
|
else if (isOpen())
|
|
67
65
|
return;
|
package/src/github.js
CHANGED
|
@@ -39,15 +39,13 @@ function getGithubDefaultBranch(repo) {
|
|
|
39
39
|
if (!repo.includes('/'))
|
|
40
40
|
throw 'malformed repo';
|
|
41
41
|
return branchCache.try(repo, async () => {
|
|
42
|
-
var _a;
|
|
43
42
|
for (const b of ['main', 'master']) // try to not consume api quota
|
|
44
43
|
if (await (0, misc_1.httpString)(`https://github.com/${repo}/raw/refs/heads/${b}/dist/plugin.js`, { method: 'HEAD', noRedirect: true }).then(() => 1, () => 0))
|
|
45
44
|
return b;
|
|
46
|
-
return (
|
|
45
|
+
return (await getRepoInfo(repo))?.default_branch;
|
|
47
46
|
});
|
|
48
47
|
}
|
|
49
48
|
async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
50
|
-
var _a, _b, _c;
|
|
51
49
|
if (typeof repo !== 'string')
|
|
52
50
|
repo = repo.main;
|
|
53
51
|
if (exports.downloading[repo])
|
|
@@ -63,19 +61,19 @@ async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
|
63
61
|
if (customRepo) { // custom repo
|
|
64
62
|
if (!pl)
|
|
65
63
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
|
|
66
|
-
const customRepo = (
|
|
67
|
-
let url = customRepo
|
|
64
|
+
const customRepo = (pl.getData?.() || pl).repo;
|
|
65
|
+
let url = customRepo?.zip;
|
|
68
66
|
if (!url)
|
|
69
67
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "bad plugin");
|
|
70
68
|
if (!url.includes('//'))
|
|
71
69
|
url = customRepo.web + url;
|
|
72
|
-
return await go(url, pl
|
|
70
|
+
return await go(url, pl?.id, customRepo.zipRoot ?? DIST_ROOT);
|
|
73
71
|
}
|
|
74
|
-
branch
|
|
72
|
+
branch ||= await getGithubDefaultBranch(repo);
|
|
75
73
|
const short = repo.split('/')[1]; // second part, repo without the owner
|
|
76
74
|
if (!short)
|
|
77
75
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
|
|
78
|
-
const folder = overwrite &&
|
|
76
|
+
const folder = overwrite && pl?.id // use existing folder
|
|
79
77
|
|| (getFolder2repo().hasOwnProperty(short) ? repo.replace('/', '-') // longer form only if another plugin is using short form, to avoid overwriting
|
|
80
78
|
: short.replace(/^hfs-/, ''));
|
|
81
79
|
const GITHUB_ZIP_ROOT = short + '-' + branch; // GitHub puts everything within this folder
|
|
@@ -137,7 +135,7 @@ async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
|
137
135
|
await (0, promises_1.rename)(tempInstallPath, installPath)
|
|
138
136
|
.catch(e => { throw e.code !== 'ENOENT' ? e : new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "missing main file"); });
|
|
139
137
|
if (wasRunning)
|
|
140
|
-
if (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(folder), { timeout:
|
|
138
|
+
if (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(folder), { timeout: 10_000 }))
|
|
141
139
|
void (0, plugins_1.startPlugin)(folder) // don't wait, in case it fails to start. We still use startPlugin instead of enablePlugin, as it will take care of disabling other themes.
|
|
142
140
|
.catch(console.warn);
|
|
143
141
|
events_1.default.emit('pluginDownloaded', { id: folder, repo });
|
|
@@ -167,7 +165,7 @@ async function readOnlinePlugin(repo, branch = '') {
|
|
|
167
165
|
main = pl.repo.web + main;
|
|
168
166
|
return (0, plugins_1.parsePluginSource)(main, await (0, misc_1.httpString)(main)); // use 'repo' as 'id' client-side
|
|
169
167
|
}
|
|
170
|
-
branch
|
|
168
|
+
branch ||= await getGithubDefaultBranch(repo);
|
|
171
169
|
const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/${plugins_1.PLUGIN_MAIN_FILE}`);
|
|
172
170
|
const pl = (0, plugins_1.parsePluginSource)(repo, res); // use 'repo' as 'id' client-side
|
|
173
171
|
pl.branch = branch;
|
|
@@ -175,13 +173,13 @@ async function readOnlinePlugin(repo, branch = '') {
|
|
|
175
173
|
}
|
|
176
174
|
async function readOnlineCompatiblePlugin(repo, branch = '') {
|
|
177
175
|
const pl = await readOnlinePlugin(repo, branch);
|
|
178
|
-
if (!
|
|
176
|
+
if (!pl?.apiRequired)
|
|
179
177
|
return; // mandatory field
|
|
180
178
|
if (!pl.badApi)
|
|
181
179
|
return pl;
|
|
182
180
|
// we try other branches (starting with 'api')
|
|
183
181
|
const res = await apiGithub('repos/' + repo + '/branches');
|
|
184
|
-
const branches = res.map((x) => x
|
|
182
|
+
const branches = res.map((x) => x?.name)
|
|
185
183
|
.filter((x) => typeof x === 'string' && x.startsWith('api'))
|
|
186
184
|
.sort().reverse();
|
|
187
185
|
for (const branch of branches) {
|
|
@@ -234,7 +232,7 @@ async function* apiGithubPaginated(uri) {
|
|
|
234
232
|
}
|
|
235
233
|
}
|
|
236
234
|
async function isPluginBlacklisted(repo) {
|
|
237
|
-
return (0, exports.getProjectInfo)().then(x =>
|
|
235
|
+
return (0, exports.getProjectInfo)().then(x => x?.repo_blacklist?.[repo]?.message || '', () => undefined);
|
|
238
236
|
}
|
|
239
237
|
async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
240
238
|
// github doesn't allow complex search, so we have to do it multiple times and merge the results
|
|
@@ -245,7 +243,6 @@ async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
|
245
243
|
const list = await Promise.all(searches.map(x => (0, misc_1.asyncGeneratorToArray)(apiGithubPaginated(`search/repositories?q=topic:hfs-plugin+${x}`))));
|
|
246
244
|
const deduped = lodash_1.default.uniqBy(list.flat(), x => x.full_name);
|
|
247
245
|
return new misc_1.AsapStream(deduped.map(async (it) => {
|
|
248
|
-
var _a;
|
|
249
246
|
const repo = it.full_name;
|
|
250
247
|
if (skipRepos.includes(repo) || await isPluginBlacklisted(repo))
|
|
251
248
|
return;
|
|
@@ -255,7 +252,7 @@ async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
|
255
252
|
Object.assign(pl, {
|
|
256
253
|
repo, // overwrite parsed value, that may be wrong
|
|
257
254
|
downloading: exports.downloading[repo],
|
|
258
|
-
license:
|
|
255
|
+
license: it.license?.spdx_id,
|
|
259
256
|
}, lodash_1.default.pick(it, ['pushed_at', 'stargazers_count', 'default_branch']));
|
|
260
257
|
return pl;
|
|
261
258
|
}));
|
|
@@ -273,7 +270,7 @@ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => argv_1.argv.central ===
|
|
|
273
270
|
.then(o => {
|
|
274
271
|
if (o)
|
|
275
272
|
cachedCentralInfo.set(o);
|
|
276
|
-
o
|
|
273
|
+
o ||= { ...cachedCentralInfo.get() || builtIn }; // fall back to built-in
|
|
277
274
|
// merge byVersions info in the main object, but collect alerts separately, to preserve multiple instances
|
|
278
275
|
const allAlerts = [o.alert];
|
|
279
276
|
for (const [ver, more] of Object.entries((0, misc_1.popKey)(o, 'byVersion') || {}))
|
|
@@ -296,4 +293,4 @@ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => argv_1.argv.central ===
|
|
|
296
293
|
(0, plugins_1.enablePlugin)(p.id, false);
|
|
297
294
|
}
|
|
298
295
|
return o;
|
|
299
|
-
}), { retain: misc_1.HOUR, retainFailure:
|
|
296
|
+
}), { retain: misc_1.HOUR, retainFailure: 60_000 });
|
package/src/i18n.js
CHANGED
|
@@ -21,11 +21,10 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
21
21
|
}
|
|
22
22
|
// If one of the keys is an "id", that should be the first. If one of the keys should work as a fallback, that should be the last. Use 'fallback' parameter if you don't want the fallback to work as a key.
|
|
23
23
|
function t(keyOrTpl, params, fallback) {
|
|
24
|
-
var _a, _b;
|
|
25
24
|
if (!keyOrTpl)
|
|
26
25
|
return '';
|
|
27
26
|
// memoize?
|
|
28
|
-
const keys = isTemplateStringsArray(keyOrTpl) ? [(fallback
|
|
27
|
+
const keys = isTemplateStringsArray(keyOrTpl) ? [(fallback ??= keyOrTpl[0])]
|
|
29
28
|
: Array.isArray(keyOrTpl) ? keyOrTpl : [keyOrTpl];
|
|
30
29
|
if (typeof params === 'string' && !fallback) {
|
|
31
30
|
fallback = params;
|
|
@@ -38,7 +37,7 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
38
37
|
if (!state.disabled)
|
|
39
38
|
for (const key of keys) {
|
|
40
39
|
for (const lang of searchLangs)
|
|
41
|
-
if (found =
|
|
40
|
+
if (found = state.translations[selectedLang = lang]?.translate?.[key])
|
|
42
41
|
break;
|
|
43
42
|
if (found)
|
|
44
43
|
break;
|
|
@@ -111,7 +110,7 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
111
110
|
yield [s.slice(ofs), false];
|
|
112
111
|
}
|
|
113
112
|
function isTemplateStringsArray(x) {
|
|
114
|
-
return
|
|
113
|
+
return x?.raw && Array.isArray(x);
|
|
115
114
|
}
|
|
116
115
|
return {
|
|
117
116
|
t,
|
package/src/icons.js
CHANGED
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
|
-
var _a;
|
|
8
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
8
|
exports.app = void 0;
|
|
10
9
|
const koa_1 = __importDefault(require("koa"));
|
|
@@ -39,7 +38,7 @@ if (new config_1.Version(process.versions.node).olderThan('18.15.0')) {
|
|
|
39
38
|
process.exit(2);
|
|
40
39
|
}
|
|
41
40
|
process.title = 'HFS ' + const_1.VERSION;
|
|
42
|
-
const keys =
|
|
41
|
+
const keys = process.env.COOKIE_SIGN_KEYS?.split(',')
|
|
43
42
|
|| [(0, misc_1.randomId)(30)]; // randomness at start gives some extra security, btu also invalidates existing sessions
|
|
44
43
|
exports.app = new koa_1.default({ keys });
|
|
45
44
|
exports.app.use(middlewares_1.sessionMiddleware)
|
|
@@ -78,7 +77,7 @@ process.on('uncaughtException', (err) => {
|
|
|
78
77
|
try {
|
|
79
78
|
console.error("uncaught:", err);
|
|
80
79
|
}
|
|
81
|
-
catch
|
|
80
|
+
catch { } // in case we are writing to a closed terminal, we may throw with "write eio at afterwritedispatched", causing an infinite loop
|
|
82
81
|
});
|
|
83
82
|
// this warning is scaring users, and has been removed in node 20.12.0 https://github.com/nodejs/node/pull/51204
|
|
84
83
|
const original = process.emitWarning;
|
package/src/lang.js
CHANGED
|
@@ -24,7 +24,7 @@ function code2file(code) {
|
|
|
24
24
|
function file2code(fn) {
|
|
25
25
|
return fn.replace(PREFIX, '').replace(SUFFIX, '');
|
|
26
26
|
}
|
|
27
|
-
const cache = (0, expiringCache_1.expiringCache)(
|
|
27
|
+
const cache = (0, expiringCache_1.expiringCache)(3_000); // 3 seconds for both a good dx and acceptable performance
|
|
28
28
|
async function getLangData(ctxOrLangCsv) {
|
|
29
29
|
if (typeof ctxOrLangCsv !== 'string') {
|
|
30
30
|
const ctx = ctxOrLangCsv;
|
|
@@ -35,7 +35,6 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
35
35
|
}
|
|
36
36
|
const csv = ctxOrLangCsv.toLowerCase();
|
|
37
37
|
return cache.try(csv, async () => {
|
|
38
|
-
var _a;
|
|
39
38
|
const langs = csv.split(',');
|
|
40
39
|
const ret = {};
|
|
41
40
|
let i = 0;
|
|
@@ -47,7 +46,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
47
46
|
try {
|
|
48
47
|
ret[k] = JSON.parse(await (0, promises_1.readFile)(fn, 'utf8'));
|
|
49
48
|
} // allow external files to override embedded translations
|
|
50
|
-
catch
|
|
49
|
+
catch {
|
|
51
50
|
if ((0, misc_1.hasProp)(embedded_1.default, k))
|
|
52
51
|
ret[k] = lodash_1.default.cloneDeep(embedded_1.default[k]);
|
|
53
52
|
else {
|
|
@@ -60,9 +59,9 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
|
-
const fromPlugins = (0, misc_1.onlyTruthy)(await Promise.all((0, plugins_1.mapPlugins)(async (pl) =>
|
|
62
|
+
const fromPlugins = (0, misc_1.onlyTruthy)(await Promise.all((0, plugins_1.mapPlugins)(async (pl) => (0, misc_1.tryJson)(await (0, promises_1.readFile)((0, path_1.join)(pl.folder, fn), 'utf8').catch(() => ''))?.translate)));
|
|
64
63
|
if (fromPlugins.length)
|
|
65
|
-
lodash_1.default.defaults((
|
|
64
|
+
lodash_1.default.defaults((ret[k] ||= {}).translate ||= {}, ...fromPlugins); // be sure we have an entry for k
|
|
66
65
|
i++;
|
|
67
66
|
}
|
|
68
67
|
return ret;
|
|
@@ -71,7 +70,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
71
70
|
let forceLangData;
|
|
72
71
|
let undo;
|
|
73
72
|
(0, config_1.defineConfig)(misc_1.CFG.force_lang, '', v => {
|
|
74
|
-
undo
|
|
73
|
+
undo?.();
|
|
75
74
|
if (!v)
|
|
76
75
|
return forceLangData = undefined;
|
|
77
76
|
const translation = embedded_1.default[v];
|