cloudron 7.0.5 → 7.1.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/CHANGELOG.md +45 -0
- package/bin/cloudron +154 -6
- package/package.json +6 -6
- package/src/actions.js +434 -110
- package/src/build-actions.js +0 -5
- 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.0
|
|
3
|
+
"version": "7.1.0",
|
|
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';
|
|
@@ -106,10 +107,14 @@ async function stopActiveTask(app, options) {
|
|
|
106
107
|
function saveCwdAppId(appId, manifestFilePath) {
|
|
107
108
|
if (!manifestFilePath) return;
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
try {
|
|
111
|
+
const sourceDir = path.dirname(manifestFilePath);
|
|
112
|
+
const cwdConfig = config.getCwdConfig(sourceDir);
|
|
113
|
+
cwdConfig.appId = appId;
|
|
114
|
+
config.setCwdConfig(sourceDir, cwdConfig);
|
|
115
|
+
} catch (e) { // will happen when run on EROFS (set HOME to avoid)
|
|
116
|
+
console.log(`Warning: Could not save app id to config: ${e.message} . Try setting $HOME`);
|
|
117
|
+
}
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// appId may be the appId or the location
|
|
@@ -1422,6 +1427,187 @@ function demuxStream(inputStream, stdout, stderr) {
|
|
|
1422
1427
|
});
|
|
1423
1428
|
}
|
|
1424
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
|
+
|
|
1425
1611
|
// cloudron exec - must work interactively. needs tty.
|
|
1426
1612
|
// cloudron exec -- ls asdf - must work
|
|
1427
1613
|
// cloudron exec -- cat /home/cloudron/start.sh > /tmp/start.sh - must work (test with binary files). should disable tty
|
|
@@ -1429,12 +1615,9 @@ function demuxStream(inputStream, stdout, stderr) {
|
|
|
1429
1615
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar xvf - -C /tmp" - must show an error
|
|
1430
1616
|
// cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar zxf - -C /tmp" - must extrack ok
|
|
1431
1617
|
async function execApp(args, localOptions, cmd) {
|
|
1432
|
-
let stdin = localOptions._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
|
|
1433
|
-
const stdout = localOptions._stdout || process.stdout; // hack for 'push', 'pull' to reuse this function
|
|
1434
|
-
|
|
1435
1618
|
const options = cmd.optsWithGlobals();
|
|
1436
1619
|
let tty = !!options.tty;
|
|
1437
|
-
let lang;
|
|
1620
|
+
let lang;
|
|
1438
1621
|
|
|
1439
1622
|
const [error, app] = await safe(getApp(options));
|
|
1440
1623
|
if (error) return exit(error);
|
|
@@ -1443,150 +1626,288 @@ async function execApp(args, localOptions, cmd) {
|
|
|
1443
1626
|
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1444
1627
|
|
|
1445
1628
|
if (args.length === 0) {
|
|
1446
|
-
args = [
|
|
1447
|
-
tty = true;
|
|
1629
|
+
args = ['/bin/bash'];
|
|
1630
|
+
tty = true;
|
|
1448
1631
|
lang = 'C.UTF-8';
|
|
1449
1632
|
}
|
|
1450
1633
|
|
|
1451
|
-
if (tty && !stdin.isTTY) exit('stdin is not tty');
|
|
1452
|
-
|
|
1453
|
-
const request = createRequest('POST', `/api/v1/apps/${app.id}/exec`, options);
|
|
1454
|
-
const response = await request.send({ cmd: args, tty, lang });
|
|
1455
|
-
if (response.status !== 200) return exit(`Failed to create exec: ${requestError(response)}`);
|
|
1456
|
-
const execId = response.body.id;
|
|
1457
|
-
|
|
1458
|
-
async function exitWithCode() {
|
|
1459
|
-
const response2 = await createRequest('GET', `/api/v1/apps/${app.id}/exec/${execId}`, options);
|
|
1460
|
-
if (response2.status !== 200) return exit(`Failed to get exec code: ${requestError(response2)}`);
|
|
1461
|
-
|
|
1462
|
-
process.exit(response2.body.exitCode);
|
|
1463
|
-
}
|
|
1634
|
+
if (tty && !process.stdin.isTTY) exit('stdin is not tty');
|
|
1464
1635
|
|
|
1465
|
-
const
|
|
1636
|
+
const execId = await createExecSession(app.id, args, { tty, lang }, options);
|
|
1466
1637
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1638
|
+
await runExecSession(app.id, execId, {
|
|
1639
|
+
stdin: process.stdin,
|
|
1640
|
+
stdout: process.stdout,
|
|
1641
|
+
stderr: process.stderr,
|
|
1471
1642
|
tty
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
const req = https.request({
|
|
1475
|
-
hostname: adminFqdn,
|
|
1476
|
-
path: `/api/v1/apps/${app.id}/exec/${execId}/start?${searchParams.toString()}`,
|
|
1477
|
-
method: 'GET',
|
|
1478
|
-
headers: {
|
|
1479
|
-
'Connection': 'Upgrade',
|
|
1480
|
-
'Upgrade': 'tcp'
|
|
1481
|
-
},
|
|
1482
|
-
rejectUnauthorized
|
|
1483
|
-
}, function handler(res) {
|
|
1484
|
-
if (res.statusCode === 403) exit('Unauthorized.'); // only admin or only owner (for docker addon)
|
|
1485
|
-
|
|
1486
|
-
exit('Could not upgrade connection to tcp. http status:', res.statusCode);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
req.on('upgrade', function (resThatShouldNotBeUsed, socket /*, upgradeHead*/) {
|
|
1490
|
-
// do not use res here! it's all socket from here on
|
|
1491
|
-
socket.on('error', exit);
|
|
1492
|
-
|
|
1493
|
-
socket.setNoDelay(true);
|
|
1494
|
-
socket.setKeepAlive(true);
|
|
1495
|
-
|
|
1496
|
-
if (tty) {
|
|
1497
|
-
stdin.setRawMode(true);
|
|
1498
|
-
stdin.pipe(socket, { end: false }); // the remote will close the connection
|
|
1499
|
-
socket.pipe(stdout); // in tty mode, stdout/stderr is merged
|
|
1500
|
-
socket.on('end', exitWithCode); // server closed the socket
|
|
1501
|
-
} else { // create stdin process on demand
|
|
1502
|
-
if (typeof stdin === 'function') stdin = stdin();
|
|
1503
|
-
|
|
1504
|
-
stdin.on('data', function (d) {
|
|
1505
|
-
var buf = Buffer.alloc(4);
|
|
1506
|
-
buf.writeUInt32BE(d.length, 0 /* offset */);
|
|
1507
|
-
socket.write(buf);
|
|
1508
|
-
socket.write(d);
|
|
1509
|
-
});
|
|
1510
|
-
stdin.on('end', function () {
|
|
1511
|
-
var buf = Buffer.alloc(4);
|
|
1512
|
-
buf.writeUInt32BE(0, 0 /* offset */);
|
|
1513
|
-
socket.write(buf);
|
|
1514
|
-
});
|
|
1515
|
-
|
|
1516
|
-
stdout.on('close', exitWithCode); // this is only emitted when stdout is a file and not the terminal
|
|
1643
|
+
}, options);
|
|
1517
1644
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (stdout !== process.stdout) stdout.end(); // for push stream
|
|
1645
|
+
const execSession = await getExecSession(app.id, execId, options);
|
|
1646
|
+
process.exit(execSession.exitCode);
|
|
1647
|
+
}
|
|
1522
1648
|
|
|
1523
|
-
|
|
1649
|
+
async function pushApp(localDir, remote, localOptions, cmd) {
|
|
1650
|
+
const options = cmd.optsWithGlobals();
|
|
1524
1651
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1652
|
+
const [error, app] = await safe(getApp(options));
|
|
1653
|
+
if (error) return exit(error);
|
|
1654
|
+
if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
|
|
1530
1655
|
|
|
1531
|
-
|
|
1532
|
-
req.end(); // this makes the request
|
|
1533
|
-
}
|
|
1656
|
+
if (app.installationState !== 'installed') exit('App is not yet running. Try again later.');
|
|
1534
1657
|
|
|
1535
|
-
function pushApp(localDir, remote, localOptions, cmd) {
|
|
1536
|
-
// deal with paths prefixed with ~
|
|
1537
1658
|
const local = localDir.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
1538
1659
|
const stat = fs.existsSync(path.resolve(local)) ? fs.lstatSync(local) : null;
|
|
1539
1660
|
|
|
1540
|
-
|
|
1661
|
+
let args, stdin;
|
|
1662
|
+
|
|
1663
|
+
if (stat && stat.isDirectory()) {
|
|
1541
1664
|
// Create a functor for stdin. If no data event handlers are attached, and there are no stream.pipe() destinations, and the stream is
|
|
1542
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.
|
|
1543
|
-
|
|
1666
|
+
stdin = function () {
|
|
1544
1667
|
var tarzip = spawn('tar', ['zcf', '-', '-C', path.dirname(local), path.basename(local)], { stdio: 'pipe' });
|
|
1545
1668
|
return tarzip.stdout;
|
|
1546
1669
|
};
|
|
1547
|
-
|
|
1548
|
-
execApp(['tar', 'zxvf', '-', '-C', remote], localOptions, cmd);
|
|
1670
|
+
args = ['tar', 'zxvf', '-', '-C', remote];
|
|
1549
1671
|
} else {
|
|
1550
1672
|
if (local === '-') {
|
|
1551
|
-
|
|
1673
|
+
stdin = process.stdin;
|
|
1552
1674
|
} else if (stat) {
|
|
1553
|
-
|
|
1675
|
+
stdin = fs.createReadStream(local);
|
|
1554
1676
|
} else {
|
|
1555
1677
|
exit('local file ' + local + ' does not exist');
|
|
1556
1678
|
}
|
|
1557
1679
|
|
|
1558
|
-
|
|
1680
|
+
stdin.on('error', function (e) { exit('Error pushing', e); });
|
|
1559
1681
|
|
|
1560
|
-
if (remote.endsWith('/')) {
|
|
1561
|
-
remote = remote + '/' + path.basename(local);
|
|
1682
|
+
if (remote.endsWith('/')) {
|
|
1683
|
+
remote = remote + '/' + path.basename(local);
|
|
1562
1684
|
}
|
|
1563
1685
|
|
|
1564
|
-
|
|
1686
|
+
args = ['bash', '-c', `cat - > "${remote}"`];
|
|
1565
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);
|
|
1566
1700
|
}
|
|
1567
1701
|
|
|
1568
|
-
function pull(remote, local, localOptions, cmd) {
|
|
1569
|
-
|
|
1570
|
-
|
|
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);
|
|
1571
1715
|
var unzip = zlib.createGunzip();
|
|
1572
1716
|
|
|
1573
1717
|
unzip.pipe(untar);
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
execApp(['tar', 'zcf', '-', '-C', remote, '.'], localOptions, cmd);
|
|
1718
|
+
stdout = unzip;
|
|
1719
|
+
args = ['tar', 'zcf', '-', '-C', remote, '.'];
|
|
1577
1720
|
} else {
|
|
1578
1721
|
if (fs.existsSync(local) && fs.lstatSync(local).isDirectory()) {
|
|
1579
1722
|
local = path.join(local, path.basename(remote));
|
|
1580
|
-
|
|
1723
|
+
stdout = fs.createWriteStream(local);
|
|
1581
1724
|
} else if (local === '-') {
|
|
1582
|
-
|
|
1725
|
+
stdout = process.stdout;
|
|
1583
1726
|
} else {
|
|
1584
|
-
|
|
1727
|
+
stdout = fs.createWriteStream(local);
|
|
1585
1728
|
}
|
|
1586
1729
|
|
|
1587
|
-
|
|
1730
|
+
stdout.on('error', function (e) { exit('Error pulling', e); });
|
|
1588
1731
|
|
|
1589
|
-
|
|
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}`);
|
|
1590
1911
|
}
|
|
1591
1912
|
}
|
|
1592
1913
|
|
|
@@ -1775,5 +2096,8 @@ export default {
|
|
|
1775
2096
|
envGet,
|
|
1776
2097
|
envSet,
|
|
1777
2098
|
envList,
|
|
1778
|
-
envUnset
|
|
2099
|
+
envUnset,
|
|
2100
|
+
|
|
2101
|
+
syncPush,
|
|
2102
|
+
syncPull
|
|
1779
2103
|
};
|
package/src/build-actions.js
CHANGED
|
@@ -295,11 +295,6 @@ async function build(localOptions, cmd) {
|
|
|
295
295
|
|
|
296
296
|
const appConfig = config.getCwdConfig(sourceDir);
|
|
297
297
|
const buildServiceConfig = getEffectiveBuildServiceConfig(options);
|
|
298
|
-
if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
|
|
299
|
-
console.log('Building using remote build service at %s', buildServiceConfig.url);
|
|
300
|
-
} else {
|
|
301
|
-
console.log('Building locally with Docker.');
|
|
302
|
-
}
|
|
303
298
|
|
|
304
299
|
let repository = appConfig.repository;
|
|
305
300
|
if (!repository || options.repository) {
|
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);
|