cloudron 6.0.0 → 7.0.1
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/CHANGELOG.md +4 -0
- package/bin/cloudron +15 -29
- package/bin/cloudron-appstore +4 -7
- package/bin/cloudron-build +18 -8
- package/bin/cloudron-versions +33 -0
- package/eslint.config.js +7 -6
- package/package.json +16 -15
- package/src/actions.js +230 -128
- package/src/appstore-actions.js +31 -59
- package/src/backup-tools.js +22 -24
- package/src/build-actions.js +113 -99
- package/src/completion.js +4 -8
- package/src/config.js +78 -53
- package/src/helper.js +155 -13
- package/src/readline.js +8 -10
- package/src/templates/CloudronManifest.appstore.json.ejs +2 -2
- package/src/versions-actions.js +184 -0
- package/test/test.js +131 -160
- package/src/superagent.js +0 -225
package/test/test.js
CHANGED
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { after, before, describe, it } from 'mocha';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import child_process from 'node:child_process';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
import util from 'node:util';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
crypto = require('crypto'),
|
|
12
|
-
expect = require('expect.js'),
|
|
13
|
-
fs = require('fs'),
|
|
14
|
-
path = require('path'),
|
|
15
|
-
safe = require('safetydance'),
|
|
16
|
-
util = require('util');
|
|
12
|
+
import safe from '@cloudron/safetydance';
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
var applocation = 'test';
|
|
22
|
-
var app;
|
|
14
|
+
const cloudron = process.env.CLOUDRON;
|
|
15
|
+
const username = process.env.USERNAME;
|
|
16
|
+
const password = process.env.PASSWORD;
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
const applocation = 'test';
|
|
19
|
+
let app;
|
|
20
|
+
|
|
21
|
+
const CLI = path.resolve(import.meta.dirname + '/../bin/cloudron');
|
|
25
22
|
|
|
26
23
|
function md5(file) {
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const data = fs.readFileSync(file);
|
|
25
|
+
const hash = crypto.createHash('md5').update(data).digest('hex');
|
|
29
26
|
return hash;
|
|
30
27
|
}
|
|
31
28
|
|
|
@@ -36,7 +33,7 @@ function cli(args, options) {
|
|
|
36
33
|
args = args.map(function (e) { return e[0] === '"' ? e.slice(1, -1) : e; }); // remove the quotes
|
|
37
34
|
|
|
38
35
|
try {
|
|
39
|
-
|
|
36
|
+
const cp = child_process.spawnSync(CLI, args, { stdio: [ options.stdin || 'pipe', options.stdout || 'pipe', 'pipe' ], encoding: options.encoding || 'utf8' });
|
|
40
37
|
return cp;
|
|
41
38
|
} catch (e) {
|
|
42
39
|
console.error(e);
|
|
@@ -44,250 +41,224 @@ function cli(args, options) {
|
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
before(
|
|
48
|
-
if (!process.env.CLOUDRON)
|
|
49
|
-
if (!process.env.USERNAME)
|
|
50
|
-
if (!process.env.PASSWORD)
|
|
51
|
-
|
|
52
|
-
done();
|
|
44
|
+
before(() => {
|
|
45
|
+
if (!process.env.CLOUDRON) throw new Error('CLOUDRON env const not set');
|
|
46
|
+
if (!process.env.USERNAME) throw new Error('USERNAME env const not set');
|
|
47
|
+
if (!process.env.PASSWORD) throw new Error('PASSWORD env const not set');
|
|
53
48
|
});
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
describe('Login', function () {
|
|
60
|
-
it('can login', function (done) {
|
|
61
|
-
var out = cli(util.format('login %s --username %s --password %s', cloudron, username, password));
|
|
62
|
-
|
|
63
|
-
if (out.stdout.indexOf('Existing token still valid.') !== -1) return done();
|
|
50
|
+
describe('Login', () => {
|
|
51
|
+
it('can login', () => {
|
|
52
|
+
const out = cli(util.format('login %s --username %s --password %s', cloudron, username, password));
|
|
64
53
|
|
|
65
|
-
|
|
54
|
+
if (out.stdout.indexOf('Existing token still valid.') !== -1) return;
|
|
66
55
|
|
|
67
|
-
|
|
56
|
+
assert.ok(out.stdout.includes('Login successful'));
|
|
68
57
|
});
|
|
69
58
|
});
|
|
70
59
|
|
|
71
60
|
describe('App install', function () {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
it('can install app', function () {
|
|
61
|
+
it('can install app', () => {
|
|
75
62
|
console.log('Installing app, this can take a while');
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
const out = cli('install --appstore-id com.hastebin.cloudronapp --location ' + applocation);
|
|
64
|
+
assert.ok(out.stdout.includes('App is installed'));
|
|
78
65
|
});
|
|
79
66
|
});
|
|
80
67
|
|
|
81
|
-
describe('Inspect',
|
|
82
|
-
it('can inspect app',
|
|
83
|
-
|
|
68
|
+
describe('Inspect', () => {
|
|
69
|
+
it('can inspect app', () => {
|
|
70
|
+
const inspect = JSON.parse(cli('inspect').stdout);
|
|
84
71
|
app = inspect.apps.filter(function (a) { return a.location === applocation; })[0];
|
|
85
|
-
|
|
72
|
+
assert.notStrictEqual(app, undefined);
|
|
86
73
|
});
|
|
87
74
|
});
|
|
88
75
|
|
|
89
76
|
describe('Exec', function () {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
var out = cli(util.format('exec --app %s -- ls -l /app/code', app.id));
|
|
94
|
-
expect(out.stdout).to.contain('total');
|
|
77
|
+
it('can execute a command and see stdout', () => {
|
|
78
|
+
const out = cli(util.format('exec --app %s -- ls -l /app/code', app.id));
|
|
79
|
+
assert.ok(out.stdout.includes('total'));
|
|
95
80
|
});
|
|
96
81
|
|
|
97
|
-
it('can execute a command and see stderr',
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
it('can execute a command and see stderr', () => {
|
|
83
|
+
const out = cli(util.format('exec --app %s -- ls /blah', app.id));
|
|
84
|
+
assert.ok(out.stderr.includes('ls: cannot access'));
|
|
100
85
|
});
|
|
101
86
|
|
|
102
|
-
it('can get binary file in stdout',
|
|
103
|
-
|
|
104
|
-
outstream.on('open',
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
it('can get binary file in stdout', async () => {
|
|
88
|
+
const outstream = fs.createWriteStream('/tmp/clitest.ls');
|
|
89
|
+
await new Promise((resolve) => outstream.on('open', resolve));
|
|
90
|
+
|
|
91
|
+
cli(util.format('exec --app %s -- cat /bin/ls', app.id), { stdout: outstream, encoding: 'binary' });
|
|
92
|
+
const result = cli(util.format('exec --app %s md5sum /bin/ls', app.id));
|
|
107
93
|
|
|
108
|
-
|
|
94
|
+
assert.ok(result.output[1].includes(md5('/tmp/clitest.ls')));
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
done();
|
|
112
|
-
});
|
|
96
|
+
fs.unlinkSync('/tmp/clitest.ls');
|
|
113
97
|
});
|
|
114
98
|
|
|
115
|
-
it('can pipe stdin to exec command',
|
|
116
|
-
|
|
99
|
+
it('can pipe stdin to exec command', async () => {
|
|
100
|
+
const randomBytes = crypto.randomBytes(256);
|
|
117
101
|
fs.writeFileSync('/tmp/randombytes', randomBytes);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
instream.on('open',
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
102
|
+
const randomBytesMd5 = crypto.createHash('md5').update(randomBytes).digest('hex');
|
|
103
|
+
|
|
104
|
+
const instream = fs.createReadStream('/tmp/randombytes');
|
|
105
|
+
await new Promise((resolve) => instream.on('open', resolve));
|
|
106
|
+
|
|
107
|
+
cli(util.format('exec --app %s -- bash -c "cat - > /app/data/sauce"', app.id), { stdin: instream });
|
|
108
|
+
const out = cli(util.format('exec --app %s md5sum /app/data/sauce', app.id));
|
|
109
|
+
assert.ok(out.stdout.includes(randomBytesMd5));
|
|
127
110
|
});
|
|
128
111
|
});
|
|
129
112
|
|
|
130
113
|
describe('Push', function () {
|
|
131
|
-
|
|
114
|
+
const RANDOM_FILE = '/tmp/randombytes';
|
|
132
115
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
it('can push a binary file', function () {
|
|
136
|
-
var randomBytes = crypto.randomBytes(500);
|
|
116
|
+
it('can push a binary file', () => {
|
|
117
|
+
const randomBytes = crypto.randomBytes(500);
|
|
137
118
|
fs.writeFileSync(RANDOM_FILE, randomBytes);
|
|
138
119
|
|
|
139
120
|
cli(util.format('push --app %s %s /tmp/push1', app.id, RANDOM_FILE));
|
|
140
|
-
|
|
141
|
-
|
|
121
|
+
const out = cli(util.format('exec --app %s md5sum /tmp/push1', app.id));
|
|
122
|
+
assert.ok(out.stdout.includes(md5(RANDOM_FILE)));
|
|
142
123
|
fs.unlinkSync(RANDOM_FILE);
|
|
143
124
|
});
|
|
144
125
|
|
|
145
|
-
it('can push to directory',
|
|
146
|
-
|
|
126
|
+
it('can push to directory', () => {
|
|
127
|
+
const testFile = import.meta.dirname + '/test.js';
|
|
147
128
|
cli(util.format('push --app %s %s /tmp/', app.id, testFile));
|
|
148
|
-
|
|
149
|
-
|
|
129
|
+
const out = cli(util.format('exec --app %s md5sum /tmp/test.js', app.id));
|
|
130
|
+
assert.ok(out.stdout.includes(md5(testFile)));
|
|
150
131
|
});
|
|
151
132
|
|
|
152
|
-
it('can push stdin',
|
|
153
|
-
|
|
133
|
+
it('can push stdin', async () => {
|
|
134
|
+
const randomBytes = crypto.randomBytes(500);
|
|
154
135
|
fs.writeFileSync(RANDOM_FILE, randomBytes);
|
|
155
136
|
|
|
156
|
-
|
|
157
|
-
istream.on('open',
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
137
|
+
const istream = fs.createReadStream(RANDOM_FILE);
|
|
138
|
+
await new Promise((resolve) => istream.on('open', resolve));
|
|
139
|
+
|
|
140
|
+
cli(util.format('push --app %s - /run/testcopy.js', app.id), { stdin: istream });
|
|
141
|
+
const out = cli(util.format('exec --app %s md5sum /run/testcopy.js', app.id));
|
|
142
|
+
assert.ok(out.stdout.includes(md5(RANDOM_FILE)));
|
|
143
|
+
fs.unlinkSync(RANDOM_FILE);
|
|
164
144
|
});
|
|
165
145
|
|
|
166
|
-
it('can push a directory',
|
|
167
|
-
|
|
146
|
+
it('can push a directory', () => {
|
|
147
|
+
const testDir = import.meta.dirname, testFile = import.meta.dirname + '/test.js';
|
|
168
148
|
cli(util.format('push --app %s %s /run', app.id, testDir));
|
|
169
|
-
|
|
170
|
-
|
|
149
|
+
const out = cli(util.format('exec --app %s md5sum /run/' + path.basename(import.meta.dirname) + '/test.js', app.id));
|
|
150
|
+
assert.ok(out.stdout.includes(md5(testFile)));
|
|
171
151
|
});
|
|
172
152
|
|
|
173
|
-
it('can push a large file',
|
|
153
|
+
it('can push a large file', () => {
|
|
174
154
|
child_process.execSync('dd if=/dev/urandom of=' + RANDOM_FILE + ' bs=10M count=1');
|
|
175
155
|
cli(util.format('push --app %s %s /tmp/push1', app.id, RANDOM_FILE));
|
|
176
|
-
|
|
177
|
-
|
|
156
|
+
const out = cli(util.format('exec --app %s md5sum /tmp/push1', app.id));
|
|
157
|
+
assert.ok(out.stdout.includes(md5(RANDOM_FILE)));
|
|
178
158
|
fs.unlinkSync(RANDOM_FILE);
|
|
179
159
|
});
|
|
180
160
|
});
|
|
181
161
|
|
|
182
162
|
describe('Pull', function () {
|
|
183
|
-
|
|
163
|
+
const RANDOM_FILE = '/tmp/randombytes';
|
|
184
164
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
before(function () {
|
|
188
|
-
var randomBytes = crypto.randomBytes(20000);
|
|
165
|
+
before(() => {
|
|
166
|
+
const randomBytes = crypto.randomBytes(20000);
|
|
189
167
|
fs.writeFileSync(RANDOM_FILE, randomBytes);
|
|
190
168
|
|
|
191
169
|
cli(util.format('push --app %s /tmp/randombytes /tmp/randombytes', app.id));
|
|
192
170
|
});
|
|
193
171
|
|
|
194
|
-
after(
|
|
172
|
+
after(() => {
|
|
195
173
|
fs.unlinkSync(RANDOM_FILE);
|
|
196
174
|
});
|
|
197
175
|
|
|
198
|
-
it('can pull a binary file',
|
|
176
|
+
it('can pull a binary file', () => {
|
|
199
177
|
cli(util.format('pull --app %s /tmp/randombytes /tmp/pullfiletest', app.id));
|
|
200
|
-
|
|
178
|
+
assert.strictEqual(md5('/tmp/pullfiletest'), md5('/tmp/randombytes'));
|
|
201
179
|
fs.unlinkSync('/tmp/pullfiletest');
|
|
202
180
|
});
|
|
203
181
|
|
|
204
|
-
it('can pull a directory',
|
|
182
|
+
it('can pull a directory', () => {
|
|
205
183
|
fs.rmSync('/tmp/pulldir', { recursive: true, force: true });
|
|
206
184
|
safe.fs.mkdirSync('/tmp/pulldir');
|
|
207
185
|
cli(util.format('pull --app %s /app/code/ /tmp/pulldir', app.id));
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
186
|
+
assert.ok(fs.existsSync('/tmp/pulldir/README.md'));
|
|
187
|
+
assert.ok(fs.existsSync('/tmp/pulldir/static/robots.txt'));
|
|
188
|
+
assert.ok(fs.existsSync('/tmp/pulldir/.gitignore'));
|
|
211
189
|
|
|
212
|
-
|
|
213
|
-
|
|
190
|
+
const result = cli(util.format('exec --app %s md5sum /app/code/node_modules/uglify-js/bin/uglifyjs', app.id));
|
|
191
|
+
assert.ok(result.output[1].includes(md5('/tmp/pulldir/node_modules/uglify-js/bin/uglifyjs')));
|
|
214
192
|
|
|
215
193
|
fs.rmSync('/tmp/pulldir', { recursive: true, force: true });
|
|
216
194
|
});
|
|
217
195
|
|
|
218
|
-
it('can pull to directory',
|
|
196
|
+
it('can pull to directory', () => {
|
|
219
197
|
safe.fs.unlinkSync('/tmp/pulledreadme.md');
|
|
220
198
|
cli(util.format('pull --app %s /app/code/README.md /tmp/pulledreadme.md', app.id));
|
|
221
199
|
|
|
222
|
-
|
|
223
|
-
|
|
200
|
+
const result = cli(util.format('exec --app %s md5sum /app/code/README.md', app.id));
|
|
201
|
+
assert.ok(result.output[1].includes(md5('/tmp/pulledreadme.md')));
|
|
224
202
|
|
|
225
203
|
fs.unlinkSync('/tmp/pulledreadme.md');
|
|
226
204
|
});
|
|
227
205
|
|
|
228
|
-
it('can pull to stdout',
|
|
206
|
+
it('can pull to stdout', async () => {
|
|
229
207
|
safe.fs.unlinkSync('/tmp/pullfiletest');
|
|
230
|
-
|
|
231
|
-
ostream.on('open',
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
208
|
+
const ostream = fs.createWriteStream('/tmp/pullfiletest');
|
|
209
|
+
await new Promise((resolve) => ostream.on('open', resolve));
|
|
210
|
+
|
|
211
|
+
cli(util.format('pull --app %s /tmp/randombytes -', app.id), { stdout: ostream });
|
|
212
|
+
assert.strictEqual(md5('/tmp/pullfiletest'), md5('/tmp/randombytes'));
|
|
213
|
+
fs.unlinkSync('/tmp/pullfiletest');
|
|
237
214
|
});
|
|
238
215
|
});
|
|
239
216
|
|
|
240
|
-
describe('Status',
|
|
241
|
-
it('can get status',
|
|
242
|
-
|
|
243
|
-
|
|
217
|
+
describe('Status', () => {
|
|
218
|
+
it('can get status', () => {
|
|
219
|
+
const out = cli('status --app ' + app.id);
|
|
220
|
+
assert.ok(out.stdout.includes('running'));
|
|
244
221
|
});
|
|
245
222
|
});
|
|
246
223
|
|
|
247
224
|
describe('Backup', function () {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
it('create succeeds', function () {
|
|
253
|
-
var out = cli('backup create --app ' + app.id);
|
|
254
|
-
expect(out.stdout).to.contain('App is backed up');
|
|
225
|
+
it('create succeeds', () => {
|
|
226
|
+
const out = cli('backup create --app ' + app.id);
|
|
227
|
+
assert.ok(out.stdout.includes('App is backed up'));
|
|
255
228
|
});
|
|
256
229
|
|
|
257
|
-
it('list succeeds',
|
|
258
|
-
|
|
259
|
-
|
|
230
|
+
it('list succeeds', () => {
|
|
231
|
+
const out = cli('backup list --app ' + app.id);
|
|
232
|
+
assert.ok(out.stdout.includes(app.id));
|
|
260
233
|
|
|
261
|
-
backupId = out.stdout.split('\n').reverse().filter(function (b) { return b; })[0].split(' ')[0];
|
|
234
|
+
/* backupId = */ out.stdout.split('\n').reverse().filter(function (b) { return b; })[0].split(' ')[0];
|
|
262
235
|
});
|
|
263
236
|
});
|
|
264
237
|
|
|
265
|
-
describe('list apps',
|
|
266
|
-
it('succeeds',
|
|
267
|
-
|
|
268
|
-
|
|
238
|
+
describe('list apps', () => {
|
|
239
|
+
it('succeeds', () => {
|
|
240
|
+
const out = cli('list');
|
|
241
|
+
assert.ok(out.stdout.includes(app.id));
|
|
269
242
|
});
|
|
270
243
|
});
|
|
271
244
|
|
|
272
|
-
describe('app logs',
|
|
273
|
-
it('succeeds',
|
|
274
|
-
|
|
275
|
-
|
|
245
|
+
describe('app logs', () => {
|
|
246
|
+
it('succeeds', () => {
|
|
247
|
+
const out = cli('logs --app ' + app.id);
|
|
248
|
+
assert.ok(out.stdout.includes('listening on'));
|
|
276
249
|
});
|
|
277
250
|
});
|
|
278
251
|
|
|
279
252
|
describe('Uninstall', function () {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
var out = cli('uninstall --app ' + app.id);
|
|
284
|
-
expect(out.stdout).to.contain('successfully uninstalled');
|
|
253
|
+
it('can uninstall', () => {
|
|
254
|
+
const out = cli('uninstall --app ' + app.id);
|
|
255
|
+
assert.ok(out.stdout.includes('successfully uninstalled'));
|
|
285
256
|
});
|
|
286
257
|
});
|
|
287
258
|
|
|
288
|
-
describe('Logout',
|
|
289
|
-
it('can logout',
|
|
290
|
-
|
|
291
|
-
|
|
259
|
+
describe('Logout', () => {
|
|
260
|
+
it('can logout', () => {
|
|
261
|
+
const out = cli('logout');
|
|
262
|
+
assert.ok(out.stdout.includes('Logged out'));
|
|
292
263
|
});
|
|
293
264
|
});
|
package/src/superagent.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
exports = module.exports = {
|
|
4
|
-
head,
|
|
5
|
-
get,
|
|
6
|
-
put,
|
|
7
|
-
post,
|
|
8
|
-
patch,
|
|
9
|
-
del,
|
|
10
|
-
options,
|
|
11
|
-
request
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// IMPORTANT: do not require box code here . This is used by migration scripts
|
|
15
|
-
const assert = require('assert'),
|
|
16
|
-
consumers = require('node:stream/consumers'),
|
|
17
|
-
debug = require('debug')('box:superagent'),
|
|
18
|
-
fs = require('fs'),
|
|
19
|
-
http = require('http'),
|
|
20
|
-
https = require('https'),
|
|
21
|
-
path = require('path'),
|
|
22
|
-
safe = require('safetydance');
|
|
23
|
-
|
|
24
|
-
class Request {
|
|
25
|
-
#boundary;
|
|
26
|
-
#redirectCount;
|
|
27
|
-
#retryCount;
|
|
28
|
-
#timer;
|
|
29
|
-
#body;
|
|
30
|
-
#okFunc;
|
|
31
|
-
#options;
|
|
32
|
-
#url;
|
|
33
|
-
|
|
34
|
-
constructor(method, url) {
|
|
35
|
-
assert.strictEqual(typeof url, 'string');
|
|
36
|
-
|
|
37
|
-
this.#url = new URL(url);
|
|
38
|
-
this.#options = {
|
|
39
|
-
method,
|
|
40
|
-
headers: {},
|
|
41
|
-
signal: null // set for timeouts
|
|
42
|
-
};
|
|
43
|
-
this.#okFunc = ({ status }) => status >=200 && status <= 299;
|
|
44
|
-
this.#timer = { timeout: 0, id: null, controller: null };
|
|
45
|
-
this.#retryCount = 0;
|
|
46
|
-
this.#body = Buffer.alloc(0);
|
|
47
|
-
this.#redirectCount = 5;
|
|
48
|
-
this.#boundary = null; // multipart only
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async _handleResponse(url, response) {
|
|
52
|
-
// const contentLength = response.headers['content-length'];
|
|
53
|
-
// if (!contentLength || contentLength > 5*1024*1024*1024) throw new Error(`Response size unknown or too large: ${contentLength}`);
|
|
54
|
-
const [consumeError, data] = await safe(consumers.buffer(response)); // have to drain response
|
|
55
|
-
if (consumeError) throw new Error(`Error consuming body stream: ${consumeError.message}`);
|
|
56
|
-
if (!response.complete) throw new Error('Incomplete response');
|
|
57
|
-
const contentType = response.headers['content-type'];
|
|
58
|
-
|
|
59
|
-
const result = {
|
|
60
|
-
url: new URL(response.headers['location'] || '', url),
|
|
61
|
-
status: response.statusCode,
|
|
62
|
-
headers: response.headers,
|
|
63
|
-
body: null,
|
|
64
|
-
text: null
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
if (contentType?.includes('application/json')) {
|
|
68
|
-
result.text = data.toString('utf8');
|
|
69
|
-
if (data.byteLength !== 0) result.body = safe.JSON.parse(result.text) || {};
|
|
70
|
-
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
71
|
-
result.text = data.toString('utf8');
|
|
72
|
-
const searchParams = new URLSearchParams(data);
|
|
73
|
-
result.body = Object.fromEntries(searchParams.entries());
|
|
74
|
-
} else if (!contentType || contentType.startsWith('text/')) {
|
|
75
|
-
result.body = data;
|
|
76
|
-
result.text = result.body.toString('utf8');
|
|
77
|
-
} else {
|
|
78
|
-
result.body = data;
|
|
79
|
-
result.text = `<binary data (${data.byteLength} bytes)>`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async _makeRequest(url) {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
const proto = url.protocol === 'https:' ? https : http;
|
|
88
|
-
const request = proto.request(url, this.#options); // ClientRequest
|
|
89
|
-
|
|
90
|
-
request.on('error', reject); // network error, dns error
|
|
91
|
-
request.on('response', async (response) => {
|
|
92
|
-
const [error, result] = await safe(this._handleResponse(url, response));
|
|
93
|
-
if (error) reject(error); else resolve(result);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
request.write(this.#body);
|
|
97
|
-
|
|
98
|
-
request.end();
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async _start() {
|
|
103
|
-
let error;
|
|
104
|
-
|
|
105
|
-
for (let i = 0; i < this.#retryCount+1; i++) {
|
|
106
|
-
if (this.#timer.timeout) this.#timer.id = setTimeout(() => this.#timer.controller.abort(), this.#timer.timeout);
|
|
107
|
-
debug(`${this.#options.method} ${this.#url.toString()}` + (i ? ` try ${i+1}` : ''));
|
|
108
|
-
|
|
109
|
-
let response, url = this.#url;
|
|
110
|
-
for (let redirects = 0; redirects < this.#redirectCount+1; redirects++) {
|
|
111
|
-
[error, response] = await safe(this._makeRequest(url));
|
|
112
|
-
if (error || (response.status < 300 || response.status > 399) || (this.#options.method !== 'GET')) break;
|
|
113
|
-
url = response.url; // follow
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!error && !this.#okFunc({ status: response.status })) {
|
|
117
|
-
error = new Error(`${response.status} ${http.STATUS_CODES[response.status]}`);
|
|
118
|
-
Object.assign(error, response);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (error) debug(`${this.#options.method} ${this.#url.toString()} ${error.message}`);
|
|
122
|
-
if (this.#timer.timeout) clearTimeout(this.#timer.id);
|
|
123
|
-
if (!error) return response;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw error;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
set(name, value) {
|
|
130
|
-
this.#options.headers[name.toLowerCase()] = value;
|
|
131
|
-
return this;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
query(data) {
|
|
135
|
-
Object.entries(data).forEach(([key, value]) => this.#url.searchParams.append(key, value));
|
|
136
|
-
return this;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
redirects(count) {
|
|
140
|
-
this.#redirectCount = count;
|
|
141
|
-
return this;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
send(data) {
|
|
145
|
-
const contentType = this.#options.headers['content-type'];
|
|
146
|
-
if (!contentType || contentType === 'application/json') {
|
|
147
|
-
this.#options.headers['content-type'] = 'application/json';
|
|
148
|
-
this.#body = Buffer.from(JSON.stringify(data), 'utf8');
|
|
149
|
-
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
150
|
-
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
151
|
-
this.#body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
|
|
152
|
-
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
153
|
-
}
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
timeout(msecs) {
|
|
158
|
-
this.#timer.controller = new AbortController();
|
|
159
|
-
this.#timer.timeout = msecs;
|
|
160
|
-
this.#options.signal = this.#timer.controller.signal;
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
retry(count) {
|
|
165
|
-
this.#retryCount = Math.max(0, count);
|
|
166
|
-
return this;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
ok(func) {
|
|
170
|
-
this.#okFunc = func;
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
disableTLSCerts() {
|
|
175
|
-
this.#options.rejectUnauthorized = true;
|
|
176
|
-
return this;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
auth(username, password) {
|
|
180
|
-
this.set('Authorization', 'Basic ' + btoa(`${username}:${password}`));
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
field(name, value) {
|
|
185
|
-
if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
186
|
-
|
|
187
|
-
const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n`, 'utf8');
|
|
188
|
-
const partData = Buffer.from(value, 'utf8');
|
|
189
|
-
this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
|
|
190
|
-
|
|
191
|
-
return this;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
attach(name, filepathOrBuffer) { // this is only used in tests and thus simplistic
|
|
195
|
-
if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
196
|
-
|
|
197
|
-
const filename = Buffer.isBuffer(filepathOrBuffer) ? name : path.basename(filepathOrBuffer);
|
|
198
|
-
const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${filename}"\r\n\r\n`, 'utf8');
|
|
199
|
-
const partData = Buffer.isBuffer(filepathOrBuffer) ? filepathOrBuffer : fs.readFileSync(filepathOrBuffer);
|
|
200
|
-
this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
|
|
201
|
-
|
|
202
|
-
return this;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
then(onFulfilled, onRejected) {
|
|
206
|
-
if (this.#boundary) {
|
|
207
|
-
const partTrailer = Buffer.from(`--${this.#boundary}--\r\n`, 'utf8');
|
|
208
|
-
this.#body = Buffer.concat([this.#body, partTrailer]);
|
|
209
|
-
|
|
210
|
-
this.#options.headers['content-type'] = `multipart/form-data; boundary=${this.#boundary}`;
|
|
211
|
-
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
this._start().then(onFulfilled, onRejected);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function head(url) { return new Request('HEAD', url); }
|
|
219
|
-
function get(url) { return new Request('GET', url); }
|
|
220
|
-
function put(url) { return new Request('PUT', url); }
|
|
221
|
-
function post(url) { return new Request('POST', url); }
|
|
222
|
-
function patch(url) { return new Request('PATCH', url); }
|
|
223
|
-
function del(url) { return new Request('DELETE', url); }
|
|
224
|
-
function options(url) { return new Request('OPTIONS', url); }
|
|
225
|
-
function request(method, url) { return new Request(method, url); }
|