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.
Files changed (71) hide show
  1. package/admin/assets/{index-CHMhmNYS.js → index-DSkhXlWX.js} +103 -103
  2. package/admin/assets/{sha512-Dsv49IAs.js → sha512-CoddZKeD.js} +1 -1
  3. package/admin/index.html +1 -1
  4. package/frontend/assets/{index-legacy-BI8icwFp.js → index-legacy-C4Gclsbz.js} +4 -4
  5. package/frontend/assets/{sha512-legacy-D82alHBq.js → sha512-legacy-C-FMk44u.js} +1 -1
  6. package/frontend/index.html +1 -1
  7. package/package.json +4 -3
  8. package/src/QuickZipStream.js +11 -8
  9. package/src/SendList.js +10 -8
  10. package/src/ThrottledStream.js +8 -6
  11. package/src/acme.js +5 -7
  12. package/src/adminApis.js +6 -7
  13. package/src/api.accounts.js +4 -6
  14. package/src/api.auth.js +7 -11
  15. package/src/api.get_file_list.js +11 -16
  16. package/src/api.lang.js +1 -1
  17. package/src/api.monitor.js +2 -3
  18. package/src/api.net.js +1 -2
  19. package/src/api.plugins.js +8 -10
  20. package/src/api.vfs.js +14 -19
  21. package/src/apiMiddleware.js +4 -4
  22. package/src/auth.js +6 -9
  23. package/src/basicWeb.js +1 -2
  24. package/src/block.js +1 -1
  25. package/src/commands.js +9 -9
  26. package/src/comments.js +4 -5
  27. package/src/config.js +8 -9
  28. package/src/connections.js +11 -6
  29. package/src/consoleLog.js +6 -6
  30. package/src/const.js +4 -4
  31. package/src/cross.js +19 -21
  32. package/src/customHtml.js +3 -4
  33. package/src/ddns.js +4 -4
  34. package/src/debounceAsync.js +3 -3
  35. package/src/errorPages.js +2 -3
  36. package/src/events.js +7 -10
  37. package/src/fileAttr.js +6 -6
  38. package/src/first.js +13 -8
  39. package/src/frontEndApis.js +5 -5
  40. package/src/geo.js +3 -5
  41. package/src/github.js +14 -17
  42. package/src/i18n.js +3 -4
  43. package/src/icons.js +1 -1
  44. package/src/index.js +2 -3
  45. package/src/lang.js +5 -6
  46. package/src/listen.js +25 -28
  47. package/src/log.js +18 -17
  48. package/src/middlewares.js +12 -15
  49. package/src/misc.js +9 -11
  50. package/src/nat.js +14 -16
  51. package/src/outboundProxy.js +7 -7
  52. package/src/perm.js +7 -12
  53. package/src/persistence.js +1 -1
  54. package/src/plugins.js +62 -73
  55. package/src/roots.js +1 -2
  56. package/src/selfCheck.js +5 -4
  57. package/src/serveFile.js +6 -7
  58. package/src/serveGuiAndSharedFiles.js +7 -9
  59. package/src/serveGuiFiles.js +11 -20
  60. package/src/stat.js +40 -0
  61. package/src/statWorker.js +11 -0
  62. package/src/throttler.js +5 -7
  63. package/src/update.js +7 -7
  64. package/src/upload.js +11 -13
  65. package/src/util-files.js +42 -32
  66. package/src/util-http.js +80 -29
  67. package/src/util-os.js +2 -4
  68. package/src/vfs.js +77 -68
  69. package/src/walkDir.js +9 -9
  70. package/src/watchLoad.js +4 -4
  71. package/src/zip.js +5 -6
