@dynamicweb/cli 1.0.16 → 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.
@@ -1,377 +1,502 @@
1
- import fetch from 'node-fetch';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import FormData from 'form-data';
5
- import { setupEnv, getAgent } from './env.js';
6
- import { setupUser } from './login.js';
7
- import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
8
- import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js';
9
- import { extractWithProgress } from '../extractor.js';
10
-
11
- export function filesCommand() {
12
- return {
13
- command: 'files [dirPath] [outPath]',
14
- describe: 'Handles files',
15
- builder: (yargs) => {
16
- return yargs
17
- .positional('dirPath', {
18
- describe: 'The directory to list or export'
19
- })
20
- .positional('outPath', {
21
- describe: 'The directory to export the specified directory to',
22
- default: '.'
23
- })
24
- .option('list', {
25
- alias: 'l',
26
- type: 'boolean',
27
- describe: 'Lists all directories and files'
28
- })
29
- .option('export', {
30
- alias: 'e',
31
- type: 'boolean',
32
- describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]'
33
- })
34
- .option('import', {
35
- alias: 'i',
36
- type: 'boolean',
37
- describe: 'Imports the file at [dirPath] to [outPath]'
38
- })
39
- .option('overwrite', {
40
- alias: 'o',
41
- type: 'boolean',
42
- describe: 'Used with import, will overwrite existing files at destination if set to true'
43
- })
44
- .option('createEmpty', {
45
- type: 'boolean',
46
- describe: 'Used with import, will create a file even if its empty'
47
- })
48
- .option('includeFiles', {
49
- alias: 'f',
50
- type: 'boolean',
51
- describe: 'Used with export, includes files in list of directories and files'
52
- })
53
- .option('recursive', {
54
- alias: 'r',
55
- type: 'boolean',
56
- describe: 'Used with list, import and export, handles all directories recursively'
57
- })
58
- .option('raw', {
59
- type: 'boolean',
60
- describe: 'Used with export, keeps zip file instead of unpacking it'
61
- })
62
- .option('iamstupid', {
63
- type: 'boolean',
64
- describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
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
- })
78
- },
79
- handler: (argv) => {
80
- if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
81
- handleFiles(argv)
82
- }
83
- }
84
- }
85
-
86
- async function handleFiles(argv) {
87
- let env = await setupEnv(argv);
88
- let user = await setupUser(argv, env);
89
-
90
- if (argv.list) {
91
- let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
92
- console.log(files.name)
93
- let hasFiles = files.files?.data && files.files?.data.length !== 0;
94
- resolveTree(files.directories, '', hasFiles);
95
- resolveTree(files.files?.data ?? [], '', false);
96
- }
97
-
98
- if (argv.export) {
99
- if (argv.dirPath) {
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
- }
113
- } else {
114
- await interactiveConfirm('Are you sure you want a full export of files?', async () => {
115
- console.log('Full export is starting')
116
- let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
117
- let dirs = filesStructure.directories;
118
- for (let id = 0; id < dirs.length; id++) {
119
- const dir = dirs[id];
120
- await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
121
- }
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) console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip')
124
- })
125
- }
126
- } else if (argv.import) {
127
- if (argv.dirPath && argv.outPath) {
128
- let resolvedPath = path.resolve(argv.dirPath);
129
- if (argv.recursive) {
130
- await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite);
131
- } else {
132
- let filesInDir = getFilesInDirectory(resolvedPath);
133
- await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite);
134
- }
135
- }
136
- }
137
- }
138
-
139
- function getFilesInDirectory(dirPath) {
140
- return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath)
141
- .map(file => path.join(dirPath, file))
142
- .filter(file => fs.statSync(file).isFile());
143
- }
144
-
145
- async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) {
146
- let filesInDir = getFilesInDirectory(dirPath);
147
- if (filesInDir.length > 0)
148
- await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite);
149
-
150
- const subDirectories = fs.readdirSync(dirPath)
151
- .map(subDir => path.join(dirPath, subDir))
152
- .filter(subDir => fs.statSync(subDir).isDirectory());
153
- for (let subDir of subDirectories) {
154
- await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite);
155
- }
156
- }
157
-
158
- function resolveTree(dirs, indentLevel, parentHasFiles) {
159
- let end = `└──`
160
- let mid = `├──`
161
- for (let id = 0; id < dirs.length; id++) {
162
- const dir = dirs[id];
163
- let indentPipe = true;
164
- if (dirs.length == 1) {
165
- if (parentHasFiles) {
166
- console.log(indentLevel + mid, dir.name)
167
- } else {
168
- console.log(indentLevel + end, dir.name)
169
- indentPipe = false;
170
- }
171
- } else if (id != dirs.length - 1) {
172
- console.log(indentLevel + mid, dir.name)
173
- } else {
174
- if (parentHasFiles) {
175
- console.log(indentLevel + mid, dir.name)
176
- } else {
177
- console.log(indentLevel + end, dir.name)
178
- indentPipe = false;
179
- }
180
- }
181
- let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
182
- if (indentPipe) {
183
- resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
184
- resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
185
- } else {
186
- resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
187
- resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
188
- }
189
- }
190
- }
191
-
192
- async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
193
- let excludeDirectories = '';
194
- if (!iamstupid) {
195
- excludeDirectories = 'system/log';
196
- if (dirPath === 'cache.net') {
197
- return;
198
- }
199
- }
200
-
201
- const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
202
-
203
- displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
204
-
205
- const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
206
- method: 'POST',
207
- body: JSON.stringify(data),
208
- headers: {
209
- 'Authorization': `Bearer ${user.apiKey}`,
210
- 'Content-Type': 'application/json'
211
- },
212
- agent: getAgent(env.protocol)
213
- });
214
-
215
- const filename = outname || tryGetFileNameFromResponse(res, dirPath);
216
- if (!filename) return;
217
-
218
- const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
219
- const updater = createThrottledStatusUpdater();
220
-
221
- await downloadWithProgress(res, filePath, {
222
- onData: (received) => {
223
- updater.update(`Received:\t${formatBytes(received)}`);
224
- }
225
- });
226
-
227
- updater.stop();
228
-
229
- if (singleFileMode) {
230
- console.log(`Successfully downloaded: ${filename}`);
231
- } else {
232
- console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
233
- }
234
-
235
- await extractArchive(filename, filePath, outPath, raw);
236
- }
237
-
238
- function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
239
- const data = {
240
- 'DirectoryPath': directoryPath ?? '/',
241
- 'ExcludeDirectories': [excludeDirectories],
242
- };
243
-
244
- if (recursive && !singleFileMode) {
245
- return { endpoint: 'DirectoryDownload', data };
246
- }
247
-
248
- data['Ids'] = fileNames;
249
- return { endpoint: 'FileDownload', data };
250
- }
251
-
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;
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) {});
290
- }
291
-
292
- async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
293
- let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? '/'}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, {
294
- method: 'GET',
295
- headers: {
296
- 'Authorization': `Bearer ${user.apiKey}`
297
- },
298
- agent: getAgent(env.protocol)
299
- });
300
- if (res.ok) {
301
- return await res.json();
302
- } else {
303
- console.log(res);
304
- console.log(res.json());
305
- process.exit(1);
306
- }
307
- }
308
-
309
- export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
310
- console.log('Uploading files')
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();
333
- form.append('path', destinationPath);
334
- form.append('skipExistingFiles', String(!overwrite));
335
- form.append('allowOverwrite', String(overwrite));
336
-
337
- filePathsChunk.forEach(fileToUpload => {
338
- console.log(`${fileToUpload}`)
339
- form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
340
- });
341
-
342
- const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
343
- method: 'POST',
344
- body: form,
345
- headers: {
346
- 'Authorization': `Bearer ${user.apiKey}`
347
- },
348
- agent: getAgent(env.protocol)
349
- });
350
-
351
- if (res.ok) {
352
- console.log(await res.json())
353
- }
354
- else {
355
- console.log(res)
356
- console.log(await res.json())
357
- process.exit(1);
358
- }
359
- }
360
-
361
- export function resolveFilePath(filePath) {
362
- let p = path.parse(path.resolve(filePath))
363
- let regex = wildcardToRegExp(p.base);
364
- let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
365
- if (resolvedPath === undefined)
366
- {
367
- console.log('Could not find any files with the name ' + filePath);
368
- process.exit(1);
369
- }
370
- return path.join(p.dir, resolvedPath);
371
- }
372
-
373
-
374
- function wildcardToRegExp(wildcard) {
375
- return new RegExp('^' + wildcard.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$');
376
- }
377
-
1
+ import fetch from 'node-fetch';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import FormData from 'form-data';
5
+ import { setupEnv, getAgent } from './env.js';
6
+ import { setupUser } from './login.js';
7
+ import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
8
+ import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js';
9
+ import { extractWithProgress } from '../extractor.js';
10
+
11
+ export function filesCommand() {
12
+ return {
13
+ command: 'files [dirPath] [outPath]',
14
+ describe: 'Handles files',
15
+ builder: (yargs) => {
16
+ return yargs
17
+ .positional('dirPath', {
18
+ describe: 'The directory to list or export'
19
+ })
20
+ .positional('outPath', {
21
+ describe: 'The directory to export the specified directory to',
22
+ default: '.'
23
+ })
24
+ .option('list', {
25
+ alias: 'l',
26
+ type: 'boolean',
27
+ describe: 'Lists all directories and files'
28
+ })
29
+ .option('export', {
30
+ alias: 'e',
31
+ type: 'boolean',
32
+ describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]'
33
+ })
34
+ .option('import', {
35
+ alias: 'i',
36
+ type: 'boolean',
37
+ describe: 'Imports the file at [dirPath] to [outPath]'
38
+ })
39
+ .option('overwrite', {
40
+ alias: 'o',
41
+ type: 'boolean',
42
+ describe: 'Used with import, will overwrite existing files at destination if set to true'
43
+ })
44
+ .option('createEmpty', {
45
+ type: 'boolean',
46
+ describe: 'Used with import, will create a file even if its empty'
47
+ })
48
+ .option('includeFiles', {
49
+ alias: 'f',
50
+ type: 'boolean',
51
+ describe: 'Used with export, includes files in list of directories and files'
52
+ })
53
+ .option('recursive', {
54
+ alias: 'r',
55
+ type: 'boolean',
56
+ describe: 'Used with list, import and export, handles all directories recursively'
57
+ })
58
+ .option('raw', {
59
+ type: 'boolean',
60
+ describe: 'Used with export, keeps zip file instead of unpacking it'
61
+ })
62
+ .option('iamstupid', {
63
+ type: 'boolean',
64
+ describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
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
+ })
78
+ .option('json', {
79
+ type: 'boolean',
80
+ describe: 'Outputs a single JSON response for automation-friendly parsing'
81
+ })
82
+ },
83
+ handler: async (argv) => {
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
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ async function handleFiles(argv, output) {
100
+ let env = await setupEnv(argv);
101
+ let user = await setupUser(argv, env);
102
+
103
+ if (argv.list) {
104
+ let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
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
+ }
113
+ }
114
+
115
+ if (argv.export) {
116
+ if (argv.dirPath) {
117
+
118
+ const isFile = argv.asFile || argv.asDirectory
119
+ ? argv.asFile
120
+ : path.extname(argv.dirPath) !== '';
121
+
122
+ if (isFile) {
123
+ let parentDirectory = path.dirname(argv.dirPath);
124
+ parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
125
+
126
+ await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, output);
127
+ } else {
128
+ await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
129
+ }
130
+ } else {
131
+ await interactiveConfirm('Are you sure you want a full export of files?', async () => {
132
+ output.log('Full export is starting');
133
+ let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
134
+ let dirs = filesStructure.directories;
135
+ for (let id = 0; id < dirs.length; id++) {
136
+ const dir = dirs[id];
137
+ await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
138
+ }
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');
141
+ })
142
+ }
143
+ } else if (argv.import) {
144
+ if (argv.dirPath && argv.outPath) {
145
+ let resolvedPath = path.resolve(argv.dirPath);
146
+ if (argv.recursive) {
147
+ await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output);
148
+ } else {
149
+ let filesInDir = getFilesInDirectory(resolvedPath);
150
+ await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ function getFilesInDirectory(dirPath) {
157
+ return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath)
158
+ .map(file => path.join(dirPath, file))
159
+ .filter(file => fs.statSync(file).isFile());
160
+ }
161
+
162
+ async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
163
+ let filesInDir = getFilesInDirectory(dirPath);
164
+ if (filesInDir.length > 0)
165
+ await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
166
+
167
+ const subDirectories = fs.readdirSync(dirPath)
168
+ .map(subDir => path.join(dirPath, subDir))
169
+ .filter(subDir => fs.statSync(subDir).isDirectory());
170
+ for (let subDir of subDirectories) {
171
+ await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
172
+ }
173
+ }
174
+
175
+ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
176
+ let end = `└──`
177
+ let mid = `├──`
178
+ for (let id = 0; id < dirs.length; id++) {
179
+ const dir = dirs[id];
180
+ let indentPipe = true;
181
+ if (dirs.length == 1) {
182
+ if (parentHasFiles) {
183
+ output.log(indentLevel + mid, dir.name)
184
+ } else {
185
+ output.log(indentLevel + end, dir.name)
186
+ indentPipe = false;
187
+ }
188
+ } else if (id != dirs.length - 1) {
189
+ output.log(indentLevel + mid, dir.name)
190
+ } else {
191
+ if (parentHasFiles) {
192
+ output.log(indentLevel + mid, dir.name)
193
+ } else {
194
+ output.log(indentLevel + end, dir.name)
195
+ indentPipe = false;
196
+ }
197
+ }
198
+ let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
199
+ if (indentPipe) {
200
+ resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output);
201
+ resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
202
+ } else {
203
+ resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
204
+ resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
205
+ }
206
+ }
207
+ }
208
+
209
+ async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, output) {
210
+ let excludeDirectories = '';
211
+ if (!iamstupid) {
212
+ excludeDirectories = 'system/log';
213
+ if (dirPath === 'cache.net') {
214
+ return;
215
+ }
216
+ }
217
+
218
+ const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
219
+
220
+ displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output);
221
+
222
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
223
+ method: 'POST',
224
+ body: JSON.stringify(data),
225
+ headers: {
226
+ 'Authorization': `Bearer ${user.apiKey}`,
227
+ 'Content-Type': 'application/json'
228
+ },
229
+ agent: getAgent(env.protocol)
230
+ });
231
+
232
+ const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose);
233
+ if (!filename) return;
234
+
235
+ const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
236
+ const updater = output.json ? null : createThrottledStatusUpdater();
237
+
238
+ await downloadWithProgress(res, filePath, {
239
+ onData: (received) => {
240
+ if (updater) {
241
+ updater.update(`Received:\t${formatBytes(received)}`);
242
+ }
243
+ }
244
+ });
245
+
246
+ if (updater) {
247
+ updater.stop();
248
+ }
249
+
250
+ if (singleFileMode) {
251
+ output.log(`Successfully downloaded: ${filename}`);
252
+ } else {
253
+ output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
254
+ }
255
+
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);
266
+ }
267
+
268
+ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
269
+ const data = {
270
+ 'DirectoryPath': directoryPath ?? '/',
271
+ 'ExcludeDirectories': [excludeDirectories],
272
+ };
273
+
274
+ if (recursive && !singleFileMode) {
275
+ return { endpoint: 'DirectoryDownload', data };
276
+ }
277
+
278
+ data['Ids'] = fileNames;
279
+ return { endpoint: 'FileDownload', data };
280
+ }
281
+
282
+ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) {
283
+ if (singleFileMode) {
284
+ const fileName = path.basename(fileNames[0] || 'unknown');
285
+ output.log('Downloading file: ' + fileName);
286
+
287
+ return;
288
+ }
289
+
290
+ const directoryPathDisplayName = directoryPath === '/.'
291
+ ? 'Base'
292
+ : directoryPath;
293
+
294
+ output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
295
+ }
296
+
297
+ async function extractArchive(filename, filePath, outPath, raw, output) {
298
+ if (raw) {
299
+ return;
300
+ }
301
+
302
+ output.log(`\nExtracting ${filename} to ${outPath}`);
303
+ let destinationFilename = filename.replace('.zip', '');
304
+ if (destinationFilename === 'Base')
305
+ destinationFilename = '';
306
+
307
+ const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
308
+ const updater = output.json ? null : createThrottledStatusUpdater();
309
+
310
+ await extractWithProgress(filePath, destinationPath, {
311
+ onEntry: (processedEntries, totalEntries, percent) => {
312
+ if (updater) {
313
+ updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
314
+ }
315
+ }
316
+ });
317
+
318
+ if (updater) {
319
+ updater.stop();
320
+ }
321
+ output.log(`Finished extracting ${filename} to ${outPath}\n`);
322
+
323
+ fs.unlink(filePath, function(err) {});
324
+ }
325
+
326
+ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
327
+ let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? '/'}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, {
328
+ method: 'GET',
329
+ headers: {
330
+ 'Authorization': `Bearer ${user.apiKey}`
331
+ },
332
+ agent: getAgent(env.protocol)
333
+ });
334
+ if (res.ok) {
335
+ return await res.json();
336
+ } else {
337
+ throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
338
+ }
339
+ }
340
+
341
+ export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
342
+ output.log('Uploading files')
343
+
344
+ const chunkSize = 300;
345
+ const chunks = [];
346
+
347
+ for (let i = 0; i < localFilePaths.length; i += chunkSize) {
348
+ chunks.push(localFilePaths.slice(i, i + chunkSize));
349
+ }
350
+
351
+ output.mergeMeta({
352
+ filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
353
+ chunks: (output.response.meta.chunks || 0) + chunks.length
354
+ });
355
+
356
+ for (let i = 0; i < chunks.length; i++) {
357
+ output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
358
+
359
+ const chunk = chunks[i];
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}`);
369
+ }
370
+
371
+ output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
372
+ }
373
+
374
+ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
375
+ const form = new FormData();
376
+ form.append('path', destinationPath);
377
+ form.append('skipExistingFiles', String(!overwrite));
378
+ form.append('allowOverwrite', String(overwrite));
379
+
380
+ filePathsChunk.forEach(fileToUpload => {
381
+ output.log(`${fileToUpload}`)
382
+ form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
383
+ });
384
+
385
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
386
+ method: 'POST',
387
+ body: form,
388
+ headers: {
389
+ 'Authorization': `Bearer ${user.apiKey}`
390
+ },
391
+ agent: getAgent(env.protocol)
392
+ });
393
+
394
+ if (res.ok) {
395
+ return await res.json();
396
+ }
397
+ else {
398
+ throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
399
+ }
400
+ }
401
+
402
+ export function resolveFilePath(filePath) {
403
+ let p = path.parse(path.resolve(filePath))
404
+ let regex = wildcardToRegExp(p.base);
405
+ let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
406
+ if (resolvedPath === undefined)
407
+ {
408
+ console.log('Could not find any files with the name ' + filePath);
409
+ process.exit(1);
410
+ }
411
+ return path.join(p.dir, resolvedPath);
412
+ }
413
+
414
+
415
+ function wildcardToRegExp(wildcard) {
416
+ const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
417
+ return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
418
+ }
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
+ }