@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 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\>
@@ -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, getFileNameFromResponse } from '../downloader.js';
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
- await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, []);
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
- if (recursive) {
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
- console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive);
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 || getFileNameFromResponse(res);
215
+ const filename = outname || tryGetFileNameFromResponse(res, dirPath);
202
216
  if (!filename) return;
203
217
 
204
- let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
205
- let updater = createThrottledStatusUpdater();
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
- console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
229
+ if (singleFileMode) {
230
+ console.log(`Successfully downloaded: ${filename}`);
231
+ } else {
232
+ console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
233
+ }
214
234
 
215
- if (!raw) {
216
- console.log(`\nExtracting ${filename} to ${outPath}`);
235
+ await extractArchive(filename, filePath, outPath, raw);
236
+ }
217
237
 
218
- const filenameWithoutExtension = filename.replace('.zip', '');
219
- const destinationPath = `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}`;
238
+ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
239
+ const data = {
240
+ 'DirectoryPath': directoryPath ?? '/',
241
+ 'ExcludeDirectories': [excludeDirectories],
242
+ };
220
243
 
221
- updater = createThrottledStatusUpdater();
222
- await extractWithProgress(filePath, destinationPath, {
223
- onEntry: (processedEntries, totalEntries, percent) => {
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
- console.log(`Finished extracting ${filename} to ${outPath}\n`);
248
+ data['Ids'] = fileNames;
249
+ return { endpoint: 'FileDownload', data };
250
+ }
230
251
 
231
- fs.unlink(filePath, function(err) {});
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
- let form = new FormData();
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
- localFilePaths.forEach((localPath, index) => {
259
- let fileToUpload = resolveFilePath(localPath)
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
- let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
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
- throw new Error(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`);
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
@@ -2,7 +2,7 @@
2
2
  "name": "@dynamicweb/cli",
3
3
  "type": "module",
4
4
  "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.",
5
- "version": "1.0.14",
5
+ "version": "1.0.16",
6
6
  "main": "bin/index.js",
7
7
  "files": [
8
8
  "bin/*"