package/src/stat.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getStatWorker = getStatWorker;
4
+ const node_worker_threads_1 = require("node:worker_threads");
5
+ const node_fs_1 = require("node:fs");
6
+ const cross_1 = require("./cross");
7
+ // all stat requests for the same worker are serialized, potentially introducing extra latency
8
+ const pool = new Map();
9
+ function getStatWorker(key) {
10
+ const existing = pool.get(key);
11
+ if (existing)
12
+ return existing;
13
+ const worker = new node_worker_threads_1.Worker(__dirname + '/statWorker.js');
14
+ worker.unref();
15
+ const requests = new Map();
16
+ worker.on('message', (msg) => {
17
+ const k = msg.path;
18
+ requests.get(k)?.resolve(msg.error ? Promise.reject(new Error(msg.error))
19
+ : Object.setPrototypeOf(msg.result, node_fs_1.Stats.prototype));
20
+ requests.delete(k);
21
+ });
22
+ worker.on('error', (err) => {
23
+ for (const p of requests.values())
24
+ p.reject(err);
25
+ requests.clear();
26
+ worker.terminate().catch(() => { });
27
+ pool.delete(key);
28
+ });
29
+ pool.set(key, query);
30
+ return query;
31
+ function query(path) {
32
+ const was = requests.get(path);
33
+ if (was)
34
+ return was;
35
+ const ret = (0, cross_1.pendingPromise)();
36
+ requests.set(path, ret);
37
+ worker.postMessage(path);
38
+ return ret;
39
+ }
40
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ const { parentPort } = require('node:worker_threads');
3
+ const { statSync } = require('node:fs');
4
+ parentPort.on('message', async (path) => {
5
+ try { // we use statSync to not use (and not risk to saturate) libuv's thread pool
6
+ parentPort.postMessage({ path, result: { ...statSync(path) } });
7
+ }
8
+ catch (err) {
9
+ parentPort.postMessage({ path, error: err?.message || String(err) });
10
+ }
11
+ });
package/src/throttler.js CHANGED
@@ -26,8 +26,6 @@ maxKbpsPerIp.sub(v => {
26
26
  group.updateLimit(v);
27
27
  });
28
28
  const throttler = async (ctx, next) => {
29
- var _a;
30
- var _b;
31
29
  await next();
32
30
  let { body } = ctx;
33
31
  const downloadTotal = ctx.response.length;
@@ -36,11 +34,11 @@ const throttler = async (ctx, next) => {
36
34
  if (!body || !(body instanceof stream_1.Readable))
37
35
  return;
38
36
  // we wrap the stream also for unlimited connections to get speed and other features
39
- const noLimit = ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.ignore_limits) || (0, misc_1.isLocalHost)(ctx);
40
- const ipGroup = ip2group[_b = noLimit ? '' : ctx.ip] || (ip2group[_b] = {
37
+ const noLimit = ctx.state.account?.ignore_limits || (0, misc_1.isLocalHost)(ctx);
38
+ const ipGroup = ip2group[noLimit ? '' : ctx.ip] ||= {
41
39
  count: 0,
42
40
  group: new ThrottledStream_1.ThrottleGroup(noLimit ? Infinity : maxKbpsPerIp.get(), noLimit ? undefined : mainThrottleGroup),
43
- });
41
+ };
44
42
  const conn = (0, connections_1.getConnection)(ctx);
45
43
  if (!conn)
46
44
  throw 'assert throttler connection';
@@ -95,12 +93,12 @@ setInterval(() => {
95
93
  last = now;
96
94
  {
97
95
  const v = exports.totalSent.get();
98
- exports.totalOutSpeed = roundSpeed((v - (lastSent !== null && lastSent !== void 0 ? lastSent : v)) / past);
96
+ exports.totalOutSpeed = roundSpeed((v - (lastSent ?? v)) / past);
99
97
  lastSent = v;
100
98
  }
101
99
  {
102
100
  const v = exports.totalGot.get();
103
- exports.totalInSpeed = roundSpeed((v - (lastGot !== null && lastGot !== void 0 ? lastGot : v)) / past);
101
+ exports.totalInSpeed = roundSpeed((v - (lastGot ?? v)) / past);
104
102
  lastGot = v;
105
103
  }
106
104
  }, 1000);
package/src/update.js CHANGED
@@ -52,7 +52,7 @@ setInterval((0, misc_1.debounceAsync)(async () => {
52
52
  exports.autoCheckUpdateResult.set(u);
53
53
  lastCheckUpdate.set(Date.now());
54
54
  }
55
- catch (_a) { }
55
+ catch { }
56
56
  }), misc_1.HOUR);
57
57
  const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
58
58
  const ReleaseAssetKeys = ['name', 'browser_download_url'];
@@ -75,7 +75,7 @@ async function getVersions(interrupt) {
75
75
  const rel = prepareRelease(x);
76
76
  if (rel.versionScalar === curV)
77
77
  continue;
78
- if (interrupt === null || interrupt === void 0 ? void 0 : interrupt(rel))
78
+ if (interrupt?.(rel))
79
79
  break; // avoid fetching too much data
80
80
  ret.push(rel);
81
81
  }
@@ -154,7 +154,7 @@ async function update(tagOrUrl = '') {
154
154
  const { mode } = await (0, misc_1.statWithTimeout)(bin);
155
155
  await (0, promises_1.chmod)(newBin, mode).catch(console.error);
156
156
  }
157
- await (0, promises_1.rename)(INSTALLED_FN, PREVIOUS_FN).catch(e => (e === null || e === void 0 ? void 0 : e.code) !== 'ENOENT' && console.warn(String(e)));
157
+ await (0, promises_1.rename)(INSTALLED_FN, PREVIOUS_FN).catch(e => e?.code !== 'ENOENT' && console.warn(String(e)));
158
158
  await (0, promises_1.rename)(LOCAL_UPDATE, INSTALLED_FN).catch(console.warn);
159
159
  (0, first_1.onProcessExit)(() => {
160
160
  const oldBinFile = 'old-' + binFile;
@@ -162,7 +162,7 @@ async function update(tagOrUrl = '') {
162
162
  try {
163
163
  (0, fs_1.unlinkSync)(oldBin);
164
164
  }
165
- catch (_a) { }
165
+ catch { }
166
166
  (0, fs_1.renameSync)(bin, oldBin);
167
167
  console.log("launching new version in background", newBinFile);
168
168
  launch(newBin, ['--updating', binFile, '--cwd .'], { sync: true }); // sync necessary to work on Mac by double-click
@@ -172,11 +172,11 @@ async function update(tagOrUrl = '') {
172
172
  }
173
173
  catch (e) {
174
174
  plugins_1.pluginsWatcher.unpause();
175
- throw (e === null || e === void 0 ? void 0 : e.message) || String(e);
175
+ throw e?.message || String(e);
176
176
  }
177
177
  }
178
178
  function launch(cmd, pars = [], options) {
179
- return ((options === null || options === void 0 ? void 0 : options.sync) ? child_process_1.spawnSync : child_process_1.spawn)((0, util_os_1.cmdEscape)(cmd), pars, { detached: true, shell: true, stdio: [0, 1, 2], ...options });
179
+ return (options?.sync ? child_process_1.spawnSync : child_process_1.spawn)((0, util_os_1.cmdEscape)(cmd), pars, { detached: true, shell: true, stdio: [0, 1, 2], ...options });
180
180
  }
181
181
  if (argv_1.argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
182
182
  const bin = process.execPath;
@@ -195,7 +195,7 @@ if (argv_1.argv.updating) { // we were launched with a temporary name, restore o
195
195
  try {
196
196
  (0, fs_1.writeFileSync)(const_1.ARGS_FILE, JSON.stringify(['--updated', '--cwd', process.cwd().replaceAll(' ', '\\ ')]));
197
197
  }
198
- catch (_a) { }
198
+ catch { }
199
199
  void (0, open_1.default)(dest);
200
200
  }
201
201
  process.exit();
package/src/upload.js CHANGED
@@ -23,7 +23,7 @@ const events_1 = __importDefault(require("./events"));
23
23
  const promises_1 = require("fs/promises");
24
24
  const expiringCache_1 = require("./expiringCache");
25
25
  const first_1 = require("./first");
26
- exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after', 86400);
26
+ exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after', 86_400);
27
27
  exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
28
28
  exports.dontOverwriteUploading = (0, config_1.defineConfig)('dont_overwrite_uploading', true);
29
29
  const waitingToBeDeleted = {};
@@ -35,7 +35,7 @@ const waitingToBeDeleted = {};
35
35
  try {
36
36
  fs_1.default.rmSync(path, { force: true });
37
37
  }
38
- catch (_a) { }
38
+ catch { }
39
39
  });
40
40
  const ATTR_UPLOADER = 'uploader';
41
41
  function getUploadMeta(path) {
@@ -50,11 +50,11 @@ function setUploadMeta(path, ctx) {
50
50
  function getUploadTempFor(fullPath) {
51
51
  return (0, path_1.join)((0, path_1.dirname)(fullPath), 'hfs$upload-' + (0, path_1.basename)(fullPath).slice(-200));
52
52
  }
53
- const diskSpaceCache = (0, expiringCache_1.expiringCache)(3000); // invalidate shortly
53
+ const diskSpaceCache = (0, expiringCache_1.expiringCache)(3_000); // invalidate shortly
54
54
  const uploadingFiles = new Map();
55
55
  // stay sync because we use this function with formidable()
56
56
  function uploadWriter(base, baseUri, path, ctx) {
57
- if ((0, misc_1.dirTraversal)(path))
57
+ if ((0, misc_1.hasDirTraversal)(path))
58
58
  return fail(const_1.HTTP_FOOL);
59
59
  if ((0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx))
60
60
  return fail();
@@ -77,7 +77,7 @@ function uploadWriter(base, baseUri, path, ctx) {
77
77
  try {
78
78
  // refer to the source of the closest node that actually belongs to the vfs, so that cache is more effective
79
79
  let closestVfsNode = base; // if base=root, there's no parent and no original
80
- while ((closestVfsNode === null || closestVfsNode === void 0 ? void 0 : closestVfsNode.parent) && !closestVfsNode.original)
80
+ while (closestVfsNode?.parent && !closestVfsNode.original)
81
81
  closestVfsNode = closestVfsNode.parent; // if it's not original, it surely has a parent
82
82
  const dirToCheck = closestVfsNode.source;
83
83
  const res = diskSpaceCache.try(dirToCheck, () => (0, util_os_1.getDiskSpaceSync)(dirToCheck));
@@ -108,7 +108,7 @@ function uploadWriter(base, baseUri, path, ctx) {
108
108
  try {
109
109
  fs_1.default.accessSync(tempName, fs_1.default.constants.W_OK);
110
110
  }
111
- catch (_a) {
111
+ catch {
112
112
  try {
113
113
  fs_1.default.closeSync(fs_1.default.openSync(tempName, 'w'));
114
114
  fs_1.default.unlinkSync(tempName);
@@ -118,7 +118,7 @@ function uploadWriter(base, baseUri, path, ctx) {
118
118
  }
119
119
  }
120
120
  const stats = (0, misc_1.try_)(() => fs_1.default.statSync(tempName));
121
- const resumableSize = (stats === null || stats === void 0 ? void 0 : stats.size) || 0;
121
+ const resumableSize = stats?.size || 0;
122
122
  // checks for resume feature
123
123
  const par = String(ctx.query.resume || '');
124
124
  const resume = parseInt(par) || 0;
@@ -136,13 +136,13 @@ function uploadWriter(base, baseUri, path, ctx) {
136
136
  // append if resuming
137
137
  if (!resume && stats)
138
138
  fs_1.default.unlinkSync(tempName);
139
- const writeStream = (0, misc_1.createStreamLimiter)(contentLength !== null && contentLength !== void 0 ? contentLength : Infinity);
139
+ const writeStream = (0, misc_1.createStreamLimiter)(contentLength ?? Infinity);
140
140
  const fullSize = stillToWrite + resume;
141
141
  ctx.state.uploadDestinationPath = tempName;
142
142
  // allow plugins to mess with the write-stream, because the read-stream can be complicated in case of multipart
143
143
  const obj = { ctx, writeStream, fullPath, tempName, resume, fullSize, uri: '' };
144
144
  const resEvent = events_1.default.emit('uploadStart', obj);
145
- if (resEvent === null || resEvent === void 0 ? void 0 : resEvent.isDefaultPrevented())
145
+ if (resEvent?.isDefaultPrevented())
146
146
  return;
147
147
  const fileStream = fs_1.default.createWriteStream(tempName, resume ? { flags: 'r+', start: resume } : undefined);
148
148
  writeStream.on('error', e => {
@@ -256,8 +256,7 @@ function uploadWriter(base, baseUri, path, ctx) {
256
256
  return false;
257
257
  }
258
258
  function delayedDelete(path, secs) {
259
- var _a;
260
- clearTimeout((_a = waitingToBeDeleted[path]) === null || _a === void 0 ? void 0 : _a.timeout);
259
+ clearTimeout(waitingToBeDeleted[path]?.timeout);
261
260
  return waitingToBeDeleted[path] = {
262
261
  mtime,
263
262
  expires: Date.now() + secs * 1000,
@@ -268,8 +267,7 @@ function uploadWriter(base, baseUri, path, ctx) {
268
267
  };
269
268
  }
270
269
  function cancelDeletion(path) {
271
- var _a;
272
- clearTimeout((_a = waitingToBeDeleted[path]) === null || _a === void 0 ? void 0 : _a.timeout);
270
+ clearTimeout(waitingToBeDeleted[path]?.timeout);
273
271
  delete waitingToBeDeleted[path];
274
272
  }
275
273
  function releaseFile() {
package/src/util-files.js CHANGED
@@ -7,18 +7,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseFileCache = void 0;
8
8
  exports.statWithTimeout = statWithTimeout;
9
9
  exports.isDirectory = isDirectory;
10
- exports.readFileBusy = readFileBusy;
10
+ exports.readFileWithBusyRetry = readFileWithBusyRetry;
11
11
  exports.watchDir = watchDir;
12
- exports.dirTraversal = dirTraversal;
13
- exports.adjustStaticPathForGlob = adjustStaticPathForGlob;
12
+ exports.hasDirTraversal = hasDirTraversal;
13
+ exports.escapeGlobPath = escapeGlobPath;
14
14
  exports.unzip = unzip;
15
- exports.prepareFolder = prepareFolder;
15
+ exports.ensureParentFolder = ensureParentFolder;
16
16
  exports.createFileWithPath = createFileWithPath;
17
- exports.safeWriteStream = safeWriteStream;
17
+ exports.createSafeWriteStream = createSafeWriteStream;
18
18
  exports.isValidFileName = isValidFileName;
19
19
  exports.exists = exists;
20
+ exports.loadFileCached = loadFileCached;
20
21
  exports.parseFile = parseFile;
21
- exports.parseFileContent = parseFileContent;
22
22
  const promises_1 = require("fs/promises");
23
23
  const cross_1 = require("./cross");
24
24
  const config_1 = require("./config");
@@ -27,24 +27,34 @@ const path_1 = require("path");
27
27
  const fast_glob_1 = __importDefault(require("fast-glob"));
28
28
  const const_1 = require("./const");
29
29
  const events_1 = require("events");
30
+ const stat_1 = require("./stat");
30
31
  // @ts-ignore
31
32
  const unzip_stream_1 = __importDefault(require("unzip-stream"));
32
33
  const fileTimeout = (0, config_1.defineConfig)('file_timeout', 3, x => x * 1000);
33
- function statWithTimeout(path) {
34
- return (0, cross_1.haveTimeout)(fileTimeout.compiled(), (0, promises_1.stat)(path));
34
+ // a smart (and a bit arbitrary) way to decide if we need the stat-workers functionality. Without it, we may be a bit faster. We'll see with experience if we need a dedicated configuration.
35
+ const disableStatWorkers = Number(process.env.UV_THREADPOOL_SIZE) >= 10;
36
+ // some paths (mostly offline networked ones) can take 20+ seconds to fail
37
+ async function statWithTimeout(path) {
38
+ // with just 4 requests we saturate the default UV_THREADPOOL_SIZE, blocking even local fs operations, so we move all UNC requests to worker threads
39
+ const workerKey = !disableStatWorkers && const_1.IS_WINDOWS && getUncHost(path); // pool requests by server name
40
+ const op = workerKey ? (0, stat_1.getStatWorker)(workerKey)(path) : (0, promises_1.stat)(path);
41
+ return (0, cross_1.haveTimeout)(fileTimeout.compiled(), op);
42
+ }
43
+ function getUncHost(path) {
44
+ return /^\\\\([^\\]+)\\/.exec(path)?.[1];
35
45
  }
36
46
  async function isDirectory(path) {
37
47
  try {
38
48
  return (await statWithTimeout(path)).isDirectory();
39
49
  }
40
- catch (_a) { }
50
+ catch { }
41
51
  }
42
- async function readFileBusy(path) {
52
+ async function readFileWithBusyRetry(path) {
43
53
  return (0, promises_1.readFile)(path, 'utf8').catch(e => {
44
- if ((e === null || e === void 0 ? void 0 : e.code) !== 'EBUSY')
54
+ if (e?.code !== 'EBUSY')
45
55
  throw e;
46
56
  console.debug('busy');
47
- return (0, cross_1.wait)(100).then(() => readFileBusy(path));
57
+ return (0, cross_1.wait)(100).then(() => readFileWithBusyRetry(path));
48
58
  });
49
59
  }
50
60
  function watchDir(dir, cb, atStart = false) {
@@ -53,7 +63,7 @@ function watchDir(dir, cb, atStart = false) {
53
63
  try {
54
64
  watcher = (0, fs_1.watch)(dir, controlledCb);
55
65
  }
56
- catch (_a) {
66
+ catch {
57
67
  // failing watching the content of the dir, we try to monitor its parent, but filtering events only for our target dir
58
68
  const base = (0, path_1.basename)(dir);
59
69
  try {
@@ -64,7 +74,7 @@ function watchDir(dir, cb, atStart = false) {
64
74
  watcher.close(); // if we succeed, we give up the parent watching
65
75
  watcher = (0, fs_1.watch)(dir, controlledCb); // attempt at passing to a more specific watching
66
76
  }
67
- catch (_a) { }
77
+ catch { }
68
78
  controlledCb();
69
79
  });
70
80
  }
@@ -76,7 +86,7 @@ function watchDir(dir, cb, atStart = false) {
76
86
  controlledCb();
77
87
  return {
78
88
  working() { return Boolean(watcher); },
79
- stop() { watcher === null || watcher === void 0 ? void 0 : watcher.close(); },
89
+ stop() { watcher?.close(); },
80
90
  pause() { paused = true; },
81
91
  unpause() { paused = false; },
82
92
  };
@@ -85,29 +95,29 @@ function watchDir(dir, cb, atStart = false) {
85
95
  cb();
86
96
  }
87
97
  }
88
- function dirTraversal(s) {
98
+ function hasDirTraversal(s) {
89
99
  return s && /(^|[/\\])\.\.($|[/\\])/.test(s);
90
100
  }
91
101
  // apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
92
- function adjustStaticPathForGlob(path) {
102
+ function escapeGlobPath(path) {
93
103
  return fast_glob_1.default.escapePath(path.replace(/\\/g, '/'));
94
104
  }
95
105
  async function unzip(stream, cb) {
96
- let pending = Promise.resolve();
106
+ let chain = Promise.resolve();
97
107
  return new Promise((resolve, reject) => stream.pipe(unzip_stream_1.default.Parse())
98
- .on('end', () => pending.then(resolve))
108
+ .on('end', () => chain.then(resolve))
99
109
  .on('error', reject)
100
- .on('entry', (entry) => pending = pending.then(async () => {
110
+ .on('entry', (entry) => chain = chain.then(async () => {
101
111
  const { path, type } = entry;
102
112
  const dest = await (0, cross_1.try_)(() => cb(path), e => console.warn(String(e)));
103
113
  if (!dest || type !== 'File')
104
114
  return entry.autodrain();
105
115
  console.debug('unzip', dest);
106
- const thisFile = entry.pipe(await safeWriteStream(dest));
116
+ const thisFile = entry.pipe(await createSafeWriteStream(dest));
107
117
  await (0, events_1.once)(thisFile, 'finish');
108
118
  })));
109
119
  }
110
- async function prepareFolder(path, dirnameIt = true) {
120
+ async function ensureParentFolder(path, dirnameIt = true) {
111
121
  if (dirnameIt)
112
122
  path = (0, path_1.dirname)(path);
113
123
  if ((0, cross_1.isWindowsDrive)(path))
@@ -116,7 +126,7 @@ async function prepareFolder(path, dirnameIt = true) {
116
126
  await (0, promises_1.mkdir)(path, { recursive: true });
117
127
  return true;
118
128
  }
119
- catch (_a) {
129
+ catch {
120
130
  return false;
121
131
  }
122
132
  }
@@ -126,13 +136,13 @@ function createFileWithPath(path, options) {
126
136
  try {
127
137
  (0, fs_1.mkdirSync)(folder, { recursive: true });
128
138
  }
129
- catch (_a) {
139
+ catch {
130
140
  return;
131
141
  }
132
142
  return (0, fs_1.createWriteStream)(path, options);
133
143
  }
134
- async function safeWriteStream(path, options) {
135
- await prepareFolder(path);
144
+ async function createSafeWriteStream(path, options) {
145
+ await ensureParentFolder(path);
136
146
  return new Promise((resolve, reject) => {
137
147
  const first = (0, fs_1.createWriteStream)(path, options)
138
148
  .on('open', () => resolve(first))
@@ -142,7 +152,7 @@ async function safeWriteStream(path, options) {
142
152
  if (typeof options === 'string')
143
153
  options = { encoding: options };
144
154
  else
145
- options || (options = {});
155
+ options ||= {};
146
156
  if (options.flags && options.flags !== 'w') // we only handle the 'w' case
147
157
  return reject(e);
148
158
  options.flags = 'r+';
@@ -153,22 +163,22 @@ async function safeWriteStream(path, options) {
153
163
  });
154
164
  }
155
165
  function isValidFileName(name) {
156
- return !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !dirTraversal(name);
166
+ return !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !hasDirTraversal(name);
157
167
  }
158
168
  function exists(path) {
159
169
  return (0, promises_1.access)(path).then(() => true, () => false);
160
170
  }
161
171
  // parse a file, caching unless timestamp has changed
162
172
  exports.parseFileCache = new Map();
163
- async function parseFile(path, parse) {
173
+ async function loadFileCached(path, loader) {
164
174
  const { mtime: ts } = await statWithTimeout(path);
165
175
  const cached = exports.parseFileCache.get(path);
166
176
  if (cached && Number(ts) === Number(cached.ts))
167
177
  return cached.parsed;
168
- const parsed = parse(path);
178
+ const parsed = loader(path);
169
179
  exports.parseFileCache.set(path, { ts, parsed });
170
180
  return parsed;
171
181
  }
172
- async function parseFileContent(path, parse) {
173
- return parseFile(path, () => (0, promises_1.readFile)(path).then(parse));
182
+ async function parseFile(path, parse) {
183
+ return loadFileCached(path, () => (0, promises_1.readFile)(path).then(parse));
174
184
  }
package/src/util-http.js CHANGED
@@ -49,6 +49,8 @@ const lodash_1 = __importDefault(require("lodash"));
49
49
  const consumers_1 = require("node:stream/consumers");
50
50
  Object.defineProperty(exports, "stream2string", { enumerable: true, get: function () { return consumers_1.text; } });
51
51
  const tls = __importStar(require("node:tls"));
52
+ const cross_1 = require("./cross");
53
+ const events_1 = require("events");
52
54
  async function httpString(url, options) {
53
55
  return await (0, consumers_1.text)(await httpStream(url, options));
54
56
  }
@@ -60,22 +62,20 @@ async function httpWithBody(url, options) {
60
62
  });
61
63
  }
62
64
  function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...options } = {}) {
63
- var _a;
64
65
  const controller = new AbortController();
65
- (_a = options.signal) !== null && _a !== void 0 ? _a : (options.signal = controller.signal);
66
- return Object.assign(new Promise(async (resolve, reject) => {
67
- var _a, _b, _c, _d;
68
- var _e, _f, _g;
69
- proxy !== null && proxy !== void 0 ? proxy : (proxy = httpStream.defaultProxy);
70
- (_a = options.headers) !== null && _a !== void 0 ? _a : (options.headers = {});
66
+ options.signal ??= controller.signal;
67
+ let ret;
68
+ return Object.assign(ret = new Promise(async (resolve, reject) => {
69
+ proxy ??= httpStream.defaultProxy;
70
+ options.headers ??= {};
71
71
  if (body) {
72
- options.method || (options.method = 'POST');
72
+ options.method ||= 'POST';
73
73
  if (lodash_1.default.isPlainObject(body)) {
74
- (_b = (_e = options.headers)['content-type']) !== null && _b !== void 0 ? _b : (_e['content-type'] = 'application/json');
74
+ options.headers['content-type'] ??= 'application/json';
75
75
  body = JSON.stringify(body);
76
76
  }
77
77
  if (!(body instanceof node_stream_1.Readable))
78
- (_c = (_f = options.headers)['content-length']) !== null && _c !== void 0 ? _c : (_f['content-length'] = Buffer.byteLength(body));
78
+ options.headers['content-length'] ??= Buffer.byteLength(body);
79
79
  }
80
80
  if (jar)
81
81
  options.headers.cookie = lodash_1.default.map(jar, (v, k) => `${k}=${v}; `).join('')
@@ -90,17 +90,16 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
90
90
  }
91
91
  if (proxy) {
92
92
  options.path = url; // full url as path
93
- (_d = (_g = options.headers).host) !== null && _d !== void 0 ? _d : (_g.host = (0, node_url_1.parse)(url).host || undefined); // keep original host header
93
+ options.headers.host ??= (0, node_url_1.parse)(url).host || undefined; // keep original host header
94
+ options.headers.Connection = 'keep-alive'; // try to reuse connections
94
95
  }
95
96
  // this needs the prefix "proxy-"
96
- const proxyAuth = (proxyParsed === null || proxyParsed === void 0 ? void 0 : proxyParsed.auth) ? { 'proxy-authorization': `Basic ${Buffer.from(proxyParsed.auth, 'utf8').toString('base64')}` } : undefined;
97
+ const proxyAuth = proxyParsed?.auth ? { 'proxy-authorization': `Basic ${Buffer.from(proxyParsed.auth, 'utf8').toString('base64')}` } : undefined;
97
98
  // https through proxy is better with CONNECT
98
99
  if (!proxy || parsed.protocol === 'http:' || !await connect())
99
100
  Object.assign(options.headers, proxyAuth);
100
101
  const proto = options.protocol === 'https:' ? node_https_1.default : node_http_1.default;
101
102
  const req = proto.request(options, res => {
102
- var _a;
103
- var _b;
104
103
  console.debug("http responded", res.statusCode, "to", url);
105
104
  if (jar)
106
105
  for (const entry of res.headers['set-cookie'] || []) {
@@ -117,43 +116,95 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
117
116
  let r = res.headers.location;
118
117
  if (r && !noRedirect) {
119
118
  r = new URL(r, url).toString(); // rewrite in case r is just a path, and thus relative to current url
120
- const stack = ((_b = options)._stack || (_b._stack = []));
119
+ const stack = (options._stack ||= []);
121
120
  if (stack.length > 20 || stack.includes(r))
122
- return reject(new Error('endless http redirection'));
121
+ return reject(Error('endless http redirection'));
123
122
  stack.push(r);
124
123
  delete options.method; // redirections are always GET
125
- (_a = options.headers) === null || _a === void 0 ? true : delete _a['content-length'];
124
+ delete options.headers?.['content-length'];
125
+ delete options.headers?.host;
126
126
  delete options.auth;
127
+ delete options.path;
128
+ delete options.agent;
127
129
  return resolve(httpStream(r, options));
128
130
  }
129
131
  resolve(res);
130
132
  }).on('error', (e) => {
131
- if (proxy && (e === null || e === void 0 ? void 0 : e.code) === 'ECONNREFUSED')
132
- console.debug("cannot connect to proxy ", proxy);
133
+ if (proxy && e?.code === 'ECONNREFUSED')
134
+ console.debug("cannot connect to proxy", proxy);
133
135
  reject(req.res || e);
134
136
  });
137
+ if (options.timeout)
138
+ req.setTimeout(options.timeout, () => req.destroy(Error('ETIMEDOUT')));
139
+ options.signal?.addEventListener('abort', () => req.destroy(Error('AbortError')), { once: true });
135
140
  if (body && body instanceof node_stream_1.Readable)
136
141
  body.pipe(req).on('end', () => req.end());
137
142
  else
138
143
  req.end(body);
139
- function connect() {
140
- return proxyParsed && new Promise(resolve => {
144
+ async function connect() {
145
+ if (!proxyParsed)
146
+ return;
147
+ options.agent = false; // disable default pooling, we manage reuse
148
+ const path = `${parsed.hostname}:${parsed.port || 443}`;
149
+ const k = proxy + path;
150
+ const pool = (proxySocketPools[k] ||= []);
151
+ const newProm = (0, cross_1.pendingPromise)();
152
+ async function handleNewProm(socket) {
153
+ options.createConnection = () => socket;
154
+ try {
155
+ const response = await ret;
156
+ await (0, events_1.once)(response, 'close'); // ensure the response is done before reusing the same socket
157
+ }
158
+ catch { }
159
+ newProm.resolve(socket); // when we are done with this request, resolve and pass the socket on
160
+ }
161
+ if (pool.length >= 4)
162
+ return seqRace = seqRace.then(async () => {
163
+ try { // race tracking the index
164
+ const [i, socket] = await Promise.race(pool.map((prom, i) => prom.then(socket => [i, socket])));
165
+ pool[i] = socket.poolRef = newProm; // replacing ensures the pool reflects the current usage of open sockets
166
+ handleNewProm(socket);
167
+ return true;
168
+ }
169
+ catch (e) {
170
+ newProm.catch(() => { }); // without this we get UnhandledPromiseRejection
171
+ newProm.reject(e);
172
+ return false;
173
+ }
174
+ });
175
+ pool.push(newProm);
176
+ return new Promise(resolve => {
177
+ ;
141
178
  (proxyParsed.protocol === 'https:' ? node_https_1.default : node_http_1.default).request({
142
179
  ...proxyParsed,
180
+ auth: undefined, // void proxyParsed.auth
143
181
  method: 'CONNECT',
144
- path: `${parsed.hostname}:${parsed.port || 443}`,
145
- auth: undefined,
146
- headers: proxyAuth
147
- }).on('error', reject).on('connect', (res, socket) => {
182
+ path,
183
+ headers: { Host: path, ...proxyAuth }
184
+ }).on('connect', (res, socket) => {
148
185
  if (res.statusCode !== 200)
149
- return resolve(false);
150
- // a TLS for every request is inefficient. Consider optimizing in the future, especially for reading plugins from github, which makes tens of requests.
151
- options.createConnection = () => tls.connect({ socket, servername: parsed.hostname || undefined });
186
+ return failed(res);
187
+ console.debug("new connection to proxy for", path);
188
+ const tlsSocket = Object.assign(tls.connect({ socket, servername: parsed.hostname || undefined }), { poolRef: newProm });
189
+ tlsSocket.on('close', disconnected);
190
+ tlsSocket.on('error', disconnected);
191
+ handleNewProm(tlsSocket);
152
192
  resolve(true);
153
- }).end();
193
+ function disconnected() {
194
+ lodash_1.default.pull(pool, tlsSocket.poolRef); // the ref may have changed in time
195
+ }
196
+ }).on('response', failed).on('error', failed).end();
197
+ function failed(e) {
198
+ lodash_1.default.pull(pool, newProm);
199
+ newProm.catch(() => { }); // without this we get UnhandledPromiseRejection
200
+ newProm.reject(e);
201
+ resolve(false);
202
+ }
154
203
  });
155
204
  }
156
205
  }), {
157
206
  abort() { controller.abort(); }
158
207
  });
159
208
  }
209
+ const proxySocketPools = {};
210
+ let seqRace = Promise.resolve(false);
package/src/util-os.js CHANGED
@@ -48,13 +48,12 @@ async function getDiskSpaces() {
48
48
  return parseDfResult(await (0, util_1.promisify)(child_process_1.exec)(`df -k`, { timeout: DF_TIMEOUT }).then(x => x.stdout, e => e));
49
49
  }
50
50
  function parseDfResult(result) {
51
- var _a;
52
51
  if (result instanceof Error) {
53
52
  const { status } = result;
54
53
  throw status === 1 ? Error('miss') : status === 127 ? Error('unsupported') : result;
55
54
  }
56
55
  const out = result.split('\n');
57
- if (!((_a = out.shift()) === null || _a === void 0 ? void 0 : _a.startsWith('Filesystem')))
56
+ if (!out.shift()?.startsWith('Filesystem'))
58
57
  throw Error('unsupported');
59
58
  return (0, misc_1.onlyTruthy)(out.map(one => {
60
59
  const bits = one.split(/\s+/);
@@ -80,10 +79,9 @@ async function runCmd(cmd, args = [], options = {}) {
80
79
  }
81
80
  // returns pid-to-name object
82
81
  async function getWindowsServicePids() {
83
- var _a;
84
82
  const res = await runCmd('tasklist /svc /fo csv');
85
83
  const parsed = new csv_1.default().parse(res);
86
- const no = (_a = parsed === null || parsed === void 0 ? void 0 : parsed[1]) === null || _a === void 0 ? void 0 : _a[2];
84
+ const no = parsed?.[1]?.[2];
87
85
  return Object.fromEntries(parsed.slice(2).filter(x => x[2] !== no).map(x => [x[1], x[2]]));
88
86
  }
89
87
  exports.RUNNING_AS_SERVICE = const_1.IS_WINDOWS && getWindowsServicePids().then(x => {