@dynamicweb/cli 1.0.14 → 1.0.16
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 +51 -0
- package/bin/commands/files.js +120 -43
- package/bin/downloader.js +20 -2
- package/bin/index.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,26 @@ Exporting all templates from current environment to local solution
|
|
|
86
86
|
Listing the system files structure of the current environment
|
|
87
87
|
> $ dw files system -lr
|
|
88
88
|
|
|
89
|
+
### Files Source Type Detection
|
|
90
|
+
By default, the `dw files` command automatically detects the source type based on the \<dirPath\>:
|
|
91
|
+
If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file.
|
|
92
|
+
Otherwise, it is treated as a directory.
|
|
93
|
+
In cases where this detection is incorrect, you can force the type using these flags:
|
|
94
|
+
|
|
95
|
+
- `-ad` `--asDirectory` Forces the command to treat the path as a directory, even if its name contains a dot.
|
|
96
|
+
- `-af` `--asFile` Forces the command to treat the path as a single file, even if it has no extension.
|
|
97
|
+
|
|
98
|
+
#### Examples
|
|
99
|
+
|
|
100
|
+
Exporting single file from current environment to local solution
|
|
101
|
+
> $ dw files templates/Translations.xml ./templates -e
|
|
102
|
+
|
|
103
|
+
Exporting a directory that looks like a file
|
|
104
|
+
> $ dw files templates/templates.v1 ./templates -e -ad
|
|
105
|
+
|
|
106
|
+
Exporting a file that has no extension
|
|
107
|
+
> $ dw files templates/testfile ./templates -e -af
|
|
108
|
+
|
|
89
109
|
### Swift
|
|
90
110
|
> $ dw swift \<outPath\>
|
|
91
111
|
|
|
@@ -169,3 +189,34 @@ Config is used to manage the .dwc file through the CLI, given any prop it will c
|
|
|
169
189
|
#### Examples
|
|
170
190
|
Changing the host for the dev environment
|
|
171
191
|
> $ dw config --env.dev.host localhost:6001
|
|
192
|
+
|
|
193
|
+
## Using Git Bash
|
|
194
|
+
If you're using Git Bash, you may encounter issues with path conversion that can interfere with relative paths used in commands.
|
|
195
|
+
|
|
196
|
+
### Path Conversion Issues
|
|
197
|
+
Git Bash automatically converts relative paths to absolute paths, which can cause problems.
|
|
198
|
+
You'll see a warning message if the conversion setting is not disabled:
|
|
199
|
+
|
|
200
|
+
"You appear to have path conversion turned on in your shell.
|
|
201
|
+
If you are using relative paths, this may interfere.
|
|
202
|
+
Please see https://doc.dynamicweb.dev/documentation/fundamentals/code/CLI.html for more information."
|
|
203
|
+
|
|
204
|
+
### Solution
|
|
205
|
+
To resolve this issue, disable path conversion by setting the `MSYS_NO_PATHCONV` environment variable (current session only):
|
|
206
|
+
|
|
207
|
+
> $ export MSYS_NO_PATHCONV=1
|
|
208
|
+
|
|
209
|
+
#### Examples
|
|
210
|
+
|
|
211
|
+
> $ export MSYS_NO_PATHCONV=1
|
|
212
|
+
> $ dw files -iro ./ /TestFolder --host \<host\> --apiKey \<apiKey\>
|
|
213
|
+
|
|
214
|
+
### Alternative Solutions
|
|
215
|
+
If you prefer not to disable path conversion globally, you can:
|
|
216
|
+
|
|
217
|
+
1. Prefix relative paths with `./` instead of just `/` to prevent conversion for specific commands.
|
|
218
|
+
2. Use PowerShell or CMD instead of Git Bash.
|
|
219
|
+
|
|
220
|
+
#### Examples
|
|
221
|
+
|
|
222
|
+
> $ dw files -iro ./ ./TestFolder --host \<host\> --apiKey \<apiKey\>
|
package/bin/commands/files.js
CHANGED
|
@@ -5,7 +5,7 @@ import FormData from 'form-data';
|
|
|
5
5
|
import { setupEnv, getAgent } from './env.js';
|
|
6
6
|
import { setupUser } from './login.js';
|
|
7
7
|
import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
|
|
8
|
-
import { downloadWithProgress,
|
|
8
|
+
import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js';
|
|
9
9
|
import { extractWithProgress } from '../extractor.js';
|
|
10
10
|
|
|
11
11
|
export function filesCommand() {
|
|
@@ -63,6 +63,18 @@ export function filesCommand() {
|
|
|
63
63
|
type: 'boolean',
|
|
64
64
|
describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
|
|
65
65
|
})
|
|
66
|
+
.option('asFile', {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
alias: 'af',
|
|
69
|
+
describe: 'Forces the command to treat the path as a single file, even if it has no extension.',
|
|
70
|
+
conflicts: 'asDirectory'
|
|
71
|
+
})
|
|
72
|
+
.option('asDirectory', {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
alias: 'ad',
|
|
75
|
+
describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
|
|
76
|
+
conflicts: 'asFile'
|
|
77
|
+
})
|
|
66
78
|
},
|
|
67
79
|
handler: (argv) => {
|
|
68
80
|
if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
|
|
@@ -85,7 +97,19 @@ async function handleFiles(argv) {
|
|
|
85
97
|
|
|
86
98
|
if (argv.export) {
|
|
87
99
|
if (argv.dirPath) {
|
|
88
|
-
|
|
100
|
+
|
|
101
|
+
const isFile = argv.asFile || argv.asDirectory
|
|
102
|
+
? argv.asFile
|
|
103
|
+
: path.extname(argv.dirPath) !== '';
|
|
104
|
+
|
|
105
|
+
if (isFile) {
|
|
106
|
+
let parentDirectory = path.dirname(argv.dirPath);
|
|
107
|
+
parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
|
|
108
|
+
|
|
109
|
+
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true);
|
|
110
|
+
} else {
|
|
111
|
+
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
112
|
+
}
|
|
89
113
|
} else {
|
|
90
114
|
await interactiveConfirm('Are you sure you want a full export of files?', async () => {
|
|
91
115
|
console.log('Full export is starting')
|
|
@@ -93,9 +117,9 @@ async function handleFiles(argv) {
|
|
|
93
117
|
let dirs = filesStructure.directories;
|
|
94
118
|
for (let id = 0; id < dirs.length; id++) {
|
|
95
119
|
const dir = dirs[id];
|
|
96
|
-
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, []);
|
|
120
|
+
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
97
121
|
}
|
|
98
|
-
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name));
|
|
122
|
+
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false);
|
|
99
123
|
if (argv.raw) console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip')
|
|
100
124
|
})
|
|
101
125
|
}
|
|
@@ -165,8 +189,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) {
|
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
191
|
|
|
168
|
-
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames) {
|
|
169
|
-
let endpoint;
|
|
192
|
+
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
|
|
170
193
|
let excludeDirectories = '';
|
|
171
194
|
if (!iamstupid) {
|
|
172
195
|
excludeDirectories = 'system/log';
|
|
@@ -174,19 +197,10 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
|
|
|
174
197
|
return;
|
|
175
198
|
}
|
|
176
199
|
}
|
|
177
|
-
let data = {
|
|
178
|
-
'DirectoryPath': dirPath ?? '/',
|
|
179
|
-
'ExcludeDirectories': [ excludeDirectories ],
|
|
180
|
-
}
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
endpoint = 'DirectoryDownload';
|
|
184
|
-
} else {
|
|
185
|
-
endpoint = 'FileDownload'
|
|
186
|
-
data['Ids'] = fileNames
|
|
187
|
-
}
|
|
201
|
+
const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
|
|
188
202
|
|
|
189
|
-
|
|
203
|
+
displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
|
|
190
204
|
|
|
191
205
|
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
|
|
192
206
|
method: 'POST',
|
|
@@ -198,38 +212,81 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
|
|
|
198
212
|
agent: getAgent(env.protocol)
|
|
199
213
|
});
|
|
200
214
|
|
|
201
|
-
const filename = outname ||
|
|
215
|
+
const filename = outname || tryGetFileNameFromResponse(res, dirPath);
|
|
202
216
|
if (!filename) return;
|
|
203
217
|
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
|
|
219
|
+
const updater = createThrottledStatusUpdater();
|
|
220
|
+
|
|
206
221
|
await downloadWithProgress(res, filePath, {
|
|
207
222
|
onData: (received) => {
|
|
208
223
|
updater.update(`Received:\t${formatBytes(received)}`);
|
|
209
224
|
}
|
|
210
225
|
});
|
|
226
|
+
|
|
211
227
|
updater.stop();
|
|
212
228
|
|
|
213
|
-
|
|
229
|
+
if (singleFileMode) {
|
|
230
|
+
console.log(`Successfully downloaded: ${filename}`);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
|
|
233
|
+
}
|
|
214
234
|
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
await extractArchive(filename, filePath, outPath, raw);
|
|
236
|
+
}
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
|
|
239
|
+
const data = {
|
|
240
|
+
'DirectoryPath': directoryPath ?? '/',
|
|
241
|
+
'ExcludeDirectories': [excludeDirectories],
|
|
242
|
+
};
|
|
220
243
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
updater.stop();
|
|
244
|
+
if (recursive && !singleFileMode) {
|
|
245
|
+
return { endpoint: 'DirectoryDownload', data };
|
|
246
|
+
}
|
|
228
247
|
|
|
229
|
-
|
|
248
|
+
data['Ids'] = fileNames;
|
|
249
|
+
return { endpoint: 'FileDownload', data };
|
|
250
|
+
}
|
|
230
251
|
|
|
231
|
-
|
|
252
|
+
function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
|
|
253
|
+
if (singleFileMode) {
|
|
254
|
+
const fileName = path.basename(fileNames[0] || 'unknown');
|
|
255
|
+
console.log('Downloading file: ' + fileName);
|
|
256
|
+
|
|
257
|
+
return;
|
|
232
258
|
}
|
|
259
|
+
|
|
260
|
+
const directoryPathDisplayName = directoryPath === '/.'
|
|
261
|
+
? 'Base'
|
|
262
|
+
: directoryPath;
|
|
263
|
+
|
|
264
|
+
console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function extractArchive(filename, filePath, outPath, raw) {
|
|
268
|
+
if (raw) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`\nExtracting ${filename} to ${outPath}`);
|
|
273
|
+
let destinationFilename = filename.replace('.zip', '');
|
|
274
|
+
if (destinationFilename === 'Base')
|
|
275
|
+
destinationFilename = '';
|
|
276
|
+
|
|
277
|
+
const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
|
|
278
|
+
const updater = createThrottledStatusUpdater();
|
|
279
|
+
|
|
280
|
+
await extractWithProgress(filePath, destinationPath, {
|
|
281
|
+
onEntry: (processedEntries, totalEntries, percent) => {
|
|
282
|
+
updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
updater.stop();
|
|
287
|
+
console.log(`Finished extracting ${filename} to ${outPath}\n`);
|
|
288
|
+
|
|
289
|
+
fs.unlink(filePath, function(err) {});
|
|
233
290
|
}
|
|
234
291
|
|
|
235
292
|
async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
@@ -251,18 +308,38 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
|
251
308
|
|
|
252
309
|
export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
|
|
253
310
|
console.log('Uploading files')
|
|
254
|
-
|
|
311
|
+
|
|
312
|
+
const chunkSize = 1000;
|
|
313
|
+
const chunks = [];
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < localFilePaths.length; i += chunkSize) {
|
|
316
|
+
chunks.push(localFilePaths.slice(i, i + chunkSize));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
320
|
+
console.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
|
|
321
|
+
|
|
322
|
+
const chunk = chunks[i];
|
|
323
|
+
await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite);
|
|
324
|
+
|
|
325
|
+
console.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) {
|
|
332
|
+
const form = new FormData();
|
|
255
333
|
form.append('path', destinationPath);
|
|
256
334
|
form.append('skipExistingFiles', String(!overwrite));
|
|
257
335
|
form.append('allowOverwrite', String(overwrite));
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
console.log(fileToUpload)
|
|
261
|
-
if (fileToUpload === undefined)
|
|
262
|
-
return;
|
|
336
|
+
|
|
337
|
+
filePathsChunk.forEach(fileToUpload => {
|
|
338
|
+
console.log(`${fileToUpload}`)
|
|
263
339
|
form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
|
|
264
340
|
});
|
|
265
|
-
|
|
341
|
+
|
|
342
|
+
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
|
|
266
343
|
method: 'POST',
|
|
267
344
|
body: form,
|
|
268
345
|
headers: {
|
|
@@ -270,13 +347,13 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
|
|
|
270
347
|
},
|
|
271
348
|
agent: getAgent(env.protocol)
|
|
272
349
|
});
|
|
350
|
+
|
|
273
351
|
if (res.ok) {
|
|
274
352
|
console.log(await res.json())
|
|
275
|
-
console.log(`Files uploaded`)
|
|
276
353
|
}
|
|
277
354
|
else {
|
|
278
355
|
console.log(res)
|
|
279
|
-
console.log(res.json())
|
|
356
|
+
console.log(await res.json())
|
|
280
357
|
process.exit(1);
|
|
281
358
|
}
|
|
282
359
|
}
|
package/bin/downloader.js
CHANGED
|
@@ -6,17 +6,35 @@ import fs from 'fs';
|
|
|
6
6
|
* @param {Response} res - The HTTP response object.
|
|
7
7
|
* @returns {string} The extracted file name.
|
|
8
8
|
*/
|
|
9
|
-
export function getFileNameFromResponse(res) {
|
|
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
|
+
throw new Error(msg);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
return parts[1].split('=')[1].replace('+', ' ');
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Attempts to extract the file name from an HTTP response.
|
|
23
|
+
* If extraction fails, logs the error message to the console.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} res - The HTTP response object to extract the file name from.
|
|
26
|
+
* @param {string} dirPath - The directory path to use for file name resolution.
|
|
27
|
+
* @returns {string|null} The extracted file name, or null if extraction fails.
|
|
28
|
+
*/
|
|
29
|
+
export function tryGetFileNameFromResponse(res, dirPath) {
|
|
30
|
+
try {
|
|
31
|
+
return getFileNameFromResponse(res, dirPath);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err.message);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
/**
|
|
21
39
|
* Downloads a file with progress reporting.
|
|
22
40
|
* @param {Response} res - The response from which to read the stream data.
|
package/bin/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { queryCommand } from './commands/query.js';
|
|
|
13
13
|
import { commandCommand } from './commands/command.js';
|
|
14
14
|
|
|
15
15
|
setupConfig();
|
|
16
|
+
showGitBashRelativePathWarning();
|
|
16
17
|
|
|
17
18
|
yargs(hideBin(process.argv))
|
|
18
19
|
.scriptName('dw')
|
|
@@ -58,4 +59,15 @@ function baseCommand() {
|
|
|
58
59
|
console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`)
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function showGitBashRelativePathWarning() {
|
|
65
|
+
const isGitBash = !!process.env.MSYSTEM;
|
|
66
|
+
const pathConversionDisabled = process.env.MSYS_NO_PATHCONV === '1';
|
|
67
|
+
|
|
68
|
+
if (isGitBash && !pathConversionDisabled) {
|
|
69
|
+
console.warn('You appear to have path conversion turned on in your shell.');
|
|
70
|
+
console.warn('If you are using relative paths, this may interfere.');
|
|
71
|
+
console.warn('Please see https://doc.dynamicweb.dev/documentation/fundamentals/code/CLI.html for more information.');
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
CHANGED