cloudron 7.0.6 → 7.1.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 +45 -0
- package/bin/cloudron +154 -6
- package/package.json +6 -6
- package/src/actions.js +427 -107
- package/src/helper.js +12 -4
- package/src/login-success.html +50 -0
- package/bin/cloudron-appstore +0 -65
- package/bin/cloudron-build +0 -71
- package/bin/cloudron-versions +0 -33
package/CHANGELOG.md
CHANGED
|
@@ -49,3 +49,48 @@
|
|
|
49
49
|
* Various changes to accomodate Cloudron 9 release API changes
|
|
50
50
|
* add --siteId to cloudron backup to specify site id explicitly
|
|
51
51
|
|
|
52
|
+
[7.0.0]
|
|
53
|
+
* Port from CommonJS to ESM
|
|
54
|
+
* Add initial OIDC login support
|
|
55
|
+
* Add `cloudron versions` subcommand
|
|
56
|
+
* `versions`: init with publisher info, add `--version` flag, add publishes by default
|
|
57
|
+
* Add `cloudron build info` subcommand
|
|
58
|
+
* Add `cloudron build clear` subcommand
|
|
59
|
+
* Add `cloudron build logout` subcommand
|
|
60
|
+
* Rework the build command
|
|
61
|
+
* Support source tarball upload with install and update subcommands
|
|
62
|
+
* Remove calling the /repair route in update subcommand
|
|
63
|
+
* Use POST /api/v1/apps for installations
|
|
64
|
+
* Remove unused superagent module
|
|
65
|
+
* Do not print stack if app isn't found
|
|
66
|
+
|
|
67
|
+
[7.0.1]
|
|
68
|
+
* Fix `--no-wait` flag
|
|
69
|
+
|
|
70
|
+
[7.0.2]
|
|
71
|
+
* Rename `--set-repository` to `--repository`
|
|
72
|
+
* Fix broken build command
|
|
73
|
+
|
|
74
|
+
[7.0.3]
|
|
75
|
+
* Implement `versionsUrl` for testing
|
|
76
|
+
* Fix app detection to be based on appId
|
|
77
|
+
* Save appId on update
|
|
78
|
+
|
|
79
|
+
[7.0.4]
|
|
80
|
+
* Rename `build clear` to `build reset`
|
|
81
|
+
* `versions`: add fields in init
|
|
82
|
+
* Set minBoxVersion
|
|
83
|
+
* Print the build type
|
|
84
|
+
|
|
85
|
+
[7.0.5]
|
|
86
|
+
* Gzip the source tarball for upload
|
|
87
|
+
* Print where the image is coming from
|
|
88
|
+
|
|
89
|
+
[7.0.6]
|
|
90
|
+
* Remove duplicate build info output
|
|
91
|
+
* Do not error on EROFS
|
|
92
|
+
|
|
93
|
+
[7.1.0]
|
|
94
|
+
* Add `cloudron sync push` and `cloudron sync pull` subcommands
|
|
95
|
+
* Merge all binaries into single `cloudron` command
|
|
96
|
+
|
package/bin/cloudron
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import actions from '../src/actions.js';
|
|
4
|
+
import appstoreActions from '../src/appstore-actions.js';
|
|
4
5
|
import backupTools from '../src/backup-tools.js';
|
|
6
|
+
import buildActions from '../src/build-actions.js';
|
|
5
7
|
import completion from '../src/completion.js';
|
|
6
8
|
import { Command } from 'commander';
|
|
7
9
|
import semver from 'semver';
|
|
10
|
+
import versionsActions from '../src/versions-actions.js';
|
|
8
11
|
import pkg from '../package.json' with { type: 'json' };
|
|
9
12
|
|
|
10
13
|
// ensure node version
|
|
@@ -23,13 +26,116 @@ program.option('--server <server>', 'Cloudron domain')
|
|
|
23
26
|
.option('--accept-selfsigned', 'Accept self signed SSL certificate')
|
|
24
27
|
.option('--no-wait', 'Do not wait for the operation to finish');
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const appstoreCommand = program.command('appstore').description('Commands for publishing to the Appstore')
|
|
30
|
+
.option('--appstore-token <token>', 'AppStore token');
|
|
31
|
+
|
|
32
|
+
appstoreCommand.command('login')
|
|
33
|
+
.description('Login to the appstore')
|
|
34
|
+
.option('-e, --email <email>', 'Email address')
|
|
35
|
+
.option('-p, --password <password>', 'Password (unsafe)')
|
|
36
|
+
.action(appstoreActions.login);
|
|
37
|
+
|
|
38
|
+
appstoreCommand.command('logout')
|
|
39
|
+
.description('Logout from the appstore')
|
|
40
|
+
.action(appstoreActions.logout);
|
|
41
|
+
|
|
42
|
+
appstoreCommand.command('info')
|
|
43
|
+
.description('List info of published app')
|
|
44
|
+
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
45
|
+
.action(appstoreActions.info);
|
|
46
|
+
|
|
47
|
+
appstoreCommand.command('approve')
|
|
48
|
+
.description('Approve a submitted app version')
|
|
49
|
+
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
50
|
+
.option('--no-git-push', 'Do not attempt to push to git repo')
|
|
51
|
+
.action(appstoreActions.approve);
|
|
52
|
+
|
|
53
|
+
appstoreCommand.command('notify')
|
|
54
|
+
.description('Notify forum about successful app submission')
|
|
55
|
+
.action(appstoreActions.notify);
|
|
56
|
+
|
|
57
|
+
appstoreCommand.command('revoke')
|
|
58
|
+
.description('Revoke a published app version')
|
|
59
|
+
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
60
|
+
.action(appstoreActions.revoke);
|
|
61
|
+
|
|
62
|
+
appstoreCommand.command('submit')
|
|
63
|
+
.description('Submit app to the store for review')
|
|
64
|
+
.action(appstoreActions.submit);
|
|
65
|
+
|
|
66
|
+
appstoreCommand.command('upload')
|
|
67
|
+
.description('Upload app to the store for testing')
|
|
68
|
+
.option('-i, --image <image>', 'Docker image')
|
|
69
|
+
.option('-f, --force', 'Update existing version')
|
|
70
|
+
.action(appstoreActions.upload);
|
|
71
|
+
|
|
72
|
+
appstoreCommand.command('verify-manifest')
|
|
73
|
+
.description('Verify if manifest is ready to be upload/published')
|
|
74
|
+
.action(appstoreActions.verifyManifest);
|
|
75
|
+
|
|
76
|
+
appstoreCommand.command('versions')
|
|
77
|
+
.alias('list')
|
|
78
|
+
.description('List published versions')
|
|
79
|
+
.option('--appstore-id <id>', 'Appstore id')
|
|
80
|
+
.option('--raw', 'Dump versions as json')
|
|
81
|
+
.action(appstoreActions.listVersions);
|
|
82
|
+
|
|
83
|
+
const backupCommand = program.command('backup').description('App backup commands');
|
|
84
|
+
|
|
85
|
+
function collectBuildArgs(value, collected) {
|
|
86
|
+
collected.push(value);
|
|
87
|
+
return collected;
|
|
88
|
+
}
|
|
30
89
|
|
|
31
|
-
const
|
|
32
|
-
.
|
|
90
|
+
const buildCommand = program.command('build').description('Build using the build service')
|
|
91
|
+
.option('--build-service-url <url>', 'Build service URL')
|
|
92
|
+
.option('--build-service-token <token>', 'Build service token');
|
|
93
|
+
|
|
94
|
+
buildCommand.command('build', { isDefault: true })
|
|
95
|
+
.description('Build an app')
|
|
96
|
+
.option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectBuildArgs, [])
|
|
97
|
+
.option('-f, --file <dockerfile>', 'Name of the Dockerfile')
|
|
98
|
+
.option('--repository [repository url]', 'Change the repository. This url is stored for future builds for this project. e.g registry/username/projectname')
|
|
99
|
+
.option('--no-cache', 'Do not use cache')
|
|
100
|
+
.option('--no-push', 'Do not push built image to registry')
|
|
101
|
+
.option('--raw', 'Raw output build log')
|
|
102
|
+
.option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
|
|
103
|
+
.action(buildActions.build);
|
|
104
|
+
|
|
105
|
+
buildCommand.command('reset')
|
|
106
|
+
.description('Reset build configuration for this directory')
|
|
107
|
+
.action(buildActions.reset);
|
|
108
|
+
|
|
109
|
+
buildCommand.command('info')
|
|
110
|
+
.description('Print build information')
|
|
111
|
+
.action(buildActions.info);
|
|
112
|
+
|
|
113
|
+
buildCommand.command('login')
|
|
114
|
+
.description('Login to the build service')
|
|
115
|
+
.action(buildActions.login);
|
|
116
|
+
|
|
117
|
+
buildCommand.command('logout')
|
|
118
|
+
.description('Logout from the build service')
|
|
119
|
+
.action(buildActions.logout);
|
|
120
|
+
|
|
121
|
+
buildCommand.command('logs')
|
|
122
|
+
.description('Build logs. This works only when using the Build Service')
|
|
123
|
+
.option('--id <buildid>', 'Build ID')
|
|
124
|
+
.option('--raw', 'Raw output build log')
|
|
125
|
+
.action(buildActions.logs);
|
|
126
|
+
|
|
127
|
+
buildCommand.command('push')
|
|
128
|
+
.description('Push the build image')
|
|
129
|
+
.option('--id <buildid>', 'Build ID')
|
|
130
|
+
.option('--repository [repository url]', 'Set repository to push to. e.g registry/username/projectname')
|
|
131
|
+
.option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
|
|
132
|
+
.option('--image <docker image>', 'Docker image of the form registry/repo:tag')
|
|
133
|
+
.action(buildActions.push);
|
|
134
|
+
|
|
135
|
+
buildCommand.command('status')
|
|
136
|
+
.description('Build status. This works only when using the Build Service')
|
|
137
|
+
.option('--id <buildid>', 'Build ID')
|
|
138
|
+
.action(buildActions.status);
|
|
33
139
|
|
|
34
140
|
backupCommand.command('create')
|
|
35
141
|
.description('Create new app backup')
|
|
@@ -223,6 +329,23 @@ program.command('push <local> <remote>')
|
|
|
223
329
|
console.log();
|
|
224
330
|
});
|
|
225
331
|
|
|
332
|
+
const sync = program.command('sync')
|
|
333
|
+
.description('Sync files between local and remote (only transfers changed files)');
|
|
334
|
+
|
|
335
|
+
sync.command('push <local> <remote>')
|
|
336
|
+
.description('Upload changed files to remote directory. A trailing slash on <local> syncs its contents; without it, the directory itself is placed inside <remote>.')
|
|
337
|
+
.option('--app <id/location>', 'App id or location')
|
|
338
|
+
.option('--delete', 'Delete remote files not present locally')
|
|
339
|
+
.option('--force', 'Remove files blocking directory creation at destination')
|
|
340
|
+
.action(actions.syncPush);
|
|
341
|
+
|
|
342
|
+
sync.command('pull <remote> <local>')
|
|
343
|
+
.description('Download changed files from remote directory. A trailing slash on <remote> syncs its contents; without it, the directory itself is placed inside <local>.')
|
|
344
|
+
.option('--app <id/location>', 'App id or location')
|
|
345
|
+
.option('--delete', 'Delete local files not present on remote')
|
|
346
|
+
.option('--force', 'Remove files blocking directory creation at destination')
|
|
347
|
+
.action(actions.syncPull);
|
|
348
|
+
|
|
226
349
|
program.command('repair')
|
|
227
350
|
.description('Repair an installed application (re-configure)')
|
|
228
351
|
.option('--app <id/location>', 'App id or location')
|
|
@@ -279,4 +402,29 @@ program.command('update')
|
|
|
279
402
|
.option('--no-force', 'Match appstore id and manifest id before updating', true)
|
|
280
403
|
.action(actions.update);
|
|
281
404
|
|
|
405
|
+
const versionsCommand = program.command('versions').description('Commands for publishing community packages');
|
|
406
|
+
|
|
407
|
+
versionsCommand.command('add')
|
|
408
|
+
.description('Add the current build to version file')
|
|
409
|
+
.option('--state <state>', 'Publish state (published or testing)')
|
|
410
|
+
.action(versionsActions.addOrUpdate);
|
|
411
|
+
|
|
412
|
+
versionsCommand.command('init')
|
|
413
|
+
.description('Create versions file')
|
|
414
|
+
.action(versionsActions.init);
|
|
415
|
+
|
|
416
|
+
versionsCommand.command('list')
|
|
417
|
+
.description('List existing versions')
|
|
418
|
+
.action(versionsActions.list);
|
|
419
|
+
|
|
420
|
+
versionsCommand.command('revoke')
|
|
421
|
+
.description('Revoke the latest version')
|
|
422
|
+
.action(versionsActions.revoke);
|
|
423
|
+
|
|
424
|
+
versionsCommand.command('update')
|
|
425
|
+
.description('Update existing version')
|
|
426
|
+
.option('--version <version>', 'Version to update')
|
|
427
|
+
.option('--state <state>', 'Publish state (published or testing)')
|
|
428
|
+
.action(versionsActions.addOrUpdate);
|
|
429
|
+
|
|
282
430
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudron",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Cloudron Commandline Tool",
|
|
6
6
|
"type": "module",
|
|
@@ -17,26 +17,26 @@
|
|
|
17
17
|
},
|
|
18
18
|
"author": "Cloudron Developers <support@cloudron.io>",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@cloudron/manifest-format": "^6.0
|
|
20
|
+
"@cloudron/manifest-format": "^6.1.0",
|
|
21
21
|
"@cloudron/safetydance": "^3.0.1",
|
|
22
22
|
"@cloudron/superagent": "^2.1.1",
|
|
23
23
|
"commander": "^14.0.3",
|
|
24
24
|
"debug": "^4.4.3",
|
|
25
25
|
"easy-table": "^1.2.0",
|
|
26
|
-
"ejs": "^
|
|
26
|
+
"ejs": "^5.0.1",
|
|
27
27
|
"eventsource": "^4.1.0",
|
|
28
28
|
"micromatch": "^4.0.8",
|
|
29
29
|
"open": "^11.0.0",
|
|
30
30
|
"semver": "^7.7.4",
|
|
31
|
-
"tar-fs": "^3.1.
|
|
31
|
+
"tar-fs": "^3.1.2"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">= 20.11.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
|
38
|
-
"eslint": "^10.0.
|
|
39
|
-
"globals": "^17.
|
|
38
|
+
"eslint": "^10.0.3",
|
|
39
|
+
"globals": "^17.4.0",
|
|
40
40
|
"mocha": "^11.7.5"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/actions.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as readline from './readline.js';
|
|
|
13
13
|
import safe from '@cloudron/safetydance';
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import semver from 'semver';
|
|
16
|
+
import { PassThrough } from 'stream';
|
|
16
17
|
import stream from 'stream/promises';
|
|
17
18
|
import superagent from '@cloudron/superagent';
|
|
18
19
|
import Table from 'easy-table';
|
|
@@ -30,7 +31,7 @@ function requestOptions(options) {
|
|
|
30
31
|
// ensure config can return the correct section
|
|
31
32
|
config.setActive(adminFqdn);
|
|
32
33
|
|
|
33
|
-
const token = options.token || config.token();
|
|
34
|
+
const token = options.token || process.env.CLOUDRON_CLI_AUTH_TOKEN || config.token();
|
|
34
35
|
const rejectUnauthorized = !(options.allowSelfsigned || options.acceptSelfsigned || config.allowSelfsigned());
|
|
35
36
|
|
|
36
37
|
if (!adminFqdn && !token) return exit('Login with "cloudron login" first'); // a bit rough to put this here!
|
|
@@ -1426,6 +1427,187 @@ function demuxStream(inputStream, stdout, stderr) {
|
|
|
1426
1427
|
});
|
|
1427
1428
|
}
|
|
1428
1429
|
|
|
1430
|
+
async function createExecSession(appId, args, { tty, lang }, options) {
|
|
1431
|
+
const request = createRequest('POST', `/api/v1/apps/${appId}/exec`, options);
|
|
1432
|
+
const response = await request.send({ cmd: args, tty, lang });
|
|
1433
|
+
if (response.status !== 200) exit(`Failed to create exec: ${requestError(response)}`);
|
|
1434
|
+
return response.body.id;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async function getExecSession(appId, execId, options) {
|
|
1438
|
+
const response = await createRequest('GET', `/api/v1/apps/${appId}/exec/${execId}`, options);
|
|
1439
|
+
if (response.status !== 200) exit(`Failed to get exec session: ${requestError(response)}`);
|
|
1440
|
+
return response.body;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function runExecSession(appId, execId, { stdin, stdout, stderr, tty }, options) {
|
|
1444
|
+
return new Promise((resolve, reject) => {
|
|
1445
|
+
const { adminFqdn, token: reqToken, rejectUnauthorized } = requestOptions(options);
|
|
1446
|
+
|
|
1447
|
+
const searchParams = new URLSearchParams({
|
|
1448
|
+
rows: stdout.rows || 24,
|
|
1449
|
+
columns: stdout.columns || 80,
|
|
1450
|
+
access_token: reqToken,
|
|
1451
|
+
tty
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
const req = https.request({
|
|
1455
|
+
hostname: adminFqdn,
|
|
1456
|
+
path: `/api/v1/apps/${appId}/exec/${execId}/start?${searchParams.toString()}`,
|
|
1457
|
+
method: 'GET',
|
|
1458
|
+
headers: {
|
|
1459
|
+
'Connection': 'Upgrade',
|
|
1460
|
+
'Upgrade': 'tcp'
|
|
1461
|
+
},
|
|
1462
|
+
rejectUnauthorized
|
|
1463
|
+
}, function handler(res) {
|
|
1464
|
+
if (res.statusCode === 403) return reject(new Error('Unauthorized.'));
|
|
1465
|
+
reject(new Error(`Could not upgrade connection to tcp. http status: ${res.statusCode}`));
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
req.on('upgrade', function (resThatShouldNotBeUsed, socket) {
|
|
1469
|
+
socket.on('error', reject);
|
|
1470
|
+
|
|
1471
|
+
socket.setNoDelay(true);
|
|
1472
|
+
socket.setKeepAlive(true);
|
|
1473
|
+
|
|
1474
|
+
if (tty) {
|
|
1475
|
+
stdin.setRawMode(true);
|
|
1476
|
+
stdin.pipe(socket, { end: false });
|
|
1477
|
+
socket.pipe(stdout);
|
|
1478
|
+
socket.on('end', () => resolve());
|
|
1479
|
+
} else {
|
|
1480
|
+
if (typeof stdin === 'function') stdin = stdin();
|
|
1481
|
+
let socketEnded = false;
|
|
1482
|
+
|
|
1483
|
+
stdin.on('data', function (d) {
|
|
1484
|
+
if (socketEnded) return;
|
|
1485
|
+
var buf = Buffer.alloc(4);
|
|
1486
|
+
buf.writeUInt32BE(d.length, 0);
|
|
1487
|
+
socket.write(buf);
|
|
1488
|
+
socket.write(d);
|
|
1489
|
+
});
|
|
1490
|
+
stdin.on('end', function () {
|
|
1491
|
+
if (socketEnded) return;
|
|
1492
|
+
var buf = Buffer.alloc(4);
|
|
1493
|
+
buf.writeUInt32BE(0, 0);
|
|
1494
|
+
socket.write(buf);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
demuxStream(socket, stdout, stderr);
|
|
1498
|
+
socket.on('end', function () {
|
|
1499
|
+
socketEnded = true;
|
|
1500
|
+
if (typeof stdin.end === 'function') stdin.end();
|
|
1501
|
+
socket.end();
|
|
1502
|
+
|
|
1503
|
+
if (stdout !== process.stdout && typeof stdout.end === 'function') {
|
|
1504
|
+
stdout.on('close', () => resolve());
|
|
1505
|
+
stdout.end();
|
|
1506
|
+
stdout.resume(); // drain readable side of Duplex/Transform streams so autoDestroy emits 'close'
|
|
1507
|
+
} else {
|
|
1508
|
+
setImmediate(resolve);
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
req.on('error', reject);
|
|
1515
|
+
req.end();
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
async function runExecCommand(appId, args, options) {
|
|
1520
|
+
const stdoutChunks = [];
|
|
1521
|
+
const stderrChunks = [];
|
|
1522
|
+
|
|
1523
|
+
const stdoutStream = new PassThrough();
|
|
1524
|
+
const stderrStream = new PassThrough();
|
|
1525
|
+
|
|
1526
|
+
stdoutStream.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
1527
|
+
stderrStream.on('data', (chunk) => stderrChunks.push(chunk));
|
|
1528
|
+
|
|
1529
|
+
const execId = await createExecSession(appId, args, { tty: false }, options);
|
|
1530
|
+
|
|
1531
|
+
await runExecSession(appId, execId, {
|
|
1532
|
+
stdin: () => new PassThrough(),
|
|
1533
|
+
stdout: stdoutStream,
|
|
1534
|
+
stderr: stderrStream,
|
|
1535
|
+
tty: false
|
|
1536
|
+
}, options);
|
|
1537
|
+
|
|
1538
|
+
const session = await getExecSession(appId, execId, options);
|
|
1539
|
+
|
|
1540
|
+
return {
|
|
1541
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
1542
|
+
stderr: Buffer.concat(stderrChunks),
|
|
1543
|
+
exitCode: session.exitCode
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function getLocalFileList(localPath) {
|
|
1548
|
+
const resolved = path.resolve(localPath);
|
|
1549
|
+
const files = new Map();
|
|
1550
|
+
|
|
1551
|
+
if (fs.lstatSync(resolved).isFile()) {
|
|
1552
|
+
const stat = fs.statSync(resolved);
|
|
1553
|
+
files.set(path.basename(resolved), { size: stat.size, mtimeSec: Math.floor(stat.mtimeMs / 1000) });
|
|
1554
|
+
return { baseDir: path.dirname(resolved), files };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true, recursive: true });
|
|
1558
|
+
for (const entry of entries) {
|
|
1559
|
+
if (!entry.isFile()) continue;
|
|
1560
|
+
const relativePath = path.relative(resolved, path.join(entry.parentPath || entry.path, entry.name));
|
|
1561
|
+
const stat = fs.statSync(path.join(resolved, relativePath));
|
|
1562
|
+
files.set(relativePath, { size: stat.size, mtimeSec: Math.floor(stat.mtimeMs / 1000) });
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return { baseDir: resolved, files };
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
async function getRemoteFileList(appId, dir, options) {
|
|
1569
|
+
const files = new Map();
|
|
1570
|
+
|
|
1571
|
+
let result;
|
|
1572
|
+
try {
|
|
1573
|
+
result = await runExecCommand(appId, ['find', dir, '-type', 'f', '-printf', '%s\\t%T@\\t%P\\n'], options);
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
console.error(`Warning: could not list remote files (${e.message}), syncing all files`);
|
|
1576
|
+
return files;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (result.exitCode !== 0) return files;
|
|
1580
|
+
|
|
1581
|
+
const output = result.stdout.toString().trim();
|
|
1582
|
+
if (!output) return files;
|
|
1583
|
+
|
|
1584
|
+
for (const line of output.split('\n')) {
|
|
1585
|
+
const [size, mtime, ...pathParts] = line.split('\t');
|
|
1586
|
+
const relativePath = pathParts.join('\t');
|
|
1587
|
+
files.set(relativePath, { size: parseInt(size, 10), mtimeSec: Math.floor(parseFloat(mtime)) });
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return files;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function diffFileLists(srcList, destList) {
|
|
1594
|
+
const changed = [];
|
|
1595
|
+
const extraneous = [];
|
|
1596
|
+
|
|
1597
|
+
for (const [filePath, src] of srcList) {
|
|
1598
|
+
const dest = destList.get(filePath);
|
|
1599
|
+
if (!dest || dest.size !== src.size || src.mtimeSec > dest.mtimeSec) {
|
|
1600
|
+
changed.push(filePath);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
for (const filePath of destList.keys()) {
|
|
1605
|
+
if (!srcList.has(filePath)) extraneous.push(filePath);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
return { changed, extraneous };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1429
1611
|
// cloudron exec - must work interactively. needs tty.
|
|
1430
1612
|
// cloudron exec -- ls asdf - must work
|
|
1431
1613
|
// cloudron exec -- cat /home/cloudron/start.sh > /tmp/start.sh - must work (test with binary files). should disable tty
|
|
@@ -1433,12 +1615,9 @@ function demuxStream(inputStream, stdout, stderr) {
|
|
|
1433
1615
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar xvf - -C /tmp" - must show an error
|
|
1434
1616
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar zxf - -C /tmp" - must extrack ok
|
|
1435
1617
|
async function execApp(args, localOptions, cmd) {
|
|
1436
|
-
let stdin = localOptions._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
|
|
1437
|
-
const stdout = localOptions._stdout || process.stdout; // hack for 'push', 'pull' to reuse this function
|
|
1438
|
-
|
|
1439
1618
|
const options = cmd.optsWithGlobals();
|
|
1440
1619
|
let tty = !!options.tty;
|
|
1441
|
-
let lang;
|
|
1620
|
+
let lang;
|
|
1442
1621
|
|
|
1443
1622
|
const [error, app] = await safe(getApp(options));
|
|
1444
1623
|
if (error) return exit(error);
|
|
@@ -1447,150 +1626,288 @@ async function execApp(args, localOptions, cmd) {
|
|
|
1447
1626
|
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1448
1627
|
|
|
1449
1628
|
if (args.length === 0) {
|
|
1450
|
-
args = [
|
|
1451
|
-
tty = true;
|
|
1629
|
+
args = ['/bin/bash'];
|
|
1630
|
+
tty = true;
|
|
1452
1631
|
lang = 'C.UTF-8';
|
|
1453
1632
|
}
|
|
1454
1633
|
|
|
1455
|
-
if (tty && !stdin.isTTY) exit('stdin is not tty');
|
|
1634
|
+
if (tty && !process.stdin.isTTY) exit('stdin is not tty');
|
|
1456
1635
|
|
|
1457
|
-
const
|
|
1458
|
-
const response = await request.send({ cmd: args, tty, lang });
|
|
1459
|
-
if (response.status !== 200) return exit(`Failed to create exec: ${requestError(response)}`);
|
|
1460
|
-
const execId = response.body.id;
|
|
1461
|
-
|
|
1462
|
-
async function exitWithCode() {
|
|
1463
|
-
const response2 = await createRequest('GET', `/api/v1/apps/${app.id}/exec/${execId}`, options);
|
|
1464
|
-
if (response2.status !== 200) return exit(`Failed to get exec code: ${requestError(response2)}`);
|
|
1465
|
-
|
|
1466
|
-
process.exit(response2.body.exitCode);
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
const { adminFqdn, token: reqToken, rejectUnauthorized } = requestOptions(cmd.optsWithGlobals());
|
|
1636
|
+
const execId = await createExecSession(app.id, args, { tty, lang }, options);
|
|
1470
1637
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1638
|
+
await runExecSession(app.id, execId, {
|
|
1639
|
+
stdin: process.stdin,
|
|
1640
|
+
stdout: process.stdout,
|
|
1641
|
+
stderr: process.stderr,
|
|
1475
1642
|
tty
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
const req = https.request({
|
|
1479
|
-
hostname: adminFqdn,
|
|
1480
|
-
path: `/api/v1/apps/${app.id}/exec/${execId}/start?${searchParams.toString()}`,
|
|
1481
|
-
method: 'GET',
|
|
1482
|
-
headers: {
|
|
1483
|
-
'Connection': 'Upgrade',
|
|
1484
|
-
'Upgrade': 'tcp'
|
|
1485
|
-
},
|
|
1486
|
-
rejectUnauthorized
|
|
1487
|
-
}, function handler(res) {
|
|
1488
|
-
if (res.statusCode === 403) exit('Unauthorized.'); // only admin or only owner (for docker addon)
|
|
1489
|
-
|
|
1490
|
-
exit('Could not upgrade connection to tcp. http status:', res.statusCode);
|
|
1491
|
-
});
|
|
1643
|
+
}, options);
|
|
1492
1644
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
socket.setNoDelay(true);
|
|
1498
|
-
socket.setKeepAlive(true);
|
|
1499
|
-
|
|
1500
|
-
if (tty) {
|
|
1501
|
-
stdin.setRawMode(true);
|
|
1502
|
-
stdin.pipe(socket, { end: false }); // the remote will close the connection
|
|
1503
|
-
socket.pipe(stdout); // in tty mode, stdout/stderr is merged
|
|
1504
|
-
socket.on('end', exitWithCode); // server closed the socket
|
|
1505
|
-
} else { // create stdin process on demand
|
|
1506
|
-
if (typeof stdin === 'function') stdin = stdin();
|
|
1507
|
-
|
|
1508
|
-
stdin.on('data', function (d) {
|
|
1509
|
-
var buf = Buffer.alloc(4);
|
|
1510
|
-
buf.writeUInt32BE(d.length, 0 /* offset */);
|
|
1511
|
-
socket.write(buf);
|
|
1512
|
-
socket.write(d);
|
|
1513
|
-
});
|
|
1514
|
-
stdin.on('end', function () {
|
|
1515
|
-
var buf = Buffer.alloc(4);
|
|
1516
|
-
buf.writeUInt32BE(0, 0 /* offset */);
|
|
1517
|
-
socket.write(buf);
|
|
1518
|
-
});
|
|
1519
|
-
|
|
1520
|
-
stdout.on('close', exitWithCode); // this is only emitted when stdout is a file and not the terminal
|
|
1521
|
-
|
|
1522
|
-
demuxStream(socket, stdout, process.stderr); // can get separate streams in non-tty mode
|
|
1523
|
-
socket.on('end', function () { // server closed the socket
|
|
1524
|
-
if (typeof stdin.end === 'function') stdin.end(); // required for this process to 'exit' cleanly. do not call exit() because writes may not have finished . the type check is required for when stdin: 'ignore' in execSync, not sure why
|
|
1525
|
-
if (stdout !== process.stdout) stdout.end(); // for push stream
|
|
1645
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1646
|
+
process.exit(execSession.exitCode);
|
|
1647
|
+
}
|
|
1526
1648
|
|
|
1527
|
-
|
|
1649
|
+
async function pushApp(localDir, remote, localOptions, cmd) {
|
|
1650
|
+
const options = cmd.optsWithGlobals();
|
|
1528
1651
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
}
|
|
1533
|
-
});
|
|
1652
|
+
const [error, app] = await safe(getApp(options));
|
|
1653
|
+
if (error) return exit(error);
|
|
1654
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1534
1655
|
|
|
1535
|
-
|
|
1536
|
-
req.end(); // this makes the request
|
|
1537
|
-
}
|
|
1656
|
+
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1538
1657
|
|
|
1539
|
-
function pushApp(localDir, remote, localOptions, cmd) {
|
|
1540
|
-
// deal with paths prefixed with ~
|
|
1541
1658
|
const local = localDir.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
1542
1659
|
const stat = fs.existsSync(path.resolve(local)) ? fs.lstatSync(local) : null;
|
|
1543
1660
|
|
|
1544
|
-
|
|
1661
|
+
let args, stdin;
|
|
1662
|
+
|
|
1663
|
+
if (stat && stat.isDirectory()) {
|
|
1545
1664
|
// Create a functor for stdin. If no data event handlers are attached, and there are no stream.pipe() destinations, and the stream is
|
|
1546
1665
|
// switched into flowing mode, then data will be lost. So, we have to start the tarzip only when exec is ready to attach event handlers.
|
|
1547
|
-
|
|
1666
|
+
stdin = function () {
|
|
1548
1667
|
var tarzip = spawn('tar', ['zcf', '-', '-C', path.dirname(local), path.basename(local)], { stdio: 'pipe' });
|
|
1549
1668
|
return tarzip.stdout;
|
|
1550
1669
|
};
|
|
1551
|
-
|
|
1552
|
-
execApp(['tar', 'zxvf', '-', '-C', remote], localOptions, cmd);
|
|
1670
|
+
args = ['tar', 'zxvf', '-', '-C', remote];
|
|
1553
1671
|
} else {
|
|
1554
1672
|
if (local === '-') {
|
|
1555
|
-
|
|
1673
|
+
stdin = process.stdin;
|
|
1556
1674
|
} else if (stat) {
|
|
1557
|
-
|
|
1675
|
+
stdin = fs.createReadStream(local);
|
|
1558
1676
|
} else {
|
|
1559
1677
|
exit('local file ' + local + ' does not exist');
|
|
1560
1678
|
}
|
|
1561
1679
|
|
|
1562
|
-
|
|
1680
|
+
stdin.on('error', function (e) { exit('Error pushing', e); });
|
|
1563
1681
|
|
|
1564
|
-
if (remote.endsWith('/')) {
|
|
1565
|
-
remote = remote + '/' + path.basename(local);
|
|
1682
|
+
if (remote.endsWith('/')) {
|
|
1683
|
+
remote = remote + '/' + path.basename(local);
|
|
1566
1684
|
}
|
|
1567
1685
|
|
|
1568
|
-
|
|
1686
|
+
args = ['bash', '-c', `cat - > "${remote}"`];
|
|
1569
1687
|
}
|
|
1688
|
+
|
|
1689
|
+
const execId = await createExecSession(app.id, args, { tty: false }, options);
|
|
1690
|
+
|
|
1691
|
+
await runExecSession(app.id, execId, {
|
|
1692
|
+
stdin,
|
|
1693
|
+
stdout: process.stdout,
|
|
1694
|
+
stderr: process.stderr,
|
|
1695
|
+
tty: false
|
|
1696
|
+
}, options);
|
|
1697
|
+
|
|
1698
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1699
|
+
process.exit(execSession.exitCode);
|
|
1570
1700
|
}
|
|
1571
1701
|
|
|
1572
|
-
function pull(remote, local, localOptions, cmd) {
|
|
1573
|
-
|
|
1574
|
-
|
|
1702
|
+
async function pull(remote, local, localOptions, cmd) {
|
|
1703
|
+
const options = cmd.optsWithGlobals();
|
|
1704
|
+
|
|
1705
|
+
const [error, app] = await safe(getApp(options));
|
|
1706
|
+
if (error) return exit(error);
|
|
1707
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1708
|
+
|
|
1709
|
+
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1710
|
+
|
|
1711
|
+
let args, stdout;
|
|
1712
|
+
|
|
1713
|
+
if (remote.endsWith('/')) {
|
|
1714
|
+
var untar = tar.extract(local);
|
|
1575
1715
|
var unzip = zlib.createGunzip();
|
|
1576
1716
|
|
|
1577
1717
|
unzip.pipe(untar);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
execApp(['tar', 'zcf', '-', '-C', remote, '.'], localOptions, cmd);
|
|
1718
|
+
stdout = unzip;
|
|
1719
|
+
args = ['tar', 'zcf', '-', '-C', remote, '.'];
|
|
1581
1720
|
} else {
|
|
1582
1721
|
if (fs.existsSync(local) && fs.lstatSync(local).isDirectory()) {
|
|
1583
1722
|
local = path.join(local, path.basename(remote));
|
|
1584
|
-
|
|
1723
|
+
stdout = fs.createWriteStream(local);
|
|
1585
1724
|
} else if (local === '-') {
|
|
1586
|
-
|
|
1725
|
+
stdout = process.stdout;
|
|
1587
1726
|
} else {
|
|
1588
|
-
|
|
1727
|
+
stdout = fs.createWriteStream(local);
|
|
1589
1728
|
}
|
|
1590
1729
|
|
|
1591
|
-
|
|
1730
|
+
stdout.on('error', function (e) { exit('Error pulling', e); });
|
|
1592
1731
|
|
|
1593
|
-
|
|
1732
|
+
args = ['cat', remote];
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const execId = await createExecSession(app.id, args, { tty: false }, options);
|
|
1736
|
+
|
|
1737
|
+
await runExecSession(app.id, execId, {
|
|
1738
|
+
stdin: process.stdin,
|
|
1739
|
+
stdout,
|
|
1740
|
+
stderr: process.stderr,
|
|
1741
|
+
tty: false
|
|
1742
|
+
}, options);
|
|
1743
|
+
|
|
1744
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1745
|
+
process.exit(execSession.exitCode);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async function syncPush(localDir, remoteDir, localOptions, cmd) {
|
|
1749
|
+
try {
|
|
1750
|
+
const options = cmd.optsWithGlobals();
|
|
1751
|
+
|
|
1752
|
+
const [error, app] = await safe(getApp(options));
|
|
1753
|
+
if (error) return exit(error);
|
|
1754
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1755
|
+
|
|
1756
|
+
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1757
|
+
|
|
1758
|
+
const resolvedLocal = path.resolve(localDir);
|
|
1759
|
+
if (!fs.existsSync(resolvedLocal)) exit('local path must be an existing file or directory');
|
|
1760
|
+
|
|
1761
|
+
const localStat = fs.lstatSync(resolvedLocal);
|
|
1762
|
+
if (!localStat.isFile() && !localStat.isDirectory()) exit('local path must be an existing file or directory');
|
|
1763
|
+
|
|
1764
|
+
const { baseDir: tarBaseDir, files: localList } = getLocalFileList(resolvedLocal);
|
|
1765
|
+
|
|
1766
|
+
// rsync convention: no trailing slash on source dir appends the directory name to dest
|
|
1767
|
+
if (localStat.isDirectory() && !localDir.endsWith('/') && !localDir.endsWith('/.')) {
|
|
1768
|
+
remoteDir = remoteDir.replace(/\/+$/, '') + '/' + path.basename(resolvedLocal);
|
|
1769
|
+
} else if (localStat.isFile()) {
|
|
1770
|
+
remoteDir = remoteDir.replace(/\/+$/, '');
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const remoteList = await getRemoteFileList(app.id, remoteDir, options);
|
|
1774
|
+
const { changed: filesToSync, extraneous: filesToDelete } = diffFileLists(localList, remoteList);
|
|
1775
|
+
const doDelete = options.delete && filesToDelete.length > 0;
|
|
1776
|
+
|
|
1777
|
+
if (filesToSync.length === 0 && !doDelete) {
|
|
1778
|
+
console.log('Already up to date.');
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
for (const f of filesToSync) console.log(f);
|
|
1783
|
+
if (doDelete) for (const f of filesToDelete) console.log(`delete: ${f}`);
|
|
1784
|
+
console.log(`Syncing ${filesToSync.length} file(s)${doDelete ? `, deleting ${filesToDelete.length} file(s)` : ''}...`);
|
|
1785
|
+
|
|
1786
|
+
if (filesToSync.length > 0) {
|
|
1787
|
+
const localFileList = path.join(os.tmpdir(), `.cloudron-sync-${process.pid}`);
|
|
1788
|
+
fs.writeFileSync(localFileList, filesToSync.join('\n') + '\n');
|
|
1789
|
+
|
|
1790
|
+
const stdin = function () {
|
|
1791
|
+
const tarzip = spawn('tar', ['zcf', '-', '-C', tarBaseDir, '-T', localFileList], { stdio: 'pipe' });
|
|
1792
|
+
tarzip.on('close', () => fs.unlinkSync(localFileList));
|
|
1793
|
+
return tarzip.stdout;
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
const mkdirCmd = options.force
|
|
1797
|
+
? `test -f ${remoteDir} && rm -f ${remoteDir}; mkdir -p ${remoteDir}`
|
|
1798
|
+
: `mkdir -p ${remoteDir}`;
|
|
1799
|
+
const execId = await createExecSession(app.id, ['bash', '-c', `${mkdirCmd} && tar zxf - -C ${remoteDir}`], { tty: false }, options);
|
|
1800
|
+
|
|
1801
|
+
await runExecSession(app.id, execId, {
|
|
1802
|
+
stdin,
|
|
1803
|
+
stdout: process.stdout,
|
|
1804
|
+
stderr: process.stderr,
|
|
1805
|
+
tty: false
|
|
1806
|
+
}, options);
|
|
1807
|
+
|
|
1808
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1809
|
+
if (execSession.exitCode !== 0) exit(`Sync failed with exit code ${execSession.exitCode}`);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (doDelete) {
|
|
1813
|
+
const CHUNK_SIZE = 500;
|
|
1814
|
+
for (let i = 0; i < filesToDelete.length; i += CHUNK_SIZE) {
|
|
1815
|
+
const chunk = filesToDelete.slice(i, i + CHUNK_SIZE).map(f => `${remoteDir}/${f}`);
|
|
1816
|
+
const result = await runExecCommand(app.id, ['rm', '-f', ...chunk], options);
|
|
1817
|
+
if (result.exitCode !== 0) exit(`Failed to delete remote files: ${result.stderr.toString().trim()}`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
console.log('Sync complete.');
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
exit(`Sync push failed: ${e.message}`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
async function syncPull(remoteDir, localDir, localOptions, cmd) {
|
|
1828
|
+
try {
|
|
1829
|
+
const options = cmd.optsWithGlobals();
|
|
1830
|
+
|
|
1831
|
+
const [error, app] = await safe(getApp(options));
|
|
1832
|
+
if (error) return exit(error);
|
|
1833
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1834
|
+
|
|
1835
|
+
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1836
|
+
|
|
1837
|
+
// rsync convention: no trailing slash on source appends the directory name to dest
|
|
1838
|
+
if (!remoteDir.endsWith('/')) {
|
|
1839
|
+
localDir = path.join(localDir, path.basename(remoteDir));
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const resolvedLocalDir = path.resolve(localDir);
|
|
1843
|
+
if (fs.existsSync(resolvedLocalDir) && !fs.lstatSync(resolvedLocalDir).isDirectory()) {
|
|
1844
|
+
if (options.force) fs.unlinkSync(resolvedLocalDir);
|
|
1845
|
+
else exit(`local path '${localDir}' exists and is not a directory (use --force to remove)`);
|
|
1846
|
+
}
|
|
1847
|
+
if (!fs.existsSync(resolvedLocalDir)) fs.mkdirSync(resolvedLocalDir, { recursive: true });
|
|
1848
|
+
|
|
1849
|
+
const remoteList = await getRemoteFileList(app.id, remoteDir, options);
|
|
1850
|
+
const { files: localList } = getLocalFileList(path.resolve(localDir));
|
|
1851
|
+
const { changed: filesToSync, extraneous: filesToDelete } = diffFileLists(remoteList, localList);
|
|
1852
|
+
const doDelete = options.delete && filesToDelete.length > 0;
|
|
1853
|
+
|
|
1854
|
+
if (filesToSync.length === 0 && !doDelete) {
|
|
1855
|
+
console.log('Already up to date.');
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
for (const f of filesToSync) console.log(f);
|
|
1860
|
+
if (doDelete) for (const f of filesToDelete) console.log(`delete: ${f}`);
|
|
1861
|
+
console.log(`Syncing ${filesToSync.length} file(s)${doDelete ? `, deleting ${filesToDelete.length} file(s)` : ''}...`);
|
|
1862
|
+
|
|
1863
|
+
if (filesToSync.length > 0) {
|
|
1864
|
+
const remoteFileList = '/tmp/.cloudron-sync-filelist';
|
|
1865
|
+
const fileListData = filesToSync.join('\n') + '\n';
|
|
1866
|
+
|
|
1867
|
+
const writeExecId = await createExecSession(app.id, ['bash', '-c', `cat > ${remoteFileList}`], { tty: false }, options);
|
|
1868
|
+
await runExecSession(app.id, writeExecId, {
|
|
1869
|
+
stdin: () => {
|
|
1870
|
+
const s = new PassThrough();
|
|
1871
|
+
s.end(fileListData);
|
|
1872
|
+
return s;
|
|
1873
|
+
},
|
|
1874
|
+
stdout: new PassThrough(),
|
|
1875
|
+
stderr: process.stderr,
|
|
1876
|
+
tty: false
|
|
1877
|
+
}, options);
|
|
1878
|
+
const writeSession = await getExecSession(app.id, writeExecId, options);
|
|
1879
|
+
if (writeSession.exitCode !== 0) exit('Failed to write remote file list');
|
|
1880
|
+
|
|
1881
|
+
const untar = tar.extract(path.resolve(localDir));
|
|
1882
|
+
const unzip = zlib.createGunzip();
|
|
1883
|
+
unzip.pipe(untar);
|
|
1884
|
+
|
|
1885
|
+
const execId = await createExecSession(app.id, ['bash', '-c', `tar zcf - -C ${remoteDir} -T ${remoteFileList}; rm -f ${remoteFileList}`], { tty: false }, options);
|
|
1886
|
+
|
|
1887
|
+
await runExecSession(app.id, execId, {
|
|
1888
|
+
stdin: () => {
|
|
1889
|
+
const s = new PassThrough();
|
|
1890
|
+
s.end();
|
|
1891
|
+
return s;
|
|
1892
|
+
},
|
|
1893
|
+
stdout: unzip,
|
|
1894
|
+
stderr: process.stderr,
|
|
1895
|
+
tty: false
|
|
1896
|
+
}, options);
|
|
1897
|
+
|
|
1898
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1899
|
+
if (execSession.exitCode !== 0) exit(`Sync failed with exit code ${execSession.exitCode}`);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (doDelete) {
|
|
1903
|
+
for (const f of filesToDelete) {
|
|
1904
|
+
fs.unlinkSync(path.join(path.resolve(localDir), f));
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
console.log('Sync complete.');
|
|
1909
|
+
} catch (e) {
|
|
1910
|
+
exit(`Sync pull failed: ${e.message}`);
|
|
1594
1911
|
}
|
|
1595
1912
|
}
|
|
1596
1913
|
|
|
@@ -1779,5 +2096,8 @@ export default {
|
|
|
1779
2096
|
envGet,
|
|
1780
2097
|
envSet,
|
|
1781
2098
|
envList,
|
|
1782
|
-
envUnset
|
|
2099
|
+
envUnset,
|
|
2100
|
+
|
|
2101
|
+
syncPush,
|
|
2102
|
+
syncPull
|
|
1783
2103
|
};
|
package/src/helper.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import http from 'http';
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import readline from 'readline';
|
|
6
7
|
import safe from '@cloudron/safetydance';
|
|
7
8
|
import superagent from '@cloudron/superagent';
|
|
8
9
|
import util from 'util';
|
|
@@ -128,15 +129,22 @@ async function performOidcLogin(adminFqdn, { rejectUnauthorized = true } = {}) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const receivedCode = url.searchParams.get('code');
|
|
132
|
+
const successHtml = fs.readFileSync(path.join(import.meta.dirname, 'login-success.html'), 'utf8');
|
|
131
133
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
132
|
-
res.end(
|
|
134
|
+
res.end(successHtml);
|
|
133
135
|
server.close();
|
|
134
136
|
resolve(receivedCode);
|
|
135
137
|
});
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// without the host ip, it will listen on :: . on mac, which has dual stack disabled, it will listen on ipv6 only
|
|
140
|
+
server.listen(1312, '127.0.0.1', () => {
|
|
141
|
+
// console.log('Login at:');
|
|
142
|
+
// console.log(authUrl.toString());
|
|
143
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
144
|
+
rl.question('Press ENTER to authenticate using the browser...', () => {
|
|
145
|
+
rl.close();
|
|
146
|
+
open(authUrl.toString());
|
|
147
|
+
});
|
|
140
148
|
});
|
|
141
149
|
|
|
142
150
|
server.on('error', (err) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>Login Successful</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
background: #f5f7fa;
|
|
15
|
+
color: #333;
|
|
16
|
+
}
|
|
17
|
+
.card {
|
|
18
|
+
text-align: center;
|
|
19
|
+
background: #fff;
|
|
20
|
+
border-radius: 12px;
|
|
21
|
+
padding: 48px 40px;
|
|
22
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
23
|
+
max-width: 420px;
|
|
24
|
+
}
|
|
25
|
+
.icon {
|
|
26
|
+
width: 64px; height: 64px;
|
|
27
|
+
margin: 0 auto 24px;
|
|
28
|
+
background: #e8f5e9;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
.icon svg { width: 32px; height: 32px; color: #43a047; }
|
|
35
|
+
h1 { font-size: 22px; font-weight: 600; margin-bottom: 8px; }
|
|
36
|
+
p { font-size: 15px; color: #666; line-height: 1.5; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div class="card">
|
|
41
|
+
<div class="icon">
|
|
42
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
43
|
+
<polyline points="20 6 9 17 4 12"/>
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
<h1>Authentication Successful</h1>
|
|
47
|
+
<p>You can close this tab and return to your command line.</p>
|
|
48
|
+
</div>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
package/bin/cloudron-appstore
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import appstoreActions from '../src/appstore-actions.js';
|
|
4
|
-
import { Command } from 'commander';
|
|
5
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
program.version(pkg.packageVersion);
|
|
9
|
-
|
|
10
|
-
// global options. IMPORTANT: These cannot conflict with global options!
|
|
11
|
-
program
|
|
12
|
-
.option('--appstore-token <token>', 'AppStore token');
|
|
13
|
-
|
|
14
|
-
program.command('login')
|
|
15
|
-
.description('Login to the appstore')
|
|
16
|
-
.option('-e, --email <email>', 'Email address')
|
|
17
|
-
.option('-p, --password <password>', 'Password (unsafe)')
|
|
18
|
-
.action(appstoreActions.login);
|
|
19
|
-
|
|
20
|
-
program.command('logout')
|
|
21
|
-
.description('Logout from the appstore')
|
|
22
|
-
.action(appstoreActions.logout);
|
|
23
|
-
|
|
24
|
-
program.command('info')
|
|
25
|
-
.description('List info of published app')
|
|
26
|
-
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
27
|
-
.action(appstoreActions.info);
|
|
28
|
-
|
|
29
|
-
program.command('approve')
|
|
30
|
-
.description('Approve a submitted app version')
|
|
31
|
-
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
32
|
-
.option('--no-git-push', 'Do not attempt to push to git repo')
|
|
33
|
-
.action(appstoreActions.approve);
|
|
34
|
-
|
|
35
|
-
program.command('notify')
|
|
36
|
-
.description('Notify forum about successful app submission')
|
|
37
|
-
.action(appstoreActions.notify);
|
|
38
|
-
|
|
39
|
-
program.command('revoke')
|
|
40
|
-
.description('Revoke a published app version')
|
|
41
|
-
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
42
|
-
.action(appstoreActions.revoke);
|
|
43
|
-
|
|
44
|
-
program.command('submit')
|
|
45
|
-
.description('Submit app to the store for review')
|
|
46
|
-
.action(appstoreActions.submit);
|
|
47
|
-
|
|
48
|
-
program.command('upload')
|
|
49
|
-
.description('Upload app to the store for testing')
|
|
50
|
-
.option('-i, --image <image>', 'Docker image')
|
|
51
|
-
.option('-f, --force', 'Update existing version')
|
|
52
|
-
.action(appstoreActions.upload);
|
|
53
|
-
|
|
54
|
-
program.command('verify-manifest')
|
|
55
|
-
.description('Verify if manifest is ready to be upload/published')
|
|
56
|
-
.action(appstoreActions.verifyManifest);
|
|
57
|
-
|
|
58
|
-
program.command('versions')
|
|
59
|
-
.alias('list')
|
|
60
|
-
.description('List published versions')
|
|
61
|
-
.option('--appstore-id <id>', 'Appstore id')
|
|
62
|
-
.option('--raw', 'Dump versions as json')
|
|
63
|
-
.action(appstoreActions.listVersions);
|
|
64
|
-
|
|
65
|
-
program.parse(process.argv);
|
package/bin/cloudron-build
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { program } from 'commander';
|
|
4
|
-
import buildActions from '../src/build-actions.js';
|
|
5
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
6
|
-
|
|
7
|
-
program.version(pkg.version);
|
|
8
|
-
|
|
9
|
-
program.addHelpText('after', `
|
|
10
|
-
|
|
11
|
-
Show help for default subcommand:
|
|
12
|
-
$ cloudron build build --help`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function collectArgs(value, collected) {
|
|
16
|
-
collected.push(value);
|
|
17
|
-
return collected;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// global options. IMPORTANT: These cannot conflict with global options!
|
|
21
|
-
program.option('--server <server>', 'Cloudron domain')
|
|
22
|
-
.option('--build-service-url <url>', 'Build service URL')
|
|
23
|
-
.option('--build-service-token <token>', 'Build service token');
|
|
24
|
-
|
|
25
|
-
program.command('build', { isDefault: true })
|
|
26
|
-
.description('Build an app. This is the default subcommand')
|
|
27
|
-
.option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectArgs, [])
|
|
28
|
-
.option('-f, --file <dockerfile>', 'Name of the Dockerfile')
|
|
29
|
-
.option('--repository [repository url]', 'Change the repository. This url is stored for future builds for this project. e.g registry/username/projectname')
|
|
30
|
-
.option('--no-cache', 'Do not use cache')
|
|
31
|
-
.option('--no-push', 'Do not push built image to registry')
|
|
32
|
-
.option('--raw', 'Raw output build log')
|
|
33
|
-
.option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
|
|
34
|
-
.action(buildActions.build);
|
|
35
|
-
|
|
36
|
-
program.command('reset')
|
|
37
|
-
.description('Reset build configuration for this directory')
|
|
38
|
-
.action(buildActions.reset);
|
|
39
|
-
|
|
40
|
-
program.command('info')
|
|
41
|
-
.description('Print build information')
|
|
42
|
-
.action(buildActions.info);
|
|
43
|
-
|
|
44
|
-
program.command('login')
|
|
45
|
-
.description('Login to the build service')
|
|
46
|
-
.action(buildActions.login);
|
|
47
|
-
|
|
48
|
-
program.command('logout')
|
|
49
|
-
.description('Logout from the build service')
|
|
50
|
-
.action(buildActions.logout);
|
|
51
|
-
|
|
52
|
-
program.command('logs')
|
|
53
|
-
.description('Build logs. This works only when using the Build Service')
|
|
54
|
-
.option('--id <buildid>', 'Build ID')
|
|
55
|
-
.option('--raw', 'Raw output build log')
|
|
56
|
-
.action(buildActions.logs);
|
|
57
|
-
|
|
58
|
-
program.command('push')
|
|
59
|
-
.description('Push the build image')
|
|
60
|
-
.option('--id <buildid>', 'Build ID')
|
|
61
|
-
.option('--repository [repository url]', 'Set repository to push to. e.g registry/username/projectname')
|
|
62
|
-
.option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
|
|
63
|
-
.option('--image <docker image>', 'Docker image of the form registry/repo:tag')
|
|
64
|
-
.action(buildActions.push);
|
|
65
|
-
|
|
66
|
-
program.command('status')
|
|
67
|
-
.description('Build status. This works only when using the Build Service')
|
|
68
|
-
.option('--id <buildid>', 'Build ID')
|
|
69
|
-
.action(buildActions.status);
|
|
70
|
-
|
|
71
|
-
program.parse(process.argv);
|
package/bin/cloudron-versions
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import versionsActions from '../src/versions-actions.js';
|
|
4
|
-
import { Command } from 'commander';
|
|
5
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
program.version(pkg.version);
|
|
9
|
-
|
|
10
|
-
program.command('add')
|
|
11
|
-
.description('Add the current build to version file')
|
|
12
|
-
.option('--state <state>', 'Publish state (published or testing)')
|
|
13
|
-
.action(versionsActions.addOrUpdate);
|
|
14
|
-
|
|
15
|
-
program.command('init')
|
|
16
|
-
.description('Create versions file')
|
|
17
|
-
.action(versionsActions.init);
|
|
18
|
-
|
|
19
|
-
program.command('list')
|
|
20
|
-
.description('List existing versions')
|
|
21
|
-
.action(versionsActions.list);
|
|
22
|
-
|
|
23
|
-
program.command('revoke')
|
|
24
|
-
.description('Revoke the latest version')
|
|
25
|
-
.action(versionsActions.revoke);
|
|
26
|
-
|
|
27
|
-
program.command('update')
|
|
28
|
-
.description('Update existing version')
|
|
29
|
-
.option('--version <version>', 'Version to update')
|
|
30
|
-
.option('--state <state>', 'Publish state (published or testing)')
|
|
31
|
-
.action(versionsActions.addOrUpdate);
|
|
32
|
-
|
|
33
|
-
program.parse(process.argv);
|