@dynamicweb/cli 1.0.13 → 1.0.15
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/bin/commands/files.js +37 -30
- package/bin/downloader.js +67 -0
- package/bin/extractor.js +28 -0
- package/bin/utils.js +106 -1
- package/package.json +2 -1
package/bin/commands/files.js
CHANGED
|
@@ -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, tryGetFileNameFromResponse } 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
|
|
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
|
-
|
|
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
|
-
})
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 || tryGetFileNameFromResponse(res, dirPath);
|
|
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) {
|
|
@@ -0,0 +1,67 @@
|
|
|
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, dirPath) {
|
|
10
|
+
const header = res.headers.get('content-disposition');
|
|
11
|
+
const parts = header?.split(';');
|
|
12
|
+
|
|
13
|
+
if (!parts) {
|
|
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);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parts[1].split('=')[1].replace('+', ' ');
|
|
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
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Downloads a file with progress reporting.
|
|
40
|
+
* @param {Response} res - The response from which to read the stream data.
|
|
41
|
+
* @param {string} filePath - The path to write the file to.
|
|
42
|
+
* @param {Object} options - Options for the download.
|
|
43
|
+
* @param {(receivedBytes: number, elapsedMillis: number, isFirstChunk: boolean) => void} options.onData - Callback invoked with each data chunk.
|
|
44
|
+
* @returns {Promise<void>} Resolves when the download is complete.
|
|
45
|
+
*/
|
|
46
|
+
export function downloadWithProgress(res, filePath, options) {
|
|
47
|
+
const fileStream = fs.createWriteStream(filePath);
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let receivedBytes = 0;
|
|
50
|
+
let startTime = Date.now();
|
|
51
|
+
|
|
52
|
+
res.body.pipe(fileStream);
|
|
53
|
+
res.body.on("error", reject);
|
|
54
|
+
fileStream.on("finish", resolve);
|
|
55
|
+
|
|
56
|
+
res.body.on("data", chunk => {
|
|
57
|
+
const isFirstChunk = receivedBytes === 0;
|
|
58
|
+
const elapsed = Date.now() - startTime;
|
|
59
|
+
|
|
60
|
+
receivedBytes += chunk.length;
|
|
61
|
+
|
|
62
|
+
if (options?.onData) {
|
|
63
|
+
options.onData(receivedBytes, elapsed, isFirstChunk);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
package/bin/extractor.js
ADDED
|
@@ -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.
|
|
5
|
+
"version": "1.0.15",
|
|
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",
|