cloudron 5.12.0 → 5.13.0
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/cloudron-appstore +4 -0
- package/eslint.config.js +21 -0
- package/package.json +9 -13
- package/src/actions.js +33 -47
- package/src/appstore-actions.js +36 -6
- package/src/backup-tools.js +9 -7
- package/src/build-actions.js +13 -13
- package/src/line-stream.js +30 -0
- package/src/readline.js +21 -0
- package/src/superagent.js +198 -0
- package/test/test.js +1 -1
package/bin/cloudron-appstore
CHANGED
|
@@ -54,6 +54,10 @@ program.command('upload')
|
|
|
54
54
|
.option('-f, --force', 'Update existing version')
|
|
55
55
|
.action(appstoreActions.upload);
|
|
56
56
|
|
|
57
|
+
program.command('verify-manifest')
|
|
58
|
+
.description('Verify if manifest is ready to be upload/published')
|
|
59
|
+
.action(appstoreActions.verifyManifest);
|
|
60
|
+
|
|
57
61
|
program.command('versions')
|
|
58
62
|
.alias('list')
|
|
59
63
|
.description('List published versions')
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const js = require('@eslint/js');
|
|
2
|
+
const globals = require('globals');
|
|
3
|
+
|
|
4
|
+
module.exports = [
|
|
5
|
+
js.configs.recommended,
|
|
6
|
+
{
|
|
7
|
+
files: ["**/*.js"],
|
|
8
|
+
languageOptions: {
|
|
9
|
+
globals: {
|
|
10
|
+
...globals.node,
|
|
11
|
+
},
|
|
12
|
+
ecmaVersion: 13,
|
|
13
|
+
sourceType: "commonjs"
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
semi: "error",
|
|
17
|
+
"prefer-const": "error"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudron",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Cloudron Commandline Tool",
|
|
6
6
|
"main": "main.js",
|
|
@@ -17,29 +17,25 @@
|
|
|
17
17
|
},
|
|
18
18
|
"author": "Cloudron Developers <support@cloudron.io>",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"async": "^3.2.6",
|
|
21
20
|
"cloudron-manifestformat": "^5.26.2",
|
|
22
|
-
"commander": "^
|
|
23
|
-
"debug": "^4.
|
|
21
|
+
"commander": "^13.1.0",
|
|
22
|
+
"debug": "^4.4.0",
|
|
24
23
|
"easy-table": "^1.2.0",
|
|
25
24
|
"ejs": "^3.1.10",
|
|
26
|
-
"eventsource": "^
|
|
25
|
+
"eventsource": "^3.0.5",
|
|
27
26
|
"micromatch": "^4.0.8",
|
|
28
27
|
"open": "^10.1.0",
|
|
29
|
-
"
|
|
30
|
-
"progress-stream": "^2.0.0",
|
|
31
|
-
"readline-sync": "^1.4.10",
|
|
32
|
-
"safetydance": "^2.4.0",
|
|
33
|
-
"split2": "^4.2.0",
|
|
28
|
+
"safetydance": "^2.5.0",
|
|
34
29
|
"superagent": "^10.1.1",
|
|
35
|
-
"tar-fs": "
|
|
36
|
-
"underscore": "^1.13.7"
|
|
30
|
+
"tar-fs": "^3.0.8"
|
|
37
31
|
},
|
|
38
32
|
"engines": {
|
|
39
33
|
"node": ">= 18.x.x"
|
|
40
34
|
},
|
|
41
35
|
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^9.20.0",
|
|
37
|
+
"eslint": "^9.20.1",
|
|
42
38
|
"expect.js": "^0.3.1",
|
|
43
|
-
"mocha": "^
|
|
39
|
+
"mocha": "^11.1.0"
|
|
44
40
|
}
|
|
45
41
|
}
|
package/src/actions.js
CHANGED
|
@@ -4,25 +4,22 @@ const assert = require('assert'),
|
|
|
4
4
|
config = require('./config.js'),
|
|
5
5
|
ejs = require('ejs'),
|
|
6
6
|
{ exit, locateManifest } = require('./helper.js'),
|
|
7
|
-
EventSource = require('eventsource'),
|
|
7
|
+
{ EventSource } = require('eventsource'),
|
|
8
8
|
fs = require('fs'),
|
|
9
9
|
https = require('https'),
|
|
10
|
+
LineStream = require('./line-stream.js'),
|
|
10
11
|
manifestFormat = require('cloudron-manifestformat'),
|
|
11
12
|
os = require('os'),
|
|
12
13
|
path = require('path'),
|
|
13
|
-
|
|
14
|
-
ProgressStream = require('progress-stream'),
|
|
15
|
-
readlineSync = require('readline-sync'),
|
|
14
|
+
readline = require('./readline.js'),
|
|
16
15
|
safe = require('safetydance'),
|
|
17
16
|
spawn = require('child_process').spawn,
|
|
18
17
|
semver = require('semver'),
|
|
19
|
-
split = require('split2'),
|
|
20
18
|
superagent = require('superagent'),
|
|
21
19
|
Table = require('easy-table'),
|
|
22
20
|
tar = require('tar-fs'),
|
|
23
21
|
timers = require('timers/promises'),
|
|
24
|
-
zlib = require('zlib')
|
|
25
|
-
_ = require('underscore');
|
|
22
|
+
zlib = require('zlib');
|
|
26
23
|
|
|
27
24
|
exports = module.exports = {
|
|
28
25
|
list,
|
|
@@ -163,7 +160,7 @@ async function selectAppWithRepository(repository, options) {
|
|
|
163
160
|
|
|
164
161
|
let index = -1;
|
|
165
162
|
while (true) {
|
|
166
|
-
index = parseInt(
|
|
163
|
+
index = parseInt(await readline.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
|
|
167
164
|
if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
|
|
168
165
|
else break;
|
|
169
166
|
}
|
|
@@ -334,7 +331,7 @@ async function authenticate(adminFqdn, username, password, options) {
|
|
|
334
331
|
let totpToken;
|
|
335
332
|
|
|
336
333
|
const { rejectUnauthorized, askForTotpToken } = options;
|
|
337
|
-
if (askForTotpToken) totpToken =
|
|
334
|
+
if (askForTotpToken) totpToken = await readline.question('2FA Token: ', {});
|
|
338
335
|
|
|
339
336
|
const request = superagent.post(`https://${adminFqdn}/api/v1/auth/login`)
|
|
340
337
|
.timeout(60000)
|
|
@@ -360,7 +357,7 @@ async function authenticate(adminFqdn, username, password, options) {
|
|
|
360
357
|
}
|
|
361
358
|
|
|
362
359
|
async function login(adminFqdn, localOptions, cmd) {
|
|
363
|
-
if (!adminFqdn) adminFqdn =
|
|
360
|
+
if (!adminFqdn) adminFqdn = await readline.question('Cloudron Domain (e.g. my.example.com): ', {});
|
|
364
361
|
if (!adminFqdn) return exit('');
|
|
365
362
|
|
|
366
363
|
if (adminFqdn.indexOf('https://') === 0) adminFqdn = adminFqdn.slice('https://'.length);
|
|
@@ -389,8 +386,8 @@ async function login(adminFqdn, localOptions, cmd) {
|
|
|
389
386
|
}
|
|
390
387
|
|
|
391
388
|
if (!token) {
|
|
392
|
-
const username = options.username ||
|
|
393
|
-
const password = options.password ||
|
|
389
|
+
const username = options.username || await readline.question('Username: ', {});
|
|
390
|
+
const password = options.password || await readline.question('Password: ', { noEchoBack: true });
|
|
394
391
|
|
|
395
392
|
const [error, result] = await safe(authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: false }));
|
|
396
393
|
if (error) return exit(`Failed to login: ${error.message}`);
|
|
@@ -458,7 +455,7 @@ async function list(localOptions, cmd) {
|
|
|
458
455
|
t.cell('Id', detailedApp.id);
|
|
459
456
|
t.cell('Location', detailedApp.fqdn);
|
|
460
457
|
t.cell('Manifest Id', (detailedApp.manifest.id || 'customapp') + '@' + detailedApp.manifest.version);
|
|
461
|
-
|
|
458
|
+
let prettyState;
|
|
462
459
|
if (detailedApp.installationState === 'installed') {
|
|
463
460
|
prettyState = (detailedApp.debugMode ? 'debug' : detailedApp.runState);
|
|
464
461
|
} else if (detailedApp.installationState === 'error') {
|
|
@@ -481,18 +478,19 @@ async function querySecondaryDomains(app, manifest, options) {
|
|
|
481
478
|
|
|
482
479
|
for (const env in manifest.httpPorts) {
|
|
483
480
|
const defaultDomain = (app && app.secondaryDomains && app.secondaryDomains[env]) ? app.secondaryDomains[env] : (manifest.httpPorts[env].defaultValue || '');
|
|
484
|
-
const input =
|
|
481
|
+
const input = await readline.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
|
|
485
482
|
secondaryDomains[env] = await selectDomain(input, options);
|
|
486
483
|
}
|
|
487
484
|
return secondaryDomains;
|
|
488
485
|
}
|
|
489
486
|
|
|
490
|
-
function queryPortBindings(app, manifest) {
|
|
487
|
+
async function queryPortBindings(app, manifest) {
|
|
491
488
|
const portBindings = {};
|
|
492
|
-
const allPorts =
|
|
489
|
+
const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
|
|
490
|
+
|
|
493
491
|
for (const env in allPorts) {
|
|
494
492
|
const defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
|
|
495
|
-
const port =
|
|
493
|
+
const port = await readline.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
|
|
496
494
|
if (port === '') {
|
|
497
495
|
portBindings[env] = defaultPort;
|
|
498
496
|
} else if (isNaN(parseInt(port, 10))) {
|
|
@@ -559,7 +557,7 @@ async function install(localOptions, cmd) {
|
|
|
559
557
|
manifest.dockerImage = image;
|
|
560
558
|
}
|
|
561
559
|
|
|
562
|
-
const location = options.location ||
|
|
560
|
+
const location = options.location || await readline.question('Location: ', {});
|
|
563
561
|
if (!location) return exit('');
|
|
564
562
|
|
|
565
563
|
const domainObject = await selectDomain(location, options);
|
|
@@ -604,10 +602,10 @@ async function install(localOptions, cmd) {
|
|
|
604
602
|
ports[tmp[0]] = parseInt(tmp[1], 10);
|
|
605
603
|
});
|
|
606
604
|
} else {
|
|
607
|
-
ports = queryPortBindings(null /* existing app */, manifest);
|
|
605
|
+
ports = await queryPortBindings(null /* existing app */, manifest);
|
|
608
606
|
}
|
|
609
607
|
} else { // just put in defaults
|
|
610
|
-
const allPorts =
|
|
608
|
+
const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
|
|
611
609
|
for (const portName in allPorts) {
|
|
612
610
|
ports[portName] = allPorts[portName].defaultValue;
|
|
613
611
|
}
|
|
@@ -669,7 +667,7 @@ async function setLocation(localOptions, cmd) {
|
|
|
669
667
|
const app = await getApp(options);
|
|
670
668
|
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
671
669
|
|
|
672
|
-
if (!options.location) options.location =
|
|
670
|
+
if (!options.location) options.location = await readline.question(`Enter new location (default: ${app.fqdn}): `, { });
|
|
673
671
|
const location = options.location || app.subdomain;
|
|
674
672
|
|
|
675
673
|
const domainObject = await selectDomain(location, options);
|
|
@@ -734,7 +732,7 @@ async function setLocation(localOptions, cmd) {
|
|
|
734
732
|
ports[tmp[0]] = parseInt(tmp[1], 10);
|
|
735
733
|
});
|
|
736
734
|
} else {
|
|
737
|
-
ports = queryPortBindings(app, app.manifest);
|
|
735
|
+
ports = await queryPortBindings(app, app.manifest);
|
|
738
736
|
}
|
|
739
737
|
|
|
740
738
|
for (const port in ports) {
|
|
@@ -788,10 +786,10 @@ async function update(localOptions, cmd) {
|
|
|
788
786
|
|
|
789
787
|
if (app.error && (app.error.installationState === 'pending_install')) { // install had failed. call repair to re-install
|
|
790
788
|
apiPath = `/api/v1/apps/${app.id}/repair`;
|
|
791
|
-
data =
|
|
789
|
+
data = Object.assign(data, { manifest });
|
|
792
790
|
} else {
|
|
793
791
|
apiPath = `/api/v1/apps/${app.id}/update`;
|
|
794
|
-
data =
|
|
792
|
+
data = Object.assign(data, {
|
|
795
793
|
appStoreId: options.appstoreId || '', // note case change
|
|
796
794
|
manifest: manifest,
|
|
797
795
|
skipBackup: !options.backup,
|
|
@@ -954,13 +952,13 @@ async function logs(localOptions, cmd) {
|
|
|
954
952
|
|
|
955
953
|
if (tail) {
|
|
956
954
|
const url = `${apiPath}?access_token=${token}&lines=10&format=json`;
|
|
957
|
-
|
|
955
|
+
const es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
|
|
958
956
|
|
|
959
|
-
es.
|
|
957
|
+
es.addEventListener('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
|
|
960
958
|
logPrinter(JSON.parse(e.data));
|
|
961
959
|
});
|
|
962
960
|
|
|
963
|
-
es.
|
|
961
|
+
es.addEventListener('error', function (error) {
|
|
964
962
|
if (error.status === 401) return exit('Please login first');
|
|
965
963
|
if (error.status === 412) exit('Logs currently not available.');
|
|
966
964
|
exit(error);
|
|
@@ -974,13 +972,13 @@ async function logs(localOptions, cmd) {
|
|
|
974
972
|
});
|
|
975
973
|
req.on('error', (error) => exit(`Pipe error: ${error.message}`));
|
|
976
974
|
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
.on('
|
|
975
|
+
const lineStream = new LineStream();
|
|
976
|
+
lineStream
|
|
977
|
+
.on('line', (line) => { logPrinter(JSON.parse(line)); } )
|
|
980
978
|
.on('error', (error) => exit(`JSON parse error: ${error.message}`))
|
|
981
979
|
.on('end', process.exit);
|
|
982
980
|
|
|
983
|
-
req.pipe(
|
|
981
|
+
req.pipe(lineStream);
|
|
984
982
|
}
|
|
985
983
|
}
|
|
986
984
|
|
|
@@ -1100,7 +1098,7 @@ async function backupList(localOptions, cmd) {
|
|
|
1100
1098
|
return backup;
|
|
1101
1099
|
}).sort(function (a, b) { return b.creationTime - a.creationTime; });
|
|
1102
1100
|
|
|
1103
|
-
|
|
1101
|
+
const t = new Table();
|
|
1104
1102
|
|
|
1105
1103
|
response.body.backups.forEach(function (backup) {
|
|
1106
1104
|
t.cell('Id', backup.id);
|
|
@@ -1242,9 +1240,9 @@ async function clone(localOptions, cmd) {
|
|
|
1242
1240
|
|
|
1243
1241
|
if (!options.backup) return exit('Use --backup to specify the backup id');
|
|
1244
1242
|
|
|
1245
|
-
const location = options.location ||
|
|
1243
|
+
const location = options.location || await readline.question('Cloned app location: ', {});
|
|
1246
1244
|
const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
|
|
1247
|
-
const ports = queryPortBindings(app, app.manifest);
|
|
1245
|
+
const ports = await queryPortBindings(app, app.manifest);
|
|
1248
1246
|
const backupId = options.backup;
|
|
1249
1247
|
|
|
1250
1248
|
const domainObject = await selectDomain(location, options);
|
|
@@ -1417,19 +1415,7 @@ function push(localDir, remote, localOptions, cmd) {
|
|
|
1417
1415
|
if (local === '-') {
|
|
1418
1416
|
localOptions._stdin = process.stdin;
|
|
1419
1417
|
} else if (stat) {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
localOptions._stdin = progress;
|
|
1423
|
-
fs.createReadStream(local).pipe(progress);
|
|
1424
|
-
|
|
1425
|
-
const bar = new ProgressBar('Uploading [:bar] :percent: :etas', {
|
|
1426
|
-
complete: '=',
|
|
1427
|
-
incomplete: ' ',
|
|
1428
|
-
width: 100,
|
|
1429
|
-
total: stat.size
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
progress.on('progress', function (p) { bar.update(p.percentage / 100); /* bar.tick(p.transferred - bar.curr); */ });
|
|
1418
|
+
localOptions._stdin = fs.createReadStream(local);
|
|
1433
1419
|
} else {
|
|
1434
1420
|
exit('local file ' + local + ' does not exist');
|
|
1435
1421
|
}
|
package/src/appstore-actions.js
CHANGED
|
@@ -9,9 +9,8 @@ const assert = require('assert'),
|
|
|
9
9
|
{ exit, locateManifest } = require('./helper.js'),
|
|
10
10
|
manifestFormat = require('cloudron-manifestformat'),
|
|
11
11
|
path = require('path'),
|
|
12
|
-
|
|
12
|
+
readline = require('./readline.js'),
|
|
13
13
|
safe = require('safetydance'),
|
|
14
|
-
semver = require('semver'),
|
|
15
14
|
superagent = require('superagent'),
|
|
16
15
|
Table = require('easy-table');
|
|
17
16
|
|
|
@@ -24,6 +23,7 @@ exports = module.exports = {
|
|
|
24
23
|
upload,
|
|
25
24
|
revoke,
|
|
26
25
|
approve,
|
|
26
|
+
verifyManifest,
|
|
27
27
|
|
|
28
28
|
notify
|
|
29
29
|
};
|
|
@@ -71,8 +71,8 @@ async function authenticate(options) { // maybe we can use options.token to vali
|
|
|
71
71
|
console.log(`${webDomain} login` + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const email = options.email ||
|
|
75
|
-
const password = options.password ||
|
|
74
|
+
const email = options.email || await readline.question('Email: ', {});
|
|
75
|
+
const password = options.password || await readline.question('Password: ', { noEchoBack: true });
|
|
76
76
|
|
|
77
77
|
config.setAppStoreToken(null);
|
|
78
78
|
|
|
@@ -80,7 +80,7 @@ async function authenticate(options) { // maybe we can use options.token to vali
|
|
|
80
80
|
if (response.statusCode === 401 && response.body.message.indexOf('TOTP') !== -1) {
|
|
81
81
|
if (response.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
|
|
82
82
|
|
|
83
|
-
options.totpToken =
|
|
83
|
+
options.totpToken = await readline.question('2FA token: ', {});
|
|
84
84
|
options.email = email;
|
|
85
85
|
options.password = password;
|
|
86
86
|
options.hideBanner = true;
|
|
@@ -265,6 +265,36 @@ async function updateVersion(manifest, baseDir, options) {
|
|
|
265
265
|
if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
async function verifyManifest(localOptions, cmd) {
|
|
269
|
+
const options = cmd.optsWithGlobals();
|
|
270
|
+
// try to find the manifest of this project
|
|
271
|
+
const manifestFilePath = locateManifest();
|
|
272
|
+
if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
|
|
273
|
+
|
|
274
|
+
const result = manifestFormat.parseFile(manifestFilePath);
|
|
275
|
+
if (result.error) return exit(result.error.message);
|
|
276
|
+
|
|
277
|
+
const manifest = result.manifest;
|
|
278
|
+
|
|
279
|
+
const sourceDir = path.dirname(manifestFilePath);
|
|
280
|
+
const appConfig = config.getAppConfig(sourceDir);
|
|
281
|
+
|
|
282
|
+
// image can be passed in options for buildbot
|
|
283
|
+
if (options.image) {
|
|
284
|
+
manifest.dockerImage = options.image;
|
|
285
|
+
} else {
|
|
286
|
+
manifest.dockerImage = appConfig.dockerImage;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
|
|
290
|
+
|
|
291
|
+
// ensure we remove the docker hub handle
|
|
292
|
+
if (manifest.dockerImage.indexOf('docker.io/') === 0) manifest.dockerImage = manifest.dockerImage.slice('docker.io/'.length);
|
|
293
|
+
|
|
294
|
+
const error = manifestFormat.checkAppstoreRequirements(manifest);
|
|
295
|
+
if (error) return exit(error);
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
async function upload(localOptions, cmd) {
|
|
269
299
|
const options = cmd.optsWithGlobals();
|
|
270
300
|
// try to find the manifest of this project
|
|
@@ -384,7 +414,7 @@ async function approve(localOptions, cmd) {
|
|
|
384
414
|
if (safe.error) return exit(`Failed to get head of ${defaultBranch}: ${safe.error.message}`);
|
|
385
415
|
latestTagSha = latestTagSha.trim();
|
|
386
416
|
|
|
387
|
-
if (defaultBranchSha !== latestTagSha)
|
|
417
|
+
if (defaultBranchSha !== latestTagSha) console.warn(`Latest tag ${latestTag} does not match HEAD of ${defaultBranch}`);
|
|
388
418
|
}
|
|
389
419
|
|
|
390
420
|
const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`, options);
|
package/src/backup-tools.js
CHANGED
|
@@ -11,7 +11,6 @@ exports = module.exports = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const assert = require('assert'),
|
|
14
|
-
async = require('async'),
|
|
15
14
|
crypto = require('crypto'),
|
|
16
15
|
debug = require('debug')('cloudron-backup'),
|
|
17
16
|
fs = require('fs'),
|
|
@@ -268,15 +267,16 @@ async function decryptDir(inDir, outDir, options) {
|
|
|
268
267
|
const outDirAbs = path.resolve(process.cwd(), outDir);
|
|
269
268
|
|
|
270
269
|
const tbd = [ '' ]; // only has paths relative to inDirAbs
|
|
271
|
-
|
|
270
|
+
while (true) {
|
|
272
271
|
const cur = tbd.pop();
|
|
273
272
|
const entries = fs.readdirSync(path.join(inDirAbs, cur), { withFileTypes: true });
|
|
274
|
-
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
275
|
if (entry.isDirectory()) {
|
|
276
276
|
tbd.push(path.join(cur, entry.name));
|
|
277
|
-
|
|
277
|
+
continue;
|
|
278
278
|
} else if (!entry.isFile()) {
|
|
279
|
-
|
|
279
|
+
continue;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
const encryptedFilePath = path.join(cur, entry.name);
|
|
@@ -298,8 +298,10 @@ async function decryptDir(inDir, outDir, options) {
|
|
|
298
298
|
safe.fs.rmSync(outfile);
|
|
299
299
|
throw new Error(`Could not decrypt ${infile}: ${decryptError.message}`);
|
|
300
300
|
}
|
|
301
|
-
}
|
|
302
|
-
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (tbd.length === 0) break;
|
|
304
|
+
}
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
async function decryptFilename(filePath, options) {
|
package/src/build-actions.js
CHANGED
|
@@ -11,14 +11,14 @@ exports = module.exports = {
|
|
|
11
11
|
const assert = require('assert'),
|
|
12
12
|
config = require('./config.js'),
|
|
13
13
|
crypto = require('crypto'),
|
|
14
|
-
EventSource = require('eventsource'),
|
|
14
|
+
{ EventSource } = require('eventsource'),
|
|
15
15
|
execSync = require('child_process').execSync,
|
|
16
16
|
exit = require('./helper.js').exit,
|
|
17
17
|
fs = require('fs'),
|
|
18
18
|
helper = require('./helper.js'),
|
|
19
19
|
manifestFormat = require('cloudron-manifestformat'),
|
|
20
20
|
micromatch = require('micromatch'),
|
|
21
|
-
|
|
21
|
+
readline = require('./readline.js'),
|
|
22
22
|
superagent = require('superagent'),
|
|
23
23
|
os = require('os'),
|
|
24
24
|
path = require('path'),
|
|
@@ -34,7 +34,7 @@ function requestError(response) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// analyzes options and merges with any existing build service config
|
|
37
|
-
function
|
|
37
|
+
async function resolveBuildServiceConfig(options) {
|
|
38
38
|
const buildService = config.getBuildServiceConfig();
|
|
39
39
|
if (!buildService.type) buildService.type = 'local'; // default
|
|
40
40
|
|
|
@@ -49,7 +49,7 @@ function getBuildServiceConfig(options) {
|
|
|
49
49
|
if (typeof options.setBuildService === 'string') {
|
|
50
50
|
url = options.setBuildService;
|
|
51
51
|
} else {
|
|
52
|
-
url =
|
|
52
|
+
url = await readline.question('Enter build service URL: ', { });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (url.indexOf('://') === -1) url = `https://${url}`;
|
|
@@ -66,11 +66,11 @@ function getBuildServiceConfig(options) {
|
|
|
66
66
|
async function login(localOptions, cmd) {
|
|
67
67
|
const options = cmd.optsWithGlobals();
|
|
68
68
|
|
|
69
|
-
const buildServiceConfig =
|
|
69
|
+
const buildServiceConfig = await resolveBuildServiceConfig(options);
|
|
70
70
|
|
|
71
71
|
console.log('Build Service login' + ` (${buildServiceConfig.url}):`);
|
|
72
72
|
|
|
73
|
-
const token = options.buildServiceToken ||
|
|
73
|
+
const token = options.buildServiceToken || await readline.question('Token: ', {});
|
|
74
74
|
|
|
75
75
|
const response = await superagent.get(`${buildServiceConfig.url}/api/v1/profile`).query({ accessToken: token }).ok(() => true);
|
|
76
76
|
if (response.statusCode === 401 || response.statusCode === 403) return exit(`Authentication error: ${requestError(response)}`);
|
|
@@ -93,7 +93,7 @@ async function followBuildLog(buildId, raw) {
|
|
|
93
93
|
const es = new EventSource(`${tmp.href}api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
|
|
94
94
|
let prevId = null, prevWasStatus = false;
|
|
95
95
|
|
|
96
|
-
es.
|
|
96
|
+
es.addEventListener('message', function (e) {
|
|
97
97
|
if (raw) return console.dir(e);
|
|
98
98
|
|
|
99
99
|
const data = safe.JSON.parse(e.data);
|
|
@@ -138,15 +138,15 @@ async function followBuildLog(buildId, raw) {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
let didConnect = false;
|
|
141
|
-
es.
|
|
141
|
+
es.addEventListener('open', () => didConnect = true, { once: true });
|
|
142
142
|
|
|
143
143
|
return new Promise((resolve, reject) => {
|
|
144
|
-
es.
|
|
144
|
+
es.addEventListener('error', function (error) { // server close or network error or some interruption
|
|
145
145
|
if (raw) console.dir(error);
|
|
146
146
|
|
|
147
147
|
es.close();
|
|
148
148
|
if (didConnect) resolve(); else reject(new Error('Failed to connect'));
|
|
149
|
-
});
|
|
149
|
+
}, { once: true });
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -315,14 +315,14 @@ async function build(localOptions, cmd) {
|
|
|
315
315
|
const sourceDir = path.dirname(manifestFilePath);
|
|
316
316
|
|
|
317
317
|
const appConfig = config.getAppConfig(sourceDir);
|
|
318
|
-
const buildServiceConfig =
|
|
318
|
+
const buildServiceConfig = await resolveBuildServiceConfig(options);
|
|
319
319
|
|
|
320
320
|
let repository = appConfig.repository;
|
|
321
321
|
if (!repository || options.setRepository) {
|
|
322
322
|
if (typeof options.setRepository === 'string') {
|
|
323
323
|
repository = options.setRepository;
|
|
324
324
|
} else {
|
|
325
|
-
repository =
|
|
325
|
+
repository = await readline.question(`Enter docker repository (e.g registry/username/${manifest.id || path.basename(sourceDir)}): `, {});
|
|
326
326
|
if (!repository) exit('No repository provided');
|
|
327
327
|
console.log();
|
|
328
328
|
}
|
|
@@ -374,7 +374,7 @@ async function push(localOptions, cmd) {
|
|
|
374
374
|
tag = options.tag;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
const buildServiceConfig =
|
|
377
|
+
const buildServiceConfig = await resolveBuildServiceConfig(options);
|
|
378
378
|
|
|
379
379
|
const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds/${options.id}/push`)
|
|
380
380
|
.query({ accessToken: buildServiceConfig.token })
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Transform } = require('stream');
|
|
4
|
+
|
|
5
|
+
class LineStream extends Transform {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super();
|
|
8
|
+
this._buffer = '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_transform(chunk, encoding, callback) {
|
|
12
|
+
this._buffer += chunk.toString('utf8');
|
|
13
|
+
|
|
14
|
+
const lines = this._buffer.split('\n');
|
|
15
|
+
this._buffer = lines.pop(); // maybe incomplete line
|
|
16
|
+
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
this.emit('line', line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
callback();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_flush(callback) {
|
|
25
|
+
if (this._buffer) this.emit('line', this.buff_bufferer);
|
|
26
|
+
callback();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports = module.exports = LineStream;
|
package/src/readline.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports = module.exports = {
|
|
4
|
+
question
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const readline = require('node:readline/promises'),
|
|
8
|
+
{ Writable } = require('node:stream');
|
|
9
|
+
|
|
10
|
+
async function question(query, options) {
|
|
11
|
+
const output = options.noEchoBack
|
|
12
|
+
? new Writable({ write: function (chunk, encoding, callback) { callback(); } })
|
|
13
|
+
: process.stdout;
|
|
14
|
+
process.stdin.setRawMode(options.noEchoBack); // raw mode gives each keypress as opposd to line based cooked mode
|
|
15
|
+
const rl = readline.createInterface({ input: process.stdin, output });
|
|
16
|
+
if (options.noEchoBack) process.stdout.write(query);
|
|
17
|
+
const answer = await rl.question(query, options);
|
|
18
|
+
rl.close();
|
|
19
|
+
if (options.noEchoBack) process.stdout.write('\n');
|
|
20
|
+
return answer;
|
|
21
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports = module.exports = {
|
|
4
|
+
get,
|
|
5
|
+
put,
|
|
6
|
+
post,
|
|
7
|
+
patch,
|
|
8
|
+
del,
|
|
9
|
+
options,
|
|
10
|
+
request
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// IMPORTANT: do not require box code here . This is used by migration scripts
|
|
14
|
+
const assert = require('assert'),
|
|
15
|
+
consumers = require('node:stream/consumers'),
|
|
16
|
+
debug = require('debug')('box:superagent'),
|
|
17
|
+
fs = require('fs'),
|
|
18
|
+
http = require('http'),
|
|
19
|
+
https = require('https'),
|
|
20
|
+
path = require('path'),
|
|
21
|
+
safe = require('safetydance');
|
|
22
|
+
|
|
23
|
+
class Request {
|
|
24
|
+
constructor(method, url) {
|
|
25
|
+
assert.strictEqual(typeof url, 'string');
|
|
26
|
+
|
|
27
|
+
this.url = new URL(url);
|
|
28
|
+
this.options = {
|
|
29
|
+
method,
|
|
30
|
+
headers: {},
|
|
31
|
+
signal: null // set for timeouts
|
|
32
|
+
};
|
|
33
|
+
this.okFunc = ({ status }) => status >=200 && status <= 299;
|
|
34
|
+
this.timer = { timeout: 0, id: null, controller: null };
|
|
35
|
+
this.retryCount = 0;
|
|
36
|
+
this.body = null;
|
|
37
|
+
this.redirectCount = 5;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async _handleResponse(url, response) {
|
|
41
|
+
// const contentLength = response.headers['content-length'];
|
|
42
|
+
// if (!contentLength || contentLength > 5*1024*1024*1024) throw new Error(`Response size unknown or too large: ${contentLength}`);
|
|
43
|
+
const [consumeError, data] = await safe(consumers.buffer(response)); // have to drain response
|
|
44
|
+
if (consumeError) throw new Error(`Error consuming body stream: ${consumeError.message}`);
|
|
45
|
+
if (!response.complete) throw new Error('Incomplete response');
|
|
46
|
+
const contentType = response.headers['content-type'];
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
url: new URL(response.headers['location'] || '', url),
|
|
50
|
+
status: response.statusCode,
|
|
51
|
+
headers: response.headers,
|
|
52
|
+
body: null,
|
|
53
|
+
text: null
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (contentType?.includes('application/json')) {
|
|
57
|
+
result.text = data.toString('utf8');
|
|
58
|
+
if (data.byteLength !== 0) result.body = safe.JSON.parse(result.text) || {};
|
|
59
|
+
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
60
|
+
result.text = data.toString('utf8');
|
|
61
|
+
const searchParams = new URLSearchParams(data);
|
|
62
|
+
result.body = Object.fromEntries(searchParams.entries());
|
|
63
|
+
} else if (!contentType || contentType.startsWith('text/')) {
|
|
64
|
+
result.body = data;
|
|
65
|
+
result.text = result.body.toString('utf8');
|
|
66
|
+
} else {
|
|
67
|
+
result.body = data;
|
|
68
|
+
result.text = `<binary data (${data.byteLength} bytes)>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async _makeRequest(url) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const proto = url.protocol === 'https:' ? https : http;
|
|
77
|
+
const request = proto.request(url, this.options); // ClientRequest
|
|
78
|
+
|
|
79
|
+
request.on('error', reject); // network error, dns error
|
|
80
|
+
request.on('response', async (response) => {
|
|
81
|
+
const [error, result] = await safe(this._handleResponse(url, response));
|
|
82
|
+
if (error) reject(error); else resolve(result);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (this.body) request.write(this.body);
|
|
86
|
+
|
|
87
|
+
request.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async _start() {
|
|
92
|
+
let error;
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < this.retryCount+1; i++) {
|
|
95
|
+
if (this.timer.timeout) this.timer.id = setTimeout(() => this.timer.controller.abort(), this.timer.timeout);
|
|
96
|
+
debug(`${this.options.method} ${this.url.toString()}` + (i ? ` try ${i+1}` : ''));
|
|
97
|
+
|
|
98
|
+
let response, url = this.url;
|
|
99
|
+
for (let redirects = 0; redirects < this.redirectCount+1; redirects++) {
|
|
100
|
+
[error, response] = await safe(this._makeRequest(url));
|
|
101
|
+
if (error || (response.status < 300 || response.status > 399) || (this.options.method !== 'GET')) break;
|
|
102
|
+
url = response.url; // follow
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!error && !this.okFunc({ status: response.status })) {
|
|
106
|
+
error = new Error(`${response.status} ${http.STATUS_CODES[response.status]}`);
|
|
107
|
+
Object.assign(error, response);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (error) debug(`${this.options.method} ${this.url.toString()} ${error.message}`);
|
|
111
|
+
if (this.timer.timeout) clearTimeout(this.timer.id);
|
|
112
|
+
if (!error) return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set(name, value) {
|
|
119
|
+
this.options.headers[name.toLowerCase()] = value;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
query(data) {
|
|
124
|
+
Object.entries(data).forEach(([key, value]) => this.url.searchParams.append(key, value));
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
redirects(count) {
|
|
129
|
+
this.redirectCount = count;
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
send(data) {
|
|
134
|
+
const contentType = this.options.headers['content-type'];
|
|
135
|
+
if (!contentType || contentType === 'application/json') {
|
|
136
|
+
this.options.headers['content-type'] = 'application/json';
|
|
137
|
+
this.body = Buffer.from(JSON.stringify(data), 'utf8');
|
|
138
|
+
this.options.headers['content-length'] = this.body.byteLength;
|
|
139
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
140
|
+
this.body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
|
|
141
|
+
this.options.headers['content-length'] = this.body.byteLength;
|
|
142
|
+
}
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
timeout(msecs) {
|
|
147
|
+
this.timer.controller = new AbortController();
|
|
148
|
+
this.timer.timeout = msecs;
|
|
149
|
+
this.options.signal = this.timer.controller.signal;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
retry(count) {
|
|
154
|
+
this.retryCount = Math.max(0, count);
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ok(func) {
|
|
159
|
+
this.okFunc = func;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
disableTLSCerts() {
|
|
164
|
+
this.options.rejectUnauthorized = true;
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
auth(username, password) {
|
|
169
|
+
this.set('Authorization', 'Basic ' + btoa(`${username}:${password}`));
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
attach(name, filepath) { // this is only used in tests and thus simplistic
|
|
174
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
175
|
+
|
|
176
|
+
const partHeader = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${path.basename(filepath)}"\r\n\r\n`, 'utf8');
|
|
177
|
+
const partData = fs.readFileSync(filepath);
|
|
178
|
+
const partTrailer = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
|
|
179
|
+
this.body = Buffer.concat([partHeader, partData, partTrailer]);
|
|
180
|
+
|
|
181
|
+
this.options.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
|
182
|
+
this.options.headers['content-length'] = this.body.byteLength;
|
|
183
|
+
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
then(onFulfilled, onRejected) {
|
|
188
|
+
this._start().then(onFulfilled, onRejected);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function get(url) { return new Request('GET', url); }
|
|
193
|
+
function put(url) { return new Request('PUT', url); }
|
|
194
|
+
function post(url) { return new Request('POST', url); }
|
|
195
|
+
function patch(url) { return new Request('PATCH', url); }
|
|
196
|
+
function del(url) { return new Request('DELETE', url); }
|
|
197
|
+
function options(url) { return new Request('OPTIONS', url); }
|
|
198
|
+
function request(method, url) { return new Request(method, url); }
|
package/test/test.js
CHANGED
|
@@ -32,7 +32,7 @@ function md5(file) {
|
|
|
32
32
|
function cli(args, options) {
|
|
33
33
|
// https://github.com/nodejs/node-v0.x-archive/issues/9265
|
|
34
34
|
options = options || { };
|
|
35
|
-
args =
|
|
35
|
+
args = Array.isArray(args) ? args : args.match(/[^\s"]+|"([^"]+)"/g);
|
|
36
36
|
args = args.map(function (e) { return e[0] === '"' ? e.slice(1, -1) : e; }); // remove the quotes
|
|
37
37
|
|
|
38
38
|
try {
|