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/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 =
|
|
40
|
-
const ipGroup = ip2group[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
|
175
|
+
throw e?.message || String(e);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
function launch(cmd, pars = [], options) {
|
|
179
|
-
return (
|
|
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
|
|
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',
|
|
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
|
|
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)(
|
|
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.
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
10
|
+
exports.readFileWithBusyRetry = readFileWithBusyRetry;
|
|
11
11
|
exports.watchDir = watchDir;
|
|
12
|
-
exports.
|
|
13
|
-
exports.
|
|
12
|
+
exports.hasDirTraversal = hasDirTraversal;
|
|
13
|
+
exports.escapeGlobPath = escapeGlobPath;
|
|
14
14
|
exports.unzip = unzip;
|
|
15
|
-
exports.
|
|
15
|
+
exports.ensureParentFolder = ensureParentFolder;
|
|
16
16
|
exports.createFileWithPath = createFileWithPath;
|
|
17
|
-
exports.
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
50
|
+
catch { }
|
|
41
51
|
}
|
|
42
|
-
async function
|
|
52
|
+
async function readFileWithBusyRetry(path) {
|
|
43
53
|
return (0, promises_1.readFile)(path, 'utf8').catch(e => {
|
|
44
|
-
if (
|
|
54
|
+
if (e?.code !== 'EBUSY')
|
|
45
55
|
throw e;
|
|
46
56
|
console.debug('busy');
|
|
47
|
-
return (0, cross_1.wait)(100).then(() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
106
|
+
let chain = Promise.resolve();
|
|
97
107
|
return new Promise((resolve, reject) => stream.pipe(unzip_stream_1.default.Parse())
|
|
98
|
-
.on('end', () =>
|
|
108
|
+
.on('end', () => chain.then(resolve))
|
|
99
109
|
.on('error', reject)
|
|
100
|
-
.on('entry', (entry) =>
|
|
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
|
|
116
|
+
const thisFile = entry.pipe(await createSafeWriteStream(dest));
|
|
107
117
|
await (0, events_1.once)(thisFile, 'finish');
|
|
108
118
|
})));
|
|
109
119
|
}
|
|
110
|
-
async function
|
|
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
|
|
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
|
|
139
|
+
catch {
|
|
130
140
|
return;
|
|
131
141
|
}
|
|
132
142
|
return (0, fs_1.createWriteStream)(path, options);
|
|
133
143
|
}
|
|
134
|
-
async function
|
|
135
|
-
await
|
|
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
|
|
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) && !
|
|
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
|
|
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 =
|
|
178
|
+
const parsed = loader(path);
|
|
169
179
|
exports.parseFileCache.set(path, { ts, parsed });
|
|
170
180
|
return parsed;
|
|
171
181
|
}
|
|
172
|
-
async function
|
|
173
|
-
return
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
72
|
+
options.method ||= 'POST';
|
|
73
73
|
if (lodash_1.default.isPlainObject(body)) {
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = (
|
|
119
|
+
const stack = (options._stack ||= []);
|
|
121
120
|
if (stack.length > 20 || stack.includes(r))
|
|
122
|
-
return reject(
|
|
121
|
+
return reject(Error('endless http redirection'));
|
|
123
122
|
stack.push(r);
|
|
124
123
|
delete options.method; // redirections are always GET
|
|
125
|
-
|
|
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 &&
|
|
132
|
-
console.debug("cannot connect to 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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 => {
|