@dynamicweb/cli 1.1.0 → 1.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/README.md +12 -0
- package/bin/commands/command.js +85 -10
- package/bin/commands/files.js +186 -62
- package/bin/commands/query.js +94 -22
- package/bin/downloader.js +8 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ The files command is used to list out and export the structure in your Dynamicwe
|
|
|
98
98
|
- `-e` `--export` It will export \<dirPath\> into \<outPath\> on your local machine, unzipped by default
|
|
99
99
|
- `--raw` This will keep the content zipped
|
|
100
100
|
- `--iamstupid` This will include the export of the /files/system/log and /files/.cache folders
|
|
101
|
+
- `--json` This will output a single JSON object for automation-friendly parsing
|
|
101
102
|
|
|
102
103
|
#### Examples
|
|
103
104
|
Exporting all templates from current environment to local solution
|
|
@@ -108,6 +109,9 @@ Exporting all templates from current environment to local solution
|
|
|
108
109
|
Listing the system files structure of the current environment
|
|
109
110
|
> $ dw files system -lr
|
|
110
111
|
|
|
112
|
+
Uploading files with JSON output for automation
|
|
113
|
+
> $ dw files ./Files templates -i -r --json
|
|
114
|
+
|
|
111
115
|
### Files Source Type Detection
|
|
112
116
|
By default, the `dw files` command automatically detects the source type based on the \<dirPath\>:
|
|
113
117
|
If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file.
|
|
@@ -152,6 +156,7 @@ Pulling and overriding local solution with latest nightly build
|
|
|
152
156
|
The query command will fire any query towards the admin Api with the given query parameters. This means any query parameter that's necessary for the given query, is required as an option in this command. It's also possible to list which parameters is necessary for the given query through the options;
|
|
153
157
|
- `-l` `--list` Will list all the properties for the given \<query\>
|
|
154
158
|
- `-i` `--interactive` Will perform the \<query\> but without any parameters, as they will be asked for one by one in interactive mode
|
|
159
|
+
- `--output json` Will output a single JSON object for automation-friendly parsing
|
|
155
160
|
- `--<queryParam>` Any parameter the query needs will be sent by '--key value'
|
|
156
161
|
|
|
157
162
|
#### Examples
|
|
@@ -161,12 +166,16 @@ Getting all properties for a query
|
|
|
161
166
|
Getting file information on a specific file by name
|
|
162
167
|
> $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail
|
|
163
168
|
|
|
169
|
+
Running a query with JSON output
|
|
170
|
+
> $ dw query FileByName --name DefaultMail.html --output json
|
|
171
|
+
|
|
164
172
|
### Command
|
|
165
173
|
> $ dw command \<command\>
|
|
166
174
|
|
|
167
175
|
Using command will, like query, fire any given command in the solution. It works like query, given the query parameters necessary, however if a `DataModel` is required for the command, it is given in a json-format, either through a path to a .json file or a literal json-string in the command.
|
|
168
176
|
- `-l` `--list` Lists all the properties for the command, as well as the json model required
|
|
169
177
|
- `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }'
|
|
178
|
+
- `--output json` Outputs a single JSON object for automation-friendly parsing
|
|
170
179
|
|
|
171
180
|
#### Examples
|
|
172
181
|
Creating a copy of a page using a json-string
|
|
@@ -183,6 +192,9 @@ Where PageMove.json contains
|
|
|
183
192
|
Deleting a page
|
|
184
193
|
> $ dw command PageDelete --json '{ "id": "1383" }'
|
|
185
194
|
|
|
195
|
+
Running a command with JSON output
|
|
196
|
+
> $ dw command PageDelete --json '{ "id": "1383" }' --output json
|
|
197
|
+
|
|
186
198
|
### Install
|
|
187
199
|
> $ dw install \<filePath\>
|
|
188
200
|
|
package/bin/commands/command.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import { setupEnv, getAgent } from './env.js';
|
|
5
5
|
import { setupUser } from './login.js';
|
|
6
6
|
|
|
7
|
-
const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env']
|
|
7
|
+
const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
|
|
8
8
|
|
|
9
9
|
export function commandCommand() {
|
|
10
10
|
return {
|
|
@@ -22,22 +22,38 @@ export function commandCommand() {
|
|
|
22
22
|
alias: 'l',
|
|
23
23
|
describe: 'Lists all the properties for the command, currently not working'
|
|
24
24
|
})
|
|
25
|
+
.option('output', {
|
|
26
|
+
choices: ['json'],
|
|
27
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
28
|
+
})
|
|
25
29
|
},
|
|
26
30
|
handler: async (argv) => {
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
const output = createCommandOutput(argv);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
output.verboseLog(`Running command ${argv.command}`);
|
|
35
|
+
await handleCommand(argv, output);
|
|
36
|
+
output.finish();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
output.fail(err);
|
|
39
|
+
output.finish();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
async function handleCommand(argv) {
|
|
46
|
+
async function handleCommand(argv, output) {
|
|
34
47
|
let env = await setupEnv(argv);
|
|
35
48
|
let user = await setupUser(argv, env);
|
|
36
49
|
if (argv.list) {
|
|
37
|
-
|
|
50
|
+
const properties = await getProperties(env, user, argv.command);
|
|
51
|
+
output.addData(properties);
|
|
52
|
+
output.log(properties);
|
|
38
53
|
} else {
|
|
39
|
-
let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json))
|
|
40
|
-
|
|
54
|
+
let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json));
|
|
55
|
+
output.addData(response);
|
|
56
|
+
output.log(response);
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
@@ -82,8 +98,67 @@ async function runCommand(env, user, command, queryParams, data) {
|
|
|
82
98
|
agent: getAgent(env.protocol)
|
|
83
99
|
})
|
|
84
100
|
if (!res.ok) {
|
|
85
|
-
|
|
86
|
-
process.exit(1);
|
|
101
|
+
throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
|
|
87
102
|
}
|
|
88
103
|
return await res.json()
|
|
89
|
-
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createCommandOutput(argv) {
|
|
107
|
+
const response = {
|
|
108
|
+
ok: true,
|
|
109
|
+
command: 'command',
|
|
110
|
+
operation: argv.list ? 'list' : 'run',
|
|
111
|
+
status: 200,
|
|
112
|
+
data: [],
|
|
113
|
+
errors: [],
|
|
114
|
+
meta: {
|
|
115
|
+
commandName: argv.command
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
json: argv.output === 'json',
|
|
121
|
+
response,
|
|
122
|
+
log(value) {
|
|
123
|
+
if (!this.json) {
|
|
124
|
+
console.log(value);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
verboseLog(...args) {
|
|
128
|
+
if (argv.verbose && !this.json) {
|
|
129
|
+
console.info(...args);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
addData(entry) {
|
|
133
|
+
response.data.push(entry);
|
|
134
|
+
},
|
|
135
|
+
fail(err) {
|
|
136
|
+
response.ok = false;
|
|
137
|
+
response.status = err?.status || 1;
|
|
138
|
+
response.errors.push({
|
|
139
|
+
message: err?.message || 'Unknown command error.',
|
|
140
|
+
details: err?.details ?? null
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
finish() {
|
|
144
|
+
if (this.json) {
|
|
145
|
+
console.log(JSON.stringify(response, null, 2));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createCommandError(message, status, details = null) {
|
|
152
|
+
const error = new Error(message);
|
|
153
|
+
error.status = status;
|
|
154
|
+
error.details = details;
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function parseJsonSafe(res) {
|
|
159
|
+
try {
|
|
160
|
+
return await res.json();
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/bin/commands/files.js
CHANGED
|
@@ -75,24 +75,41 @@ export function filesCommand() {
|
|
|
75
75
|
describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
|
|
76
76
|
conflicts: 'asFile'
|
|
77
77
|
})
|
|
78
|
+
.option('json', {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
81
|
+
})
|
|
78
82
|
},
|
|
79
83
|
handler: async (argv) => {
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const output = createFilesOutput(argv);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
output.verboseLog(`Listing directory at: ${argv.dirPath}`);
|
|
88
|
+
await handleFiles(argv, output);
|
|
89
|
+
output.finish();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
output.fail(err);
|
|
92
|
+
output.finish();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
async function handleFiles(argv) {
|
|
99
|
+
async function handleFiles(argv, output) {
|
|
87
100
|
let env = await setupEnv(argv);
|
|
88
101
|
let user = await setupUser(argv, env);
|
|
89
102
|
|
|
90
103
|
if (argv.list) {
|
|
91
104
|
let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
output.setStatus(200);
|
|
106
|
+
output.addData(files);
|
|
107
|
+
if (!output.json) {
|
|
108
|
+
output.log(files.name);
|
|
109
|
+
let hasFiles = files.files?.data && files.files?.data.length !== 0;
|
|
110
|
+
resolveTree(files.directories, '', hasFiles, output);
|
|
111
|
+
resolveTree(files.files?.data ?? [], '', false, output);
|
|
112
|
+
}
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
if (argv.export) {
|
|
@@ -106,31 +123,31 @@ async function handleFiles(argv) {
|
|
|
106
123
|
let parentDirectory = path.dirname(argv.dirPath);
|
|
107
124
|
parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
|
|
108
125
|
|
|
109
|
-
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true);
|
|
126
|
+
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, output);
|
|
110
127
|
} else {
|
|
111
|
-
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
128
|
+
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
|
|
112
129
|
}
|
|
113
130
|
} else {
|
|
114
131
|
await interactiveConfirm('Are you sure you want a full export of files?', async () => {
|
|
115
|
-
|
|
132
|
+
output.log('Full export is starting');
|
|
116
133
|
let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
|
|
117
134
|
let dirs = filesStructure.directories;
|
|
118
135
|
for (let id = 0; id < dirs.length; id++) {
|
|
119
136
|
const dir = dirs[id];
|
|
120
|
-
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
137
|
+
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
|
|
121
138
|
}
|
|
122
|
-
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false);
|
|
123
|
-
if (argv.raw)
|
|
139
|
+
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, output);
|
|
140
|
+
if (argv.raw) output.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip');
|
|
124
141
|
})
|
|
125
142
|
}
|
|
126
143
|
} else if (argv.import) {
|
|
127
144
|
if (argv.dirPath && argv.outPath) {
|
|
128
145
|
let resolvedPath = path.resolve(argv.dirPath);
|
|
129
146
|
if (argv.recursive) {
|
|
130
|
-
await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite);
|
|
147
|
+
await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output);
|
|
131
148
|
} else {
|
|
132
149
|
let filesInDir = getFilesInDirectory(resolvedPath);
|
|
133
|
-
await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite);
|
|
150
|
+
await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
}
|
|
@@ -142,20 +159,20 @@ function getFilesInDirectory(dirPath) {
|
|
|
142
159
|
.filter(file => fs.statSync(file).isFile());
|
|
143
160
|
}
|
|
144
161
|
|
|
145
|
-
async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) {
|
|
162
|
+
async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
|
|
146
163
|
let filesInDir = getFilesInDirectory(dirPath);
|
|
147
164
|
if (filesInDir.length > 0)
|
|
148
|
-
await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite);
|
|
165
|
+
await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
|
|
149
166
|
|
|
150
167
|
const subDirectories = fs.readdirSync(dirPath)
|
|
151
168
|
.map(subDir => path.join(dirPath, subDir))
|
|
152
169
|
.filter(subDir => fs.statSync(subDir).isDirectory());
|
|
153
170
|
for (let subDir of subDirectories) {
|
|
154
|
-
await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite);
|
|
171
|
+
await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
|
|
155
172
|
}
|
|
156
173
|
}
|
|
157
174
|
|
|
158
|
-
function resolveTree(dirs, indentLevel, parentHasFiles) {
|
|
175
|
+
function resolveTree(dirs, indentLevel, parentHasFiles, output) {
|
|
159
176
|
let end = `└──`
|
|
160
177
|
let mid = `├──`
|
|
161
178
|
for (let id = 0; id < dirs.length; id++) {
|
|
@@ -163,33 +180,33 @@ function resolveTree(dirs, indentLevel, parentHasFiles) {
|
|
|
163
180
|
let indentPipe = true;
|
|
164
181
|
if (dirs.length == 1) {
|
|
165
182
|
if (parentHasFiles) {
|
|
166
|
-
|
|
183
|
+
output.log(indentLevel + mid, dir.name)
|
|
167
184
|
} else {
|
|
168
|
-
|
|
185
|
+
output.log(indentLevel + end, dir.name)
|
|
169
186
|
indentPipe = false;
|
|
170
187
|
}
|
|
171
188
|
} else if (id != dirs.length - 1) {
|
|
172
|
-
|
|
189
|
+
output.log(indentLevel + mid, dir.name)
|
|
173
190
|
} else {
|
|
174
191
|
if (parentHasFiles) {
|
|
175
|
-
|
|
192
|
+
output.log(indentLevel + mid, dir.name)
|
|
176
193
|
} else {
|
|
177
|
-
|
|
194
|
+
output.log(indentLevel + end, dir.name)
|
|
178
195
|
indentPipe = false;
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
198
|
let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
|
|
182
199
|
if (indentPipe) {
|
|
183
|
-
resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
|
|
184
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
|
|
200
|
+
resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output);
|
|
201
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
|
|
185
202
|
} else {
|
|
186
|
-
resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
|
|
187
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
|
|
203
|
+
resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
|
|
204
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
|
|
188
205
|
}
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
192
|
-
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
|
|
209
|
+
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, output) {
|
|
193
210
|
let excludeDirectories = '';
|
|
194
211
|
if (!iamstupid) {
|
|
195
212
|
excludeDirectories = 'system/log';
|
|
@@ -200,7 +217,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
|
|
|
200
217
|
|
|
201
218
|
const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
|
|
202
219
|
|
|
203
|
-
displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
|
|
220
|
+
displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output);
|
|
204
221
|
|
|
205
222
|
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
|
|
206
223
|
method: 'POST',
|
|
@@ -212,27 +229,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
|
|
|
212
229
|
agent: getAgent(env.protocol)
|
|
213
230
|
});
|
|
214
231
|
|
|
215
|
-
const filename = outname || tryGetFileNameFromResponse(res, dirPath);
|
|
232
|
+
const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose);
|
|
216
233
|
if (!filename) return;
|
|
217
234
|
|
|
218
235
|
const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
|
|
219
|
-
const updater = createThrottledStatusUpdater();
|
|
236
|
+
const updater = output.json ? null : createThrottledStatusUpdater();
|
|
220
237
|
|
|
221
238
|
await downloadWithProgress(res, filePath, {
|
|
222
239
|
onData: (received) => {
|
|
223
|
-
updater
|
|
240
|
+
if (updater) {
|
|
241
|
+
updater.update(`Received:\t${formatBytes(received)}`);
|
|
242
|
+
}
|
|
224
243
|
}
|
|
225
244
|
});
|
|
226
245
|
|
|
227
|
-
updater
|
|
246
|
+
if (updater) {
|
|
247
|
+
updater.stop();
|
|
248
|
+
}
|
|
228
249
|
|
|
229
250
|
if (singleFileMode) {
|
|
230
|
-
|
|
251
|
+
output.log(`Successfully downloaded: ${filename}`);
|
|
231
252
|
} else {
|
|
232
|
-
|
|
253
|
+
output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
|
|
233
254
|
}
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
output.addData({
|
|
257
|
+
type: 'download',
|
|
258
|
+
directoryPath: dirPath,
|
|
259
|
+
filename,
|
|
260
|
+
outPath: path.resolve(outPath),
|
|
261
|
+
recursive,
|
|
262
|
+
raw
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await extractArchive(filename, filePath, outPath, raw, output);
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
|
|
@@ -249,10 +279,10 @@ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames
|
|
|
249
279
|
return { endpoint: 'FileDownload', data };
|
|
250
280
|
}
|
|
251
281
|
|
|
252
|
-
function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
|
|
282
|
+
function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) {
|
|
253
283
|
if (singleFileMode) {
|
|
254
284
|
const fileName = path.basename(fileNames[0] || 'unknown');
|
|
255
|
-
|
|
285
|
+
output.log('Downloading file: ' + fileName);
|
|
256
286
|
|
|
257
287
|
return;
|
|
258
288
|
}
|
|
@@ -261,30 +291,34 @@ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileM
|
|
|
261
291
|
? 'Base'
|
|
262
292
|
: directoryPath;
|
|
263
293
|
|
|
264
|
-
|
|
294
|
+
output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
|
|
265
295
|
}
|
|
266
296
|
|
|
267
|
-
async function extractArchive(filename, filePath, outPath, raw) {
|
|
297
|
+
async function extractArchive(filename, filePath, outPath, raw, output) {
|
|
268
298
|
if (raw) {
|
|
269
299
|
return;
|
|
270
300
|
}
|
|
271
301
|
|
|
272
|
-
|
|
302
|
+
output.log(`\nExtracting ${filename} to ${outPath}`);
|
|
273
303
|
let destinationFilename = filename.replace('.zip', '');
|
|
274
304
|
if (destinationFilename === 'Base')
|
|
275
305
|
destinationFilename = '';
|
|
276
306
|
|
|
277
307
|
const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
|
|
278
|
-
const updater = createThrottledStatusUpdater();
|
|
308
|
+
const updater = output.json ? null : createThrottledStatusUpdater();
|
|
279
309
|
|
|
280
310
|
await extractWithProgress(filePath, destinationPath, {
|
|
281
311
|
onEntry: (processedEntries, totalEntries, percent) => {
|
|
282
|
-
updater
|
|
312
|
+
if (updater) {
|
|
313
|
+
updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
|
|
314
|
+
}
|
|
283
315
|
}
|
|
284
316
|
});
|
|
285
317
|
|
|
286
|
-
updater
|
|
287
|
-
|
|
318
|
+
if (updater) {
|
|
319
|
+
updater.stop();
|
|
320
|
+
}
|
|
321
|
+
output.log(`Finished extracting ${filename} to ${outPath}\n`);
|
|
288
322
|
|
|
289
323
|
fs.unlink(filePath, function(err) {});
|
|
290
324
|
}
|
|
@@ -300,14 +334,12 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
|
300
334
|
if (res.ok) {
|
|
301
335
|
return await res.json();
|
|
302
336
|
} else {
|
|
303
|
-
|
|
304
|
-
console.log(await res.json());
|
|
305
|
-
process.exit(1);
|
|
337
|
+
throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
|
|
306
338
|
}
|
|
307
339
|
}
|
|
308
340
|
|
|
309
|
-
export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
|
|
310
|
-
|
|
341
|
+
export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
|
|
342
|
+
output.log('Uploading files')
|
|
311
343
|
|
|
312
344
|
const chunkSize = 300;
|
|
313
345
|
const chunks = [];
|
|
@@ -316,26 +348,37 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
|
|
|
316
348
|
chunks.push(localFilePaths.slice(i, i + chunkSize));
|
|
317
349
|
}
|
|
318
350
|
|
|
351
|
+
output.mergeMeta({
|
|
352
|
+
filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
|
|
353
|
+
chunks: (output.response.meta.chunks || 0) + chunks.length
|
|
354
|
+
});
|
|
355
|
+
|
|
319
356
|
for (let i = 0; i < chunks.length; i++) {
|
|
320
|
-
|
|
357
|
+
output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
|
|
321
358
|
|
|
322
359
|
const chunk = chunks[i];
|
|
323
|
-
await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite);
|
|
324
|
-
|
|
325
|
-
|
|
360
|
+
const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output);
|
|
361
|
+
output.addData({
|
|
362
|
+
type: 'upload',
|
|
363
|
+
destinationPath,
|
|
364
|
+
files: chunk.map(filePath => path.resolve(filePath)),
|
|
365
|
+
response: body
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
|
|
326
369
|
}
|
|
327
370
|
|
|
328
|
-
|
|
371
|
+
output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
|
|
329
372
|
}
|
|
330
373
|
|
|
331
|
-
async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) {
|
|
374
|
+
async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
|
|
332
375
|
const form = new FormData();
|
|
333
376
|
form.append('path', destinationPath);
|
|
334
377
|
form.append('skipExistingFiles', String(!overwrite));
|
|
335
378
|
form.append('allowOverwrite', String(overwrite));
|
|
336
379
|
|
|
337
380
|
filePathsChunk.forEach(fileToUpload => {
|
|
338
|
-
|
|
381
|
+
output.log(`${fileToUpload}`)
|
|
339
382
|
form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
|
|
340
383
|
});
|
|
341
384
|
|
|
@@ -349,12 +392,10 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
|
|
|
349
392
|
});
|
|
350
393
|
|
|
351
394
|
if (res.ok) {
|
|
352
|
-
|
|
395
|
+
return await res.json();
|
|
353
396
|
}
|
|
354
397
|
else {
|
|
355
|
-
|
|
356
|
-
console.log(await res.json())
|
|
357
|
-
process.exit(1);
|
|
398
|
+
throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
401
|
|
|
@@ -376,3 +417,86 @@ function wildcardToRegExp(wildcard) {
|
|
|
376
417
|
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
377
418
|
}
|
|
378
419
|
|
|
420
|
+
function createFilesOutput(argv) {
|
|
421
|
+
const response = {
|
|
422
|
+
ok: true,
|
|
423
|
+
command: 'files',
|
|
424
|
+
operation: getFilesOperation(argv),
|
|
425
|
+
status: 200,
|
|
426
|
+
data: [],
|
|
427
|
+
errors: [],
|
|
428
|
+
meta: {}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
json: Boolean(argv.json),
|
|
433
|
+
verbose: Boolean(argv.verbose),
|
|
434
|
+
response,
|
|
435
|
+
log(...args) {
|
|
436
|
+
if (!this.json) {
|
|
437
|
+
console.log(...args);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
verboseLog(...args) {
|
|
441
|
+
if (this.verbose && !this.json) {
|
|
442
|
+
console.info(...args);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
addData(entry) {
|
|
446
|
+
response.data.push(entry);
|
|
447
|
+
},
|
|
448
|
+
mergeMeta(meta) {
|
|
449
|
+
response.meta = {
|
|
450
|
+
...response.meta,
|
|
451
|
+
...meta
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
setStatus(status) {
|
|
455
|
+
response.status = status;
|
|
456
|
+
},
|
|
457
|
+
fail(err) {
|
|
458
|
+
response.ok = false;
|
|
459
|
+
response.status = err?.status || 1;
|
|
460
|
+
response.errors.push({
|
|
461
|
+
message: err?.message || 'Unknown files command error.',
|
|
462
|
+
details: err?.details ?? null
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
finish() {
|
|
466
|
+
if (this.json) {
|
|
467
|
+
console.log(JSON.stringify(response, null, 2));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function getFilesOperation(argv) {
|
|
474
|
+
if (argv.list) {
|
|
475
|
+
return 'list';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (argv.export) {
|
|
479
|
+
return 'export';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (argv.import) {
|
|
483
|
+
return 'import';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return 'unknown';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function createCommandError(message, status, details = null) {
|
|
490
|
+
const error = new Error(message);
|
|
491
|
+
error.status = status;
|
|
492
|
+
error.details = details;
|
|
493
|
+
return error;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function parseJsonSafe(res) {
|
|
497
|
+
try {
|
|
498
|
+
return await res.json();
|
|
499
|
+
} catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
package/bin/commands/query.js
CHANGED
|
@@ -3,7 +3,7 @@ import { setupEnv, getAgent } from './env.js';
|
|
|
3
3
|
import { setupUser } from './login.js';
|
|
4
4
|
import { input } from '@inquirer/prompts';
|
|
5
5
|
|
|
6
|
-
const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env']
|
|
6
|
+
const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
|
|
7
7
|
|
|
8
8
|
export function queryCommand() {
|
|
9
9
|
return {
|
|
@@ -22,30 +22,43 @@ export function queryCommand() {
|
|
|
22
22
|
alias: 'i',
|
|
23
23
|
describe: 'Runs in interactive mode to ask for query parameters one by one'
|
|
24
24
|
})
|
|
25
|
+
.option('output', {
|
|
26
|
+
choices: ['json'],
|
|
27
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
28
|
+
})
|
|
25
29
|
},
|
|
26
|
-
handler: (argv) => {
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
handler: async (argv) => {
|
|
31
|
+
const output = createQueryOutput(argv);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
output.verboseLog(`Running query ${argv.query}`);
|
|
35
|
+
await handleQuery(argv, output);
|
|
36
|
+
output.finish();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
output.fail(err);
|
|
39
|
+
output.finish();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
async function handleQuery(argv) {
|
|
46
|
+
async function handleQuery(argv, output) {
|
|
34
47
|
let env = await setupEnv(argv);
|
|
35
48
|
let user = await setupUser(argv, env);
|
|
36
49
|
if (argv.list) {
|
|
37
|
-
|
|
50
|
+
const properties = await getProperties(env, user, argv.query);
|
|
51
|
+
output.addData(properties);
|
|
52
|
+
output.log(properties);
|
|
38
53
|
} else {
|
|
39
|
-
let response = await runQuery(env, user, argv.query, await getQueryParams(argv))
|
|
40
|
-
|
|
54
|
+
let response = await runQuery(env, user, argv.query, await getQueryParams(argv));
|
|
55
|
+
output.addData(response);
|
|
56
|
+
output.log(response);
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
async function getProperties(
|
|
45
|
-
let
|
|
46
|
-
let user = await setupUser(argv, env);
|
|
47
|
-
|
|
48
|
-
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, {
|
|
60
|
+
async function getProperties(env, user, query) {
|
|
61
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${query}`, {
|
|
49
62
|
method: 'GET',
|
|
50
63
|
headers: {
|
|
51
64
|
'Authorization': `Bearer ${user.apiKey}`
|
|
@@ -55,20 +68,20 @@ async function getProperties(argv) {
|
|
|
55
68
|
if (res.ok) {
|
|
56
69
|
let body = await res.json()
|
|
57
70
|
if (body.model.properties.groups === undefined) {
|
|
58
|
-
|
|
59
|
-
process.exit(1);
|
|
71
|
+
throw createCommandError('Unable to fetch query parameters.', res.status, body);
|
|
60
72
|
}
|
|
61
73
|
return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`)
|
|
62
74
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
process.exit(1);
|
|
75
|
+
|
|
76
|
+
throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res));
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
async function getQueryParams(argv) {
|
|
69
80
|
let params = {}
|
|
70
81
|
if (argv.interactive) {
|
|
71
|
-
let
|
|
82
|
+
let env = await setupEnv(argv);
|
|
83
|
+
let user = await setupUser(argv, env);
|
|
84
|
+
let properties = await getProperties(env, user, argv.query);
|
|
72
85
|
console.log('The following properties will be requested:')
|
|
73
86
|
console.log(properties)
|
|
74
87
|
for (const p of properties) {
|
|
@@ -93,8 +106,67 @@ async function runQuery(env, user, query, params) {
|
|
|
93
106
|
agent: getAgent(env.protocol)
|
|
94
107
|
})
|
|
95
108
|
if (!res.ok) {
|
|
96
|
-
|
|
97
|
-
process.exit(1);
|
|
109
|
+
throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
|
|
98
110
|
}
|
|
99
111
|
return await res.json()
|
|
100
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createQueryOutput(argv) {
|
|
115
|
+
const response = {
|
|
116
|
+
ok: true,
|
|
117
|
+
command: 'query',
|
|
118
|
+
operation: argv.list ? 'list' : 'run',
|
|
119
|
+
status: 200,
|
|
120
|
+
data: [],
|
|
121
|
+
errors: [],
|
|
122
|
+
meta: {
|
|
123
|
+
query: argv.query
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
json: argv.output === 'json',
|
|
129
|
+
response,
|
|
130
|
+
log(value) {
|
|
131
|
+
if (!this.json) {
|
|
132
|
+
console.log(value);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
verboseLog(...args) {
|
|
136
|
+
if (argv.verbose && !this.json) {
|
|
137
|
+
console.info(...args);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
addData(entry) {
|
|
141
|
+
response.data.push(entry);
|
|
142
|
+
},
|
|
143
|
+
fail(err) {
|
|
144
|
+
response.ok = false;
|
|
145
|
+
response.status = err?.status || 1;
|
|
146
|
+
response.errors.push({
|
|
147
|
+
message: err?.message || 'Unknown query command error.',
|
|
148
|
+
details: err?.details ?? null
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
finish() {
|
|
152
|
+
if (this.json) {
|
|
153
|
+
console.log(JSON.stringify(response, null, 2));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createCommandError(message, status, details = null) {
|
|
160
|
+
const error = new Error(message);
|
|
161
|
+
error.status = status;
|
|
162
|
+
error.details = details;
|
|
163
|
+
return error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function parseJsonSafe(res) {
|
|
167
|
+
try {
|
|
168
|
+
return await res.json();
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
package/bin/downloader.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
export function getFileNameFromResponse(res, dirPath) {
|
|
10
10
|
const header = res.headers.get('content-disposition');
|
|
11
11
|
const parts = header?.split(';');
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
if (!parts) {
|
|
14
14
|
const msg = `No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`;
|
|
15
15
|
throw new Error(msg);
|
|
@@ -24,13 +24,16 @@ export function getFileNameFromResponse(res, dirPath) {
|
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} res - The HTTP response object to extract the file name from.
|
|
26
26
|
* @param {string} dirPath - The directory path to use for file name resolution.
|
|
27
|
+
* @param {boolean} verbose - Whether to log missing download information.
|
|
27
28
|
* @returns {string|null} The extracted file name, or null if extraction fails.
|
|
28
29
|
*/
|
|
29
|
-
export function tryGetFileNameFromResponse(res, dirPath) {
|
|
30
|
+
export function tryGetFileNameFromResponse(res, dirPath, verbose = false) {
|
|
30
31
|
try {
|
|
31
32
|
return getFileNameFromResponse(res, dirPath);
|
|
32
33
|
} catch (err) {
|
|
33
|
-
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.log(err.message);
|
|
36
|
+
}
|
|
34
37
|
return null;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -56,7 +59,7 @@ export function downloadWithProgress(res, filePath, options) {
|
|
|
56
59
|
res.body.on("data", chunk => {
|
|
57
60
|
const isFirstChunk = receivedBytes === 0;
|
|
58
61
|
const elapsed = Date.now() - startTime;
|
|
59
|
-
|
|
62
|
+
|
|
60
63
|
receivedBytes += chunk.length;
|
|
61
64
|
|
|
62
65
|
if (options?.onData) {
|
|
@@ -64,4 +67,4 @@ export function downloadWithProgress(res, filePath, options) {
|
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
});
|
|
67
|
-
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@dynamicweb/cli",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "CLI for interacting with Dynamicweb 10 solutions from the command line.",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.1",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"bin/**"
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"node-fetch": "^3.2.10",
|
|
49
49
|
"yargs": "^17.5.1"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|