hfs 3.3.4 → 3.4.0-alpha2
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/AccountsPage-CoiPofEv.js +4 -0
- package/admin/assets/ConfigFilePage-jUH2feqi.js +4 -0
- package/admin/assets/CustomHtmlPage-Pl_AJZrl.js +2 -0
- package/admin/assets/InternetPage-DIcAwhFk.js +6 -0
- package/admin/assets/LangPage-BwVY8QRD.js +2 -0
- package/admin/assets/LogsPage-CcjS12cP.js +3 -0
- package/admin/assets/MonitorPage-DMVFXyMe.js +2 -0
- package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
- package/admin/assets/PluginsPage-CC2dINcb.js +1 -0
- package/admin/assets/icons-C_ux1zLA.js +1 -0
- package/admin/assets/index-BSSQOMG_.js +755 -0
- package/admin/assets/index-mI9kZlpb.css +1 -0
- package/admin/assets/routing-Be72yFmd.js +1 -0
- package/admin/index.html +2 -2
- package/central.json +100 -1
- package/frontend/assets/{dist-legacy-CnYHQXZ6.js → dist-legacy-D7Dg5w3L.js} +1 -1
- package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
- package/frontend/assets/{lib-legacy-3xYueNHt.js → lib-legacy-BxFHpdER.js} +1 -1
- package/frontend/assets/{sha512-legacy-Cl3f_PhN.js → sha512-legacy-C8xoQBnP.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +9 -9
- package/package.json +2 -2
- package/plugins/antibrute/plugin.js +62 -15
- package/src/acme.js +143 -50
- package/src/acmeConfig.js +43 -0
- package/src/acmeDns.js +285 -0
- package/src/admin-langs/embedded.js +33 -0
- package/src/admin-langs/hfs-admin-lang-ar.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-bg.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-de.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-el.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-en.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-es.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-fi.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-fr.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-hu.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-it.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-ja.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-ko.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-lt.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-ms.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-nl.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-pl.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-pt-br.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-ro.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-ru.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-sr-latn.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-sr.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-th.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-tr.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-uk.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-vi.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-zh.json +1149 -0
- package/src/adminApis.js +2 -2
- package/src/adminLang.js +51 -0
- package/src/api.auth.js +1 -1
- package/src/api.lang.js +22 -13
- package/src/api.log.js +5 -3
- package/src/api.net.js +20 -3
- package/src/api.plugins.js +53 -19
- package/src/api.vfs.js +3 -2
- package/src/argv.js +4 -0
- package/src/auth.js +13 -9
- package/src/basicWeb.js +6 -2
- package/src/block.js +1 -1
- package/src/config.js +1 -0
- package/src/connections.js +4 -7
- package/src/consoleLog.js +4 -0
- package/src/const.js +1 -1
- package/src/cross-const.js +8 -6
- package/src/cross.js +16 -2
- package/src/customHtml.js +3 -2
- package/src/frontEndApis.js +44 -7
- package/src/github.js +6 -37
- package/src/i18n.js +16 -4
- package/src/index.js +4 -3
- package/src/lang.js +21 -9
- package/src/langs/embedded.js +3 -2
- package/src/langs/hfs-lang-ar.json +7 -2
- package/src/langs/hfs-lang-bg.json +7 -2
- package/src/langs/hfs-lang-de.json +7 -2
- package/src/langs/hfs-lang-el.json +7 -2
- package/src/langs/hfs-lang-en.json +189 -0
- package/src/langs/hfs-lang-es.json +9 -4
- package/src/langs/hfs-lang-fi.json +8 -3
- package/src/langs/hfs-lang-fr.json +12 -7
- package/src/langs/hfs-lang-hu.json +7 -2
- package/src/langs/hfs-lang-it.json +11 -5
- package/src/langs/hfs-lang-ja.json +7 -2
- package/src/langs/hfs-lang-ko.json +7 -2
- package/src/langs/hfs-lang-lt.json +7 -2
- package/src/langs/hfs-lang-ms.json +9 -2
- package/src/langs/hfs-lang-nl.json +7 -2
- package/src/langs/hfs-lang-pl.json +7 -2
- package/src/langs/hfs-lang-pt-br.json +9 -4
- package/src/langs/hfs-lang-ro.json +7 -2
- package/src/langs/hfs-lang-ru.json +9 -4
- package/src/langs/hfs-lang-sr-latn.json +7 -2
- package/src/langs/hfs-lang-sr.json +7 -2
- package/src/langs/hfs-lang-th.json +8 -3
- package/src/langs/hfs-lang-tr.json +8 -3
- package/src/langs/hfs-lang-uk.json +16 -11
- package/src/langs/hfs-lang-vi.json +7 -2
- package/src/langs/hfs-lang-zh-hant.json +189 -0
- package/src/langs/hfs-lang-zh.json +97 -95
- package/src/listen.js +6 -3
- package/src/log.js +7 -1
- package/src/middlewares.js +87 -31
- package/src/multipartUpload.js +21 -0
- package/src/perm.js +38 -20
- package/src/pluginCatalog.js +66 -0
- package/src/pluginUsage.js +64 -0
- package/src/plugins.js +14 -2
- package/src/roots.js +22 -13
- package/src/serveFile.js +28 -11
- package/src/serveGuiAndSharedFiles.js +6 -6
- package/src/serveGuiFiles.js +7 -4
- package/src/update.js +51 -18
- package/src/upload.js +49 -8
- package/src/util-http.js +31 -5
- package/src/vfs.js +93 -4
- package/src/webdav.js +14 -8
- package/admin/assets/AccountsPage-CnZGcUox.js +0 -6
- package/admin/assets/ArrayField-F7fpCJIv.js +0 -2
- package/admin/assets/ConfigFilePage-CEgZ8Wq4.js +0 -4
- package/admin/assets/CustomHtmlPage-DDUNxF1H.js +0 -2
- package/admin/assets/DataTable-D_TeYxvk.js +0 -119
- package/admin/assets/DateTimeField-CsIdkTqz.js +0 -1
- package/admin/assets/EventManager--HiXIAY0.js +0 -1
- package/admin/assets/FileField-HsDCdr5S.js +0 -117
- package/admin/assets/InternetPage-DDwKrXJH.js +0 -8
- package/admin/assets/LangPage-Dd8hi_9K.js +0 -2
- package/admin/assets/LogsPage-BcPfugNY.js +0 -4
- package/admin/assets/MenuButton-cuehhVig.js +0 -1
- package/admin/assets/MonitorPage-Bre9d6MA.js +0 -2
- package/admin/assets/OptionsPage-Dd_cRDf4.js +0 -3
- package/admin/assets/PluginsPage-DJiseD4G.js +0 -1
- package/admin/assets/Tabs-C87E85Fh.js +0 -1
- package/admin/assets/TextEditor-BeS-mR2I.css +0 -1
- package/admin/assets/TextEditor-Cr0VI9Xi.js +0 -34
- package/admin/assets/VfsPathField-D1BkfhAM.js +0 -1
- package/admin/assets/agentIcons-CDoUTPUC.js +0 -1
- package/admin/assets/baseUrl-CnrV7Huu.js +0 -2
- package/admin/assets/icons-DJJYoK9E.js +0 -1
- package/admin/assets/index-11n5z0Ez.css +0 -1
- package/admin/assets/index-DwXlUHAs.js +0 -477
- package/admin/assets/routing-BQpoUdjq.js +0 -1
- package/frontend/assets/index-legacy-JYULclbY.js +0 -1
- package/src/langs/hfs-lang-zh-tw.json +0 -183
package/src/perm.js
CHANGED
|
@@ -33,17 +33,7 @@ const auth_1 = require("./auth");
|
|
|
33
33
|
const sessionStamp_1 = require("./sessionStamp");
|
|
34
34
|
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
|
|
35
35
|
function expandUsername(who) {
|
|
36
|
-
|
|
37
|
-
const q = [who];
|
|
38
|
-
for (const u of q) {
|
|
39
|
-
const a = getAccount(u);
|
|
40
|
-
if (!a || a.disabled)
|
|
41
|
-
continue;
|
|
42
|
-
ret.add(u);
|
|
43
|
-
if (a.belongs)
|
|
44
|
-
q.push(...a.belongs);
|
|
45
|
-
}
|
|
46
|
-
return ret;
|
|
36
|
+
return new Set(Array.from(walkActiveAccounts([who]), a => a.username));
|
|
47
37
|
}
|
|
48
38
|
// check if current username or any ancestor match the provided usernames
|
|
49
39
|
function ctxBelongsTo(ctx, usernames) {
|
|
@@ -235,14 +225,29 @@ function delAccount(username) {
|
|
|
235
225
|
}
|
|
236
226
|
// get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance.
|
|
237
227
|
function getFromAccount(account, getter) {
|
|
238
|
-
const
|
|
228
|
+
const root = typeof account === 'string' ? getAccount(account) : account;
|
|
229
|
+
if (!root)
|
|
230
|
+
return;
|
|
231
|
+
let res = getter(root); // always consider direct properties; only inherited groups are filtered by active status
|
|
232
|
+
if (res !== undefined)
|
|
233
|
+
return res;
|
|
234
|
+
for (const a of walkActiveAccounts(root.belongs || [])) {
|
|
235
|
+
res = getter(a);
|
|
236
|
+
if (res !== undefined)
|
|
237
|
+
return res;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function* walkActiveAccounts(roots) {
|
|
241
|
+
const search = [...roots];
|
|
242
|
+
const visited = new Set();
|
|
239
243
|
for (const accountOrUsername of search) {
|
|
240
244
|
const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername;
|
|
241
|
-
if (!a)
|
|
245
|
+
if (!a || visited.has(a.username))
|
|
242
246
|
continue;
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
247
|
+
visited.add(a.username);
|
|
248
|
+
if (accountIsDisabled(a))
|
|
249
|
+
continue;
|
|
250
|
+
yield a;
|
|
246
251
|
if (a.belongs)
|
|
247
252
|
search.push(...a.belongs);
|
|
248
253
|
}
|
|
@@ -257,10 +262,23 @@ function accountCanLogin(account) {
|
|
|
257
262
|
return accountHasLoginMethod(account) && !accountIsDisabled(account);
|
|
258
263
|
}
|
|
259
264
|
function accountIsDisabled(account) {
|
|
260
|
-
return
|
|
261
|
-
|
|
262
|
-
|| account.
|
|
263
|
-
|
|
265
|
+
return check(account, new Set());
|
|
266
|
+
function check(account, visiting) {
|
|
267
|
+
if (account.disabled || account.expire < Date.now())
|
|
268
|
+
return true;
|
|
269
|
+
if (!account.belongs?.length)
|
|
270
|
+
return false;
|
|
271
|
+
// consider invalid membership cycles disabled so they cannot grant privileges
|
|
272
|
+
if (visiting.has(account.username))
|
|
273
|
+
return true;
|
|
274
|
+
visiting.add(account.username);
|
|
275
|
+
const ret = account.belongs.every(u => {
|
|
276
|
+
const a = getAccount(u, false);
|
|
277
|
+
return Boolean(a && check(a, visiting));
|
|
278
|
+
});
|
|
279
|
+
visiting.delete(account.username);
|
|
280
|
+
return ret;
|
|
281
|
+
}
|
|
264
282
|
}
|
|
265
283
|
function accountCanLoginAdmin(account) {
|
|
266
284
|
return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPluginCatalog = getPluginCatalog;
|
|
7
|
+
exports.searchPluginCatalog = searchPluginCatalog;
|
|
8
|
+
const github_1 = require("./github");
|
|
9
|
+
const persistence_1 = require("./persistence");
|
|
10
|
+
const const_1 = require("./const");
|
|
11
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
12
|
+
const CACHE_KEY = 'pluginCatalog';
|
|
13
|
+
let refreshing;
|
|
14
|
+
async function* getPluginCatalog() {
|
|
15
|
+
await persistence_1.storedMap.ready();
|
|
16
|
+
const saved = await persistence_1.storedMap.get(CACHE_KEY);
|
|
17
|
+
// compatibility is determined by this HFS version, including fallback api branches
|
|
18
|
+
const previous = saved?.version === const_1.VERSION ? saved : undefined;
|
|
19
|
+
const age = previous ? Date.now() - previous.updatedAt : Infinity;
|
|
20
|
+
if (previous && age < 24 * 60 * 60_000)
|
|
21
|
+
yield { ...previous, refreshing: age >= 5 * 60_000 };
|
|
22
|
+
if (age < 5 * 60_000)
|
|
23
|
+
return;
|
|
24
|
+
// all searches share the same refresh, even if the initiating client disconnects
|
|
25
|
+
refreshing ??= refresh().finally(() => { refreshing = undefined; });
|
|
26
|
+
yield { ...await refreshing, refreshing: false };
|
|
27
|
+
async function refresh() {
|
|
28
|
+
const repositories = [];
|
|
29
|
+
for await (const repo of (0, github_1.apiGithubPaginated)('search/repositories?q=topic:hfs-plugin', { requireComplete: true }))
|
|
30
|
+
repositories.push(lodash_1.default.pick(repo, ['full_name', 'description', 'pushed_at', 'default_branch', 'stargazers_count', 'license']));
|
|
31
|
+
const old = new Map(previous?.entries.map(entry => [entry.repository.full_name, entry]));
|
|
32
|
+
const entries = await Promise.all(repositories.map(async (repository) => {
|
|
33
|
+
const cached = old.get(repository.full_name);
|
|
34
|
+
let plugin = cached?.plugin;
|
|
35
|
+
if (!cached || cached.repository.pushed_at !== repository.pushed_at
|
|
36
|
+
|| cached.repository.default_branch !== repository.default_branch) {
|
|
37
|
+
plugin = await (0, github_1.readOnlineCompatiblePlugin)(repository.full_name, repository.default_branch)
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
// repositories can carry the topic without providing a plugin at the expected path
|
|
40
|
+
if (error.message !== '404')
|
|
41
|
+
throw error;
|
|
42
|
+
}) ?? null;
|
|
43
|
+
}
|
|
44
|
+
return { repository, plugin: plugin ?? null };
|
|
45
|
+
}));
|
|
46
|
+
const next = { version: const_1.VERSION, updatedAt: Date.now(), entries };
|
|
47
|
+
// never replace a complete catalog with the result of a failed refresh
|
|
48
|
+
await persistence_1.storedMap.put(CACHE_KEY, next);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function searchPluginCatalog(catalog, text = '', skipRepos = []) {
|
|
53
|
+
const central = await (0, github_1.getProjectInfo)();
|
|
54
|
+
const terms = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
55
|
+
return catalog.entries.flatMap(({ repository, plugin }) => {
|
|
56
|
+
const repo = repository.full_name;
|
|
57
|
+
const searchable = `${repo} ${repository.description || ''} ${plugin?.description || ''}`.toLowerCase();
|
|
58
|
+
if (!plugin || skipRepos.includes(repo) || central?.repo_blacklist?.[repo]?.message
|
|
59
|
+
|| !terms.every(term => searchable.includes(term)))
|
|
60
|
+
return [];
|
|
61
|
+
// API responses add live state and dependency errors; keep the persisted metadata untouched
|
|
62
|
+
return [{ ...plugin, id: repo, repo, pushed_at: repository.pushed_at,
|
|
63
|
+
stargazers_count: repository.stargazers_count, default_branch: repository.default_branch,
|
|
64
|
+
license: repository.license?.spdx_id }];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
|
+
const misc_1 = require("./misc");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const plugins_1 = require("./plugins");
|
|
7
|
+
const persistence_1 = require("./persistence");
|
|
8
|
+
const util_http_1 = require("./util-http");
|
|
9
|
+
const debounceAsync_1 = require("./debounceAsync");
|
|
10
|
+
const shareUsageStats = (0, config_1.defineConfig)(misc_1.CFG.share_usage_stats, false);
|
|
11
|
+
const installationId = persistence_1.storedMap.singleSync('pluginUsageId', '');
|
|
12
|
+
const lastReport = persistence_1.storedMap.singleSync('lastPluginUsageReport', 0);
|
|
13
|
+
const ready = Promise.all([config_1.configReady, plugins_1.pluginsScanned, installationId.ready(), lastReport.ready()]);
|
|
14
|
+
const syncPluginUsage = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
15
|
+
await ready;
|
|
16
|
+
let uuid = installationId.get();
|
|
17
|
+
if (!shareUsageStats.get()) {
|
|
18
|
+
if (!uuid)
|
|
19
|
+
return;
|
|
20
|
+
await send('DELETE', { uuid });
|
|
21
|
+
installationId.set('');
|
|
22
|
+
lastReport.set(0);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (Date.now() < lastReport.get() + misc_1.DAY)
|
|
26
|
+
return;
|
|
27
|
+
if (!uuid) {
|
|
28
|
+
uuid = (0, node_crypto_1.randomUUID)();
|
|
29
|
+
installationId.set(uuid);
|
|
30
|
+
}
|
|
31
|
+
const enabled = plugins_1.enablePlugins.get();
|
|
32
|
+
const plugins = selectReportedPlugins([
|
|
33
|
+
...(0, plugins_1.mapPlugins)((plugin, id) => ({ ...plugin.getData(), id, enabled: enabled.includes(id) }), false),
|
|
34
|
+
...(0, plugins_1.getInactivePlugins)().map(plugin => ({ ...plugin, enabled: enabled.includes(plugin.id) })),
|
|
35
|
+
]);
|
|
36
|
+
await send('POST', { uuid, hfsVersion: String(config_1.currentVersion), plugins });
|
|
37
|
+
lastReport.set(Date.now());
|
|
38
|
+
function selectReportedPlugins(installed) {
|
|
39
|
+
const selected = new Map();
|
|
40
|
+
for (const plugin of installed) {
|
|
41
|
+
const repo = typeof plugin.repo === 'string' ? plugin.repo : plugin.repo?.main;
|
|
42
|
+
if (!repo)
|
|
43
|
+
continue;
|
|
44
|
+
const current = selected.get(repo);
|
|
45
|
+
if (!current || better(plugin, current))
|
|
46
|
+
selected.set(repo, plugin);
|
|
47
|
+
}
|
|
48
|
+
return Array.from(selected, ([repo, plugin]) => ({
|
|
49
|
+
repo,
|
|
50
|
+
version: plugin.version,
|
|
51
|
+
enabled: plugin.enabled,
|
|
52
|
+
})).sort((a, b) => a.repo.localeCompare(b.repo));
|
|
53
|
+
function better(a, b) {
|
|
54
|
+
return Number(a.enabled) > Number(b.enabled)
|
|
55
|
+
|| a.enabled === b.enabled && (a.id.length < b.id.length
|
|
56
|
+
|| a.id.length === b.id.length && a.id.localeCompare(b.id) < 0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
shareUsageStats.sub(() => void syncPluginUsage().catch(() => { }));
|
|
61
|
+
(0, misc_1.repeat)(misc_1.HOUR, syncPluginUsage);
|
|
62
|
+
async function send(method, body) {
|
|
63
|
+
await (0, util_http_1.httpWithBody)(misc_1.PLUGIN_USAGE_URL + '/report', { method, body, timeout: 5000 });
|
|
64
|
+
}
|
package/src/plugins.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.getInactivePlugins = getInactivePlugins;
|
|
|
53
53
|
exports.rescan = rescan;
|
|
54
54
|
exports.parsePluginSource = parsePluginSource;
|
|
55
55
|
exports.getMissingDependencies = getMissingDependencies;
|
|
56
|
+
const acmeDns_1 = require("./acmeDns");
|
|
56
57
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
57
58
|
const watchLoad_1 = require("./watchLoad");
|
|
58
59
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -232,6 +233,13 @@ async function initPlugin(pl, morePassedToInit, onInitError) {
|
|
|
232
233
|
return result;
|
|
233
234
|
}
|
|
234
235
|
},
|
|
236
|
+
registerAcmeDnsProvider(...args) {
|
|
237
|
+
if (unloading)
|
|
238
|
+
throw Error("Plugin is unloading");
|
|
239
|
+
const unregister = (0, acmeDns_1.registerAcmeDnsProvider)(...args);
|
|
240
|
+
cleanups.push(unregister);
|
|
241
|
+
return unregister;
|
|
242
|
+
},
|
|
235
243
|
misc, _: lodash_1.default,
|
|
236
244
|
customApiCall, notifyClient: frontEndApis_1.notifyClient, addBlock: block_1.addBlock, ctxBelongsTo: perm_1.ctxBelongsTo, getConnections: connections_1.getConnections, normalizeFilename: vfs_1.normalizeFilename,
|
|
237
245
|
getCurrentUsername: auth_1.getCurrentUsername, getAccount: perm_1.getAccount, getUsernames: perm_1.getUsernames, addAccount: perm_1.addAccount, delAccount: perm_1.delAccount, updateAccount: perm_1.updateAccount, renameAccount: perm_1.renameAccount,
|
|
@@ -290,8 +298,8 @@ const pluginsMiddleware = async (ctx, next) => {
|
|
|
290
298
|
const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
|
|
291
299
|
const name = a.shift();
|
|
292
300
|
if (plugins.has(name)) { // do it only if the plugin is loaded
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
// this server-owned route is GUI content; client headers must not grant the exemption
|
|
302
|
+
ctx.state.considerAsGui = true;
|
|
295
303
|
await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
|
|
296
304
|
}
|
|
297
305
|
return;
|
|
@@ -734,6 +742,10 @@ function parsePluginSource(id, source) {
|
|
|
734
742
|
pl.depend = (0, misc_1.tryJson)(/exports.depend\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
|
|
735
743
|
|| console.warn("Plugin dependency discarded", x));
|
|
736
744
|
pl.changelog = (0, misc_1.tryJson)(/exports.changelog\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1]);
|
|
745
|
+
if (!Array.isArray(pl.changelog))
|
|
746
|
+
delete pl.changelog;
|
|
747
|
+
else if (lodash_1.default.remove(pl.changelog, entry => !Number.isFinite(entry?.version) || typeof entry?.message !== 'string').length)
|
|
748
|
+
console.warn("Invalid plugin changelog entries discarded", id);
|
|
737
749
|
if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
|
|
738
750
|
pl.apiRequired = undefined;
|
|
739
751
|
calculateBadApi(pl);
|
package/src/roots.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.rootsMiddleware = exports.roots = void 0;
|
|
6
|
+
exports.rootsMiddleware = exports.rootsHostGuard = exports.roots = void 0;
|
|
7
7
|
const config_1 = require("./config");
|
|
8
8
|
const misc_1 = require("./misc");
|
|
9
9
|
const connections_1 = require("./connections");
|
|
@@ -17,6 +17,26 @@ exports.roots = (0, config_1.defineConfig)(misc_1.CFG.roots, {}, map => {
|
|
|
17
17
|
return (host) => values[matchers.findIndex(m => m(host))];
|
|
18
18
|
});
|
|
19
19
|
const forceAddress = (0, config_1.defineConfig)(misc_1.CFG.force_address, false);
|
|
20
|
+
const rootsHostGuard = (ctx, next) => {
|
|
21
|
+
if (!ctx.path || lodash_1.default.isEmpty(exports.roots.get()))
|
|
22
|
+
return next();
|
|
23
|
+
if (ctx.path.startsWith(misc_1.SPECIAL_URI)) {
|
|
24
|
+
if (!ctx.path.startsWith(misc_1.API_URI))
|
|
25
|
+
return next();
|
|
26
|
+
const { referer } = ctx.headers;
|
|
27
|
+
const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
|
|
28
|
+
// authentication must remain reachable before ctxAdminAccess can succeed
|
|
29
|
+
if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
|
|
30
|
+
|| ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
|
|
31
|
+
return next();
|
|
32
|
+
}
|
|
33
|
+
const root = ctx.state.root = exports.roots.compiled()(ctx.host);
|
|
34
|
+
return !ctx.state.skipFilters && forceAddress.get()
|
|
35
|
+
&& root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled()
|
|
36
|
+
? (0, connections_1.disconnect)(ctx, forceAddress.key())
|
|
37
|
+
: next();
|
|
38
|
+
};
|
|
39
|
+
exports.rootsHostGuard = rootsHostGuard;
|
|
20
40
|
const rootsMiddleware = (ctx, next) => (() => {
|
|
21
41
|
if (!ctx.path) // it was once reported "Cannot read properties of null (reading 'startsWith')" but I can't reproduce it, and it shouldn't happen anyway
|
|
22
42
|
return (0, connections_1.disconnect)(ctx, 'no path'); // let's assume that such requests can't be served anyway. This will leave a trace with an ip, anyway
|
|
@@ -27,19 +47,8 @@ const rootsMiddleware = (ctx, next) => (() => {
|
|
|
27
47
|
return; // ...unless it's an api
|
|
28
48
|
params = ctx.state.params || ctx.query; // for api we'll translate params
|
|
29
49
|
changeUriParams(v => (0, misc_1.removeStarting)(ctx.state.revProxyPath, v)); // this removal must be done before adding the root; this operation doesn't conceptually belong to "roots", and it may be placed in different middleware, but it's convenient to do it here
|
|
30
|
-
const { referer } = ctx.headers;
|
|
31
|
-
const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
|
|
32
|
-
// authentication must remain reachable before ctxAdminAccess can succeed
|
|
33
|
-
if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
|
|
34
|
-
|| ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
|
|
35
|
-
return;
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
return;
|
|
39
|
-
const root = ctx.state.root = exports.roots.compiled()(ctx.host);
|
|
40
|
-
if (!ctx.state.skipFilters && forceAddress.get()
|
|
41
|
-
&& root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled())
|
|
42
|
-
return (0, connections_1.disconnect)(ctx, forceAddress.key()); // returning truthy will not call next
|
|
51
|
+
const { root } = ctx.state;
|
|
43
52
|
if (!root || root === '/')
|
|
44
53
|
return; // no transformation is required
|
|
45
54
|
changeUriParams(v => (0, misc_1.join)(root, v));
|
package/src/serveFile.js
CHANGED
|
@@ -8,6 +8,9 @@ exports.forceDownload = forceDownload;
|
|
|
8
8
|
exports.disposition = disposition;
|
|
9
9
|
exports.serveFileNode = serveFileNode;
|
|
10
10
|
exports.enforceDownloadLimits = enforceDownloadLimits;
|
|
11
|
+
exports.resolveFileMime = resolveFileMime;
|
|
12
|
+
exports.getNodeMime = getNodeMime;
|
|
13
|
+
exports.isActiveContentMime = isActiveContentMime;
|
|
11
14
|
exports.serveFile = serveFile;
|
|
12
15
|
exports.monitorAsDownload = monitorAsDownload;
|
|
13
16
|
exports.applyRange = applyRange;
|
|
@@ -26,11 +29,12 @@ const errorPages_1 = require("./errorPages");
|
|
|
26
29
|
const stream_1 = require("stream");
|
|
27
30
|
const crypto_1 = require("crypto");
|
|
28
31
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
32
|
+
const upload_1 = require("./upload");
|
|
29
33
|
const allowedReferer = (0, config_1.defineConfig)(misc_1.CFG.allowed_referer, '');
|
|
34
|
+
const requireUploadApprovalForActiveContent = (0, config_1.defineConfig)(misc_1.CFG.require_upload_approval_for_active_content, true);
|
|
30
35
|
const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
|
|
31
36
|
const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
|
|
32
37
|
const maxDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
|
|
33
|
-
const GUI_ASSET_MIME = /^(image\/|audio\/|video\/|font\/|text\/css$|(?:text|application)\/(?:javascript|ecmascript)$|application\/x-javascript$)/i;
|
|
34
38
|
function toAsciiEquivalent(s) {
|
|
35
39
|
return iconv_lite_1.default.encode(iconv_lite_1.default.decode(Buffer.from(s), 'utf-8'), 'ascii').toString().replaceAll('?', '');
|
|
36
40
|
}
|
|
@@ -49,6 +53,7 @@ async function serveFileNode(ctx, node) {
|
|
|
49
53
|
const name = (0, vfs_1.getNodeName)(node);
|
|
50
54
|
const mimeString = typeof mime === 'string' ? mime
|
|
51
55
|
: lodash_1.default.find(mime, (_val, mask) => (0, misc_1.matches)(name, mask));
|
|
56
|
+
const effectiveMime = resolveFileMime(source || '', mimeString);
|
|
52
57
|
if (allowedReferer.get()) {
|
|
53
58
|
const ref = (0, misc_1.try_)(() => new URL(ctx.get('referer') || '').host);
|
|
54
59
|
if (ref && ref !== ctx.host // automatically accept if the referer is basically the hosting domain
|
|
@@ -58,12 +63,13 @@ async function serveFileNode(ctx, node) {
|
|
|
58
63
|
ctx.state.vfsNode = node; // useful to tell service files from files shared by the user
|
|
59
64
|
const download = 'dl' in ctx.query;
|
|
60
65
|
disposition(ctx, name, download);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
if (!download && source && isActiveContentMime(effectiveMime)) {
|
|
67
|
+
if ((0, upload_1.isUploading)(source)
|
|
68
|
+
|| requireUploadApprovalForActiveContent.get() && (await (0, upload_1.getUploadMeta)(source))?.approved === false)
|
|
69
|
+
return (0, errorPages_1.sendErrorPage)(ctx, const_1.HTTP_FORBIDDEN);
|
|
70
|
+
}
|
|
71
|
+
// shared files count even when embedded: client headers cannot grant exemptions from limits or logging
|
|
72
|
+
await serveFile(ctx, source || '', effectiveMime);
|
|
67
73
|
await enforceDownloadLimits(ctx);
|
|
68
74
|
}
|
|
69
75
|
async function enforceDownloadLimits(ctx) {
|
|
@@ -75,14 +81,25 @@ const mimeCfg = (0, config_1.defineConfig)(misc_1.CFG.mime, {}, obj => {
|
|
|
75
81
|
const values = Object.values(obj);
|
|
76
82
|
return (name) => values[matchers.findIndex(matcher => matcher(name))];
|
|
77
83
|
});
|
|
84
|
+
function resolveFileMime(filePath, mime) {
|
|
85
|
+
mime ??= mimeCfg.compiled()((0, path_1.basename)(filePath));
|
|
86
|
+
return mime === undefined || mime === const_1.MIME_AUTO ? mime_types_1.default.lookup(filePath) || '' : mime;
|
|
87
|
+
}
|
|
88
|
+
function getNodeMime(node) {
|
|
89
|
+
const name = (0, vfs_1.getNodeName)(node);
|
|
90
|
+
const mime = typeof node.mime === 'string' ? node.mime
|
|
91
|
+
: lodash_1.default.find(node.mime, (_val, mask) => (0, misc_1.matches)(name, mask));
|
|
92
|
+
return resolveFileMime(node.source || '', mime);
|
|
93
|
+
}
|
|
94
|
+
function isActiveContentMime(mime) {
|
|
95
|
+
return /^(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)(?:;|$)/i.test(mime);
|
|
96
|
+
}
|
|
78
97
|
// after this number of seconds, the browser should check the server to see if there's a newer version of the file
|
|
79
98
|
const cacheControlDiskFiles = (0, config_1.defineConfig)(misc_1.CFG.cache_control_disk_files, 5);
|
|
80
99
|
async function serveFile(ctx, filePath, mime, cached) {
|
|
81
100
|
if (!filePath)
|
|
82
101
|
return;
|
|
83
|
-
mime
|
|
84
|
-
if (mime === undefined || mime === const_1.MIME_AUTO)
|
|
85
|
-
mime = mime_types_1.default.lookup(filePath) || '';
|
|
102
|
+
mime = resolveFileMime(filePath, mime);
|
|
86
103
|
if (mime)
|
|
87
104
|
ctx.type = mime;
|
|
88
105
|
if (ctx.method === 'OPTIONS') {
|
|
@@ -90,7 +107,7 @@ async function serveFile(ctx, filePath, mime, cached) {
|
|
|
90
107
|
ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
|
|
91
108
|
return;
|
|
92
109
|
}
|
|
93
|
-
if (ctx.
|
|
110
|
+
if (!ctx.state.getOrHead)
|
|
94
111
|
return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
95
112
|
try {
|
|
96
113
|
const stats = cached?.stats || await (0, util_1.promisify)(fs_1.stat)(filePath); // using fs's function instead of fs/promises, because only the former is supported by pkg
|
|
@@ -96,7 +96,7 @@ const serveSharedFiles = async (ctx, next) => {
|
|
|
96
96
|
}
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
if (/^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()
|
|
99
|
+
if (ctx.state.getOrHead && /^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
|
|
100
100
|
return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
|
|
101
101
|
let node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
102
102
|
if (!node)
|
|
@@ -123,11 +123,11 @@ const serveSharedFiles = async (ctx, next) => {
|
|
|
123
123
|
return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
if (path.endsWith('/') && !get)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
if (path.endsWith('/') && !get) // final slash needed on browsers to make resource urls working with html pages
|
|
127
|
+
node = await (0, vfs_1.getDefaultFile)(node, ctx) || node;
|
|
128
|
+
// classify the resolved default so a file can override its folder's GUI status
|
|
129
|
+
if (node.gui_asset)
|
|
130
|
+
ctx.state.considerAsGui = true;
|
|
131
131
|
if (get === 'icon')
|
|
132
132
|
return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
|
|
133
133
|
if (!(0, vfs_1.nodeIsFolder)(node))
|
package/src/serveGuiFiles.js
CHANGED
|
@@ -19,6 +19,7 @@ const customHtml_1 = require("./customHtml");
|
|
|
19
19
|
const lodash_1 = __importDefault(require("lodash"));
|
|
20
20
|
const config_1 = require("./config");
|
|
21
21
|
const lang_1 = require("./lang");
|
|
22
|
+
const adminLang_1 = require("./adminLang");
|
|
22
23
|
const upload_1 = require("./upload");
|
|
23
24
|
const icons_1 = require("./icons");
|
|
24
25
|
const middlewares_1 = require("./middlewares");
|
|
@@ -40,10 +41,10 @@ function serveStatic(uri) {
|
|
|
40
41
|
ctx.state.dontLog = true;
|
|
41
42
|
if (ctx.method === 'OPTIONS') {
|
|
42
43
|
ctx.status = const_1.HTTP_NO_CONTENT;
|
|
43
|
-
ctx.set({ Allow: 'OPTIONS, GET' });
|
|
44
|
+
ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
if (ctx.
|
|
47
|
+
if (!ctx.state.getOrHead)
|
|
47
48
|
return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
48
49
|
const serveApp = shouldServeApp(ctx);
|
|
49
50
|
const fullPath = (0, path_1.join)(root, serveApp ? '/index.html' : ctx.path);
|
|
@@ -107,7 +108,8 @@ async function treatIndex(ctx, filesUri, body) {
|
|
|
107
108
|
return !lodash_1.default.isEmpty(configs) && [name, configs];
|
|
108
109
|
})));
|
|
109
110
|
const timestamp = await getFaviconTimestamp();
|
|
110
|
-
const
|
|
111
|
+
const adminLangs = isFrontend ? [] : await (0, adminLang_1.getAdminLangs)();
|
|
112
|
+
const lang = isFrontend ? await (0, lang_1.getLangData)(ctx) : await (0, adminLang_1.getAdminLangData)(ctx, adminLangs);
|
|
111
113
|
let group = 0; // this represents the loading-group: all plugins with the same group can be loaded concurrently
|
|
112
114
|
const loadScripts = (0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((p, id) => {
|
|
113
115
|
const js = p.frontend_js?.map(f => f.includes('//') ? f : pub + id + '/' + f);
|
|
@@ -148,7 +150,8 @@ async function treatIndex(ctx, filesUri, body) {
|
|
|
148
150
|
customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']), // exclude the sections we already apply in this phase
|
|
149
151
|
...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)),
|
|
150
152
|
icons: Object.assign({}, ...(0, plugins_1.mapPlugins)(p => iconsToObj(p.icons, p.id + '/')), iconsToObj(icons_1.customizedIcons)), // name-to-uri
|
|
151
|
-
lang
|
|
153
|
+
lang,
|
|
154
|
+
...(isFrontend ? {} : { adminLangs, adminLang: adminLang_1.adminLang.get() })
|
|
152
155
|
}, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
|
|
153
156
|
document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0])
|
|
154
157
|
document.documentElement.setAttribute('browser', ${JSON.stringify((0, misc_1.shortenAgent)(ctx.get('user-agent')))})
|
package/src/update.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.getVersions = getVersions;
|
|
|
10
10
|
exports.getUpdates = getUpdates;
|
|
11
11
|
exports.localUpdateAvailable = localUpdateAvailable;
|
|
12
12
|
exports.previousAvailable = previousAvailable;
|
|
13
|
-
exports.
|
|
13
|
+
exports.updateNotSupported = updateNotSupported;
|
|
14
14
|
exports.update = update;
|
|
15
15
|
const github_1 = require("./github");
|
|
16
16
|
const const_1 = require("./const");
|
|
@@ -29,6 +29,7 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
29
29
|
const argv_1 = require("./argv");
|
|
30
30
|
const promises_2 = require("stream/promises");
|
|
31
31
|
const updateChangelog_1 = require("./updateChangelog");
|
|
32
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
32
33
|
const updateToBeta = (0, config_1.defineConfig)(misc_1.CFG.update_to_beta, false);
|
|
33
34
|
const autoCheckUpdate = (0, config_1.defineConfig)(misc_1.CFG.auto_check_update, true);
|
|
34
35
|
const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
|
|
@@ -59,15 +60,16 @@ exports.checkForUpdates = (0, misc_1.debounceAsync)(async () => {
|
|
|
59
60
|
async function getBestUpdate() {
|
|
60
61
|
return (await getUpdates(true))[0];
|
|
61
62
|
}
|
|
62
|
-
const
|
|
63
|
-
const
|
|
63
|
+
const releaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
|
|
64
|
+
const releaseAssetKeys = ['name', 'browser_download_url'];
|
|
65
|
+
const assetPrefix = `hfs-${(0, misc_1.xlate)(process.platform, { win32: 'windows', darwin: 'mac' })}-${process.arch}`;
|
|
64
66
|
const curV = config_1.currentVersion.scalar;
|
|
65
67
|
function prepareRelease(r) {
|
|
66
68
|
const v = (0, config_1.versionToScalar)(r.name);
|
|
67
|
-
return Object.assign(lodash_1.default.pick(r,
|
|
69
|
+
return Object.assign(lodash_1.default.pick(r, releaseKeys), {
|
|
68
70
|
versionScalar: v,
|
|
69
71
|
isNewer: v > curV, // make easy to know what's newer
|
|
70
|
-
assets: r.assets.map((a) => lodash_1.default.pick(a,
|
|
72
|
+
assets: r.assets.map((a) => lodash_1.default.pick(a, releaseAssetKeys))
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
async function getVersions(filter, max = 30, stopAtCurrent = false) {
|
|
@@ -91,10 +93,19 @@ async function getVersions(filter, max = 30, stopAtCurrent = false) {
|
|
|
91
93
|
async function getUpdates(strict = false) {
|
|
92
94
|
console.log("Checking for updates");
|
|
93
95
|
void (0, github_1.getProjectInfo)(); // also check for alerts and print them asap in the console
|
|
94
|
-
const stable = prepareRelease(await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/latest'));
|
|
95
96
|
const includeBetas = updateToBeta.get() || const_1.RUNNING_BETA;
|
|
96
|
-
|
|
97
|
-
const
|
|
97
|
+
const latest = await (0, misc_1.httpStream)(`https://github.com/${const_1.HFS_REPO}/releases/latest`, { method: 'HEAD', noRedirect: true });
|
|
98
|
+
const stableTag = latest.headers.location?.split('/').at(-1);
|
|
99
|
+
if (!stableTag)
|
|
100
|
+
throw Error('Cannot determine latest stable version');
|
|
101
|
+
if ((0, config_1.versionToScalar)(stableTag) === curV && !includeBetas)
|
|
102
|
+
return [];
|
|
103
|
+
let stable = prepareRelease({ prerelease: false, tag_name: stableTag, name: stableTag, body: '', assets: [probableAsset(stableTag)], isNewer: false, versionScalar: 0 });
|
|
104
|
+
if (stable.isNewer || const_1.RUNNING_BETA && !strict)
|
|
105
|
+
stable = await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/tags/' + stableTag).then(prepareRelease).catch(() => stable);
|
|
106
|
+
const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer))
|
|
107
|
+
// preserve update detection when GitHub's REST quota is exhausted
|
|
108
|
+
.catch(() => getVersionsFromAtom(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer)));
|
|
98
109
|
if (stable.isNewer || const_1.RUNNING_BETA && !strict)
|
|
99
110
|
betas.push(stable);
|
|
100
111
|
if (stable.isNewer && stable.body) {
|
|
@@ -102,7 +113,28 @@ async function getUpdates(strict = false) {
|
|
|
102
113
|
.catch(() => []); // missing history must not prevent installing an available update
|
|
103
114
|
stable.body = (0, updateChangelog_1.updateChangelog)(stable, history, curV);
|
|
104
115
|
}
|
|
105
|
-
return betas;
|
|
116
|
+
return lodash_1.default.sortBy(betas, x => -x.versionScalar);
|
|
117
|
+
async function getVersionsFromAtom(filter) {
|
|
118
|
+
const feed = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false })
|
|
119
|
+
.parse(await (0, misc_1.httpString)(`https://github.com/${const_1.HFS_REPO}/releases.atom`))?.feed;
|
|
120
|
+
return lodash_1.default.sortBy(lodash_1.default.castArray(feed?.entry)
|
|
121
|
+
.map((x) => prepareRelease({
|
|
122
|
+
prerelease: /^v?\d+\.\d+\.\d+-/.test(x.title),
|
|
123
|
+
tag_name: x.id.split('/').at(-1),
|
|
124
|
+
name: x.title,
|
|
125
|
+
body: x.content?.['#text'] || '',
|
|
126
|
+
assets: [probableAsset(x.id.split('/').at(-1))],
|
|
127
|
+
isNewer: false,
|
|
128
|
+
versionScalar: 0,
|
|
129
|
+
}))
|
|
130
|
+
.filter(x => Number.isFinite(x.versionScalar) && !x.name.endsWith('-ignore')
|
|
131
|
+
&& x.versionScalar !== curV && filter(x)), x => -x.versionScalar);
|
|
132
|
+
}
|
|
133
|
+
function probableAsset(tag) {
|
|
134
|
+
const name = `${assetPrefix}-${tag.replace(/^v/, '')}.zip`;
|
|
135
|
+
// release-assets.yml uses this predictable name, keeping degraded checks installable
|
|
136
|
+
return { name, browser_download_url: `https://github.com/${const_1.HFS_REPO}/releases/download/${tag}/${name}` };
|
|
137
|
+
}
|
|
106
138
|
}
|
|
107
139
|
const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
|
|
108
140
|
const INSTALLED_FN = 'hfs-installed.zip';
|
|
@@ -113,13 +145,16 @@ function localUpdateAvailable() {
|
|
|
113
145
|
function previousAvailable() {
|
|
114
146
|
return (0, misc_1.exists)(PREVIOUS_FN);
|
|
115
147
|
}
|
|
116
|
-
function
|
|
117
|
-
return
|
|
148
|
+
function updateNotSupported() {
|
|
149
|
+
return process.env.DISABLE_UPDATE ? "Automatic updates are disabled"
|
|
150
|
+
: !(argv_1.argv.forceupdate || const_1.IS_BINARY) ? "Only binary versions support automatic updates"
|
|
151
|
+
: (0, promises_1.access)((0, path_1.dirname)(process.execPath), fs_1.constants.W_OK | fs_1.constants.X_OK)
|
|
152
|
+
.then(() => '', () => `Executable directory is not writable: ${(0, path_1.dirname)(process.execPath)}`);
|
|
118
153
|
}
|
|
119
154
|
async function update(tagOrUrl = '') {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
155
|
+
const error = await updateNotSupported();
|
|
156
|
+
if (error)
|
|
157
|
+
throw error;
|
|
123
158
|
let url = tagOrUrl.includes('://') && tagOrUrl;
|
|
124
159
|
if (tagOrUrl === const_1.PREVIOUS_TAG)
|
|
125
160
|
await (0, promises_1.rename)(PREVIOUS_FN, LOCAL_UPDATE);
|
|
@@ -135,11 +170,9 @@ async function update(tagOrUrl = '') {
|
|
|
135
170
|
});
|
|
136
171
|
if (!update)
|
|
137
172
|
throw "No update has been found";
|
|
138
|
-
const
|
|
139
|
-
const assetSearch = `${plat}-${process.arch}`;
|
|
140
|
-
const asset = update.assets.find((x) => x.name.includes(assetSearch) && x.name.endsWith('.zip'));
|
|
173
|
+
const asset = update.assets.find((x) => x.name.includes(assetPrefix) && x.name.endsWith('.zip'));
|
|
141
174
|
if (!asset)
|
|
142
|
-
throw `Asset not found: ${
|
|
175
|
+
throw `Asset not found: ${assetPrefix}`;
|
|
143
176
|
url = asset.browser_download_url;
|
|
144
177
|
}
|
|
145
178
|
if (url) {
|