@dynamicweb/cli 1.0.12 → 1.0.14

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.
@@ -1,11 +1,12 @@
1
1
  import fetch from 'node-fetch';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
- import extract from 'extract-zip';
5
4
  import FormData from 'form-data';
6
5
  import { setupEnv, getAgent } from './env.js';
7
6
  import { setupUser } from './login.js';
8
- import { interactiveConfirm } from '../utils.js';
7
+ import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
8
+ import { downloadWithProgress, getFileNameFromResponse } from '../downloader.js';
9
+ import { extractWithProgress } from '../extractor.js';
9
10
 
10
11
  export function filesCommand() {
11
12
  return {
@@ -38,7 +39,7 @@ export function filesCommand() {
38
39
  .option('overwrite', {
39
40
  alias: 'o',
40
41
  type: 'boolean',
41
- describe: 'Used with import, will overwrite existing files at destrination if set to true'
42
+ describe: 'Used with import, will overwrite existing files at destination if set to true'
42
43
  })
43
44
  .option('createEmpty', {
44
45
  type: 'boolean',
@@ -187,8 +188,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
187
188
 
188
189
  console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive);
189
190
 
190
- let filename;
191
- fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
191
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
192
192
  method: 'POST',
193
193
  body: JSON.stringify(data),
194
194
  headers: {
@@ -196,33 +196,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
196
196
  'Content-Type': 'application/json'
197
197
  },
198
198
  agent: getAgent(env.protocol)
199
- }).then((res) => {
200
- const header = res.headers.get('content-disposition');
201
- const parts = header?.split(';');
202
- if (!parts) {
203
- console.log(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`);
204
- return;
205
- }
206
- filename = parts[1].split('=')[1].replace('+', ' ');
207
- if (outname) filename = outname;
208
- return res;
209
- }).then(async (res) => {
210
- if (!filename) return;
211
- let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
212
- const fileStream = fs.createWriteStream(filePath);
213
- await new Promise((resolve, reject) => {
214
- res.body.pipe(fileStream);
215
- res.body.on("error", reject);
216
- fileStream.on("finish", resolve);
217
- });
218
- console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
219
- if (!raw) {
220
- let filenameWithoutExtension = filename.replace('.zip', '')
221
- await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {})
222
- fs.unlink(filePath, function(err) {})
199
+ });
200
+
201
+ const filename = outname || getFileNameFromResponse(res);
202
+ if (!filename) return;
203
+
204
+ let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
205
+ let updater = createThrottledStatusUpdater();
206
+ await downloadWithProgress(res, filePath, {
207
+ onData: (received) => {
208
+ updater.update(`Received:\t${formatBytes(received)}`);
223
209
  }
224
- return res;
225
210
  });
211
+ updater.stop();
212
+
213
+ console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
214
+
215
+ if (!raw) {
216
+ console.log(`\nExtracting ${filename} to ${outPath}`);
217
+
218
+ const filenameWithoutExtension = filename.replace('.zip', '');
219
+ const destinationPath = `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}`;
220
+
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();
228
+
229
+ console.log(`Finished extracting ${filename} to ${outPath}\n`);
230
+
231
+ fs.unlink(filePath, function(err) {});
232
+ }
226
233
  }
227
234
 
228
235
  async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
@@ -13,6 +13,11 @@ export function installCommand() {
13
13
  .positional('filePath', {
14
14
  describe: 'Path to the file to install'
15
15
  })
16
+ .option('queue', {
17
+ alias: 'q',
18
+ type: 'boolean',
19
+ describe: 'Queues the install for next Dynamicweb recycle'
20
+ })
16
21
  },
17
22
  handler: (argv) => {
18
23
  if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`)
@@ -25,13 +30,14 @@ async function handleInstall(argv) {
25
30
  let env = await setupEnv(argv);
26
31
  let user = await setupUser(argv, env);
27
32
  await uploadFiles(env, user, [ argv.filePath ], 'System/AddIns/Local', false, true);
28
- await installAddin(env, user, resolveFilePath(argv.filePath))
33
+ await installAddin(env, user, resolveFilePath(argv.filePath), argv.queue)
29
34
  }
30
35
 
31
- async function installAddin(env, user, resolvedPath) {
36
+ async function installAddin(env, user, resolvedPath, queue) {
32
37
  console.log('Installing addin')
33
38
  let filename = path.basename(resolvedPath);
34
39
  let data = {
40
+ 'Queue': queue,
35
41
  'Ids': [
36
42
  `${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}`
37
43
  ]
@@ -118,6 +118,7 @@ async function loginInteractive(result, verbose) {
118
118
  getConfig().env[result.environment].users[result.username].apiKey = apiKey;
119
119
  getConfig().env[result.environment].current = getConfig().env[result.environment].current || {};
120
120
  getConfig().env[result.environment].current.user = result.username;
121
+ console.log("You're now logged in as " + result.username)
121
122
  updateConfig();
122
123
  }
123
124
 
@@ -131,12 +132,11 @@ async function login(username, password, env, protocol, verbose) {
131
132
  headers: {
132
133
  'Content-Type': 'application/x-www-form-urlencoded'
133
134
  },
134
- agent: getAgent(protocol)
135
+ agent: getAgent(protocol),
136
+ redirect: "manual"
135
137
  });
136
138
 
137
- if (res.ok) {
138
- console.log(res)
139
- console.log(res.json())
139
+ if (res.ok || res.status == 302) {
140
140
  let user = parseCookies(res.headers.get('set-cookie')).user;
141
141
  if (!user) return;
142
142
  return await getToken(user, env, protocol, verbose)
@@ -0,0 +1,49 @@
1
+ import { Response } from 'node-fetch';
2
+ import fs from 'fs';
3
+
4
+ /**
5
+ * Extracts the file name from the HTTP response.
6
+ * @param {Response} res - The HTTP response object.
7
+ * @returns {string} The extracted file name.
8
+ */
9
+ export function getFileNameFromResponse(res) {
10
+ const header = res.headers.get('content-disposition');
11
+ const parts = header?.split(';');
12
+
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`);
15
+ }
16
+
17
+ return parts[1].split('=')[1].replace('+', ' ');
18
+ }
19
+
20
+ /**
21
+ * Downloads a file with progress reporting.
22
+ * @param {Response} res - The response from which to read the stream data.
23
+ * @param {string} filePath - The path to write the file to.
24
+ * @param {Object} options - Options for the download.
25
+ * @param {(receivedBytes: number, elapsedMillis: number, isFirstChunk: boolean) => void} options.onData - Callback invoked with each data chunk.
26
+ * @returns {Promise<void>} Resolves when the download is complete.
27
+ */
28
+ export function downloadWithProgress(res, filePath, options) {
29
+ const fileStream = fs.createWriteStream(filePath);
30
+ return new Promise((resolve, reject) => {
31
+ let receivedBytes = 0;
32
+ let startTime = Date.now();
33
+
34
+ res.body.pipe(fileStream);
35
+ res.body.on("error", reject);
36
+ fileStream.on("finish", resolve);
37
+
38
+ res.body.on("data", chunk => {
39
+ const isFirstChunk = receivedBytes === 0;
40
+ const elapsed = Date.now() - startTime;
41
+
42
+ receivedBytes += chunk.length;
43
+
44
+ if (options?.onData) {
45
+ options.onData(receivedBytes, elapsed, isFirstChunk);
46
+ }
47
+ });
48
+ });
49
+ }
@@ -0,0 +1,28 @@
1
+ import extract from 'extract-zip';
2
+
3
+ /**
4
+ * Extracts files from a zip archive with progress reporting.
5
+ *
6
+ * @param {string} filePath - The path to the zip file to extract.
7
+ * @param {string} destinationPath - The directory where files will be extracted.
8
+ * @param {Object} [options] - Optional settings.
9
+ * @param {(processedEntries: number, totalEntries: number, percent: string) => void} [options.onEntry] - Callback invoked on each entry extracted.
10
+ * Receives the number of processed files, total entry count, and percent complete as arguments.
11
+ * @returns {Promise<void>} A promise that resolves when extraction is complete.
12
+ */
13
+ export function extractWithProgress(filePath, destinationPath, options) {
14
+ let processedEntries = 0;
15
+
16
+ return extract(filePath, {
17
+ dir: destinationPath,
18
+ onEntry: (_, zipFile) => {
19
+ processedEntries++;
20
+
21
+ const percent = Math.floor((processedEntries / zipFile.entryCount) * 100).toFixed(0);
22
+
23
+ if (options?.onEntry) {
24
+ options.onEntry(processedEntries, zipFile.entryCount, percent);
25
+ }
26
+ }
27
+ }, function (err) {});
28
+ }
package/bin/utils.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import yargsInteractive from 'yargs-interactive';
2
+ import logUpdate from 'log-update';
3
+
4
+ const WRITE_THROTTLE_MS = 500;
2
5
 
3
6
  export async function interactiveConfirm(question, func) {
4
7
  await yargsInteractive()
@@ -17,4 +20,106 @@ export async function interactiveConfirm(question, func) {
17
20
  if (!result.confirm) return;
18
21
  func()
19
22
  });
20
- }
23
+ }
24
+
25
+ /**
26
+ * Creates a throttled status updater that periodically logs status messages and elapsed time.
27
+ *
28
+ * The updater allows you to update the displayed status message at any time, but throttles the actual log updates
29
+ * to the specified interval. When called with no message, the updater stops and finalizes the log output.
30
+ *
31
+ * @param {number} [intervalMs=WRITE_THROTTLE_MS] - The interval in milliseconds at which to update the log output.
32
+ * @returns {{
33
+ * update: (message?: string) => void,
34
+ * stop: () => void
35
+ * }} An object with `update` and `stop` methods:
36
+ * - `update(message)`: Updates the status message and starts the updater if not already running. If called with no message, stops the updater.
37
+ * - `stop()`: Stops the updater and finalizes the log output.
38
+ *
39
+ * @example
40
+ * const updater = createThrottledStatusUpdater(500);
41
+ * updater.update('Processing...');
42
+ * // ... later
43
+ * updater.update('Still working...');
44
+ * // ... when done
45
+ * updater.update(); // or updater.stop();
46
+ */
47
+ export function createThrottledStatusUpdater(intervalMs = WRITE_THROTTLE_MS) {
48
+ let latestMessage;
49
+ let timer = null;
50
+ let stopped = false;
51
+ let startTime;
52
+
53
+ function write() {
54
+ const elapsed = `Elapsed:\t${formatElapsed(Date.now() - startTime)}`;
55
+ const lines = latestMessage ? [latestMessage, elapsed] : [elapsed];
56
+ logUpdate(lines.join('\n'));
57
+ }
58
+
59
+ function start() {
60
+ if (timer) return;
61
+
62
+ startTime = Date.now();
63
+
64
+ timer = setInterval(() => {
65
+ if (stopped) return;
66
+ write();
67
+ }, intervalMs);
68
+ }
69
+
70
+ function stop() {
71
+ stopped = true;
72
+ if (timer) clearInterval(timer);
73
+ write();
74
+ logUpdate.done();
75
+ }
76
+
77
+ // The updater function you call from anywhere
78
+ function update(message) {
79
+ if (!message) {
80
+ stop();
81
+ return;
82
+ }
83
+ latestMessage = message;
84
+ start();
85
+ }
86
+
87
+ return {
88
+ update,
89
+ stop
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Converts a number of bytes into a human-readable string with appropriate units.
95
+ *
96
+ * @param {number} bytes - The number of bytes to format.
97
+ * @returns {string} The formatted string representing the size in appropriate units (Bytes, KB, MB, GB, TB, PB).
98
+ */
99
+ export function formatBytes(bytes) {
100
+ if (bytes === 0) return '0 Bytes';
101
+ const k = 1024;
102
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
103
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
104
+ return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
105
+ }
106
+
107
+ /**
108
+ * Formats a duration given in milliseconds into a human-readable string.
109
+ *
110
+ * The output is in the format:
111
+ * - "Xh Ym Zs" if the duration is at least 1 hour,
112
+ * - "Ym Zs" if the duration is at least 1 minute but less than 1 hour,
113
+ * - "Zs" if the duration is less than 1 minute.
114
+ *
115
+ * @param {number} ms - The duration in milliseconds.
116
+ * @returns {string} The formatted elapsed time string.
117
+ */
118
+ export function formatElapsed(ms) {
119
+ const sec = Math.floor(ms / 1000);
120
+ const min = Math.floor(sec / 60);
121
+ const hr = Math.floor(min / 60);
122
+ if (hr > 0) return `${hr}h ${min % 60}m ${sec % 60}s`;
123
+ if (min > 0) return `${min}m ${sec % 60}s`;
124
+ return `${sec}s`;
125
+ }
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.12",
5
+ "version": "1.0.14",
6
6
  "main": "bin/index.js",
7
7
  "files": [
8
8
  "bin/*"
@@ -33,6 +33,7 @@
33
33
  "fetch": "^1.1.0",
34
34
  "form-data": "^4.0.0",
35
35
  "https": "^1.0.0",
36
+ "log-update": "^6.1.0",
36
37
  "node-fetch": "^3.2.10",
37
38
  "path": "^0.12.7",
38
39
  "yargs": "^17.5.1",