@dynamicweb/cli 1.1.0 → 2.0.0-beta.0

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.
@@ -2,7 +2,7 @@ import fetch from 'node-fetch';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4
  import FormData from 'form-data';
5
- import { setupEnv, getAgent } from './env.js';
5
+ import { setupEnv, getAgent, createCommandError } from './env.js';
6
6
  import { setupUser } from './login.js';
7
7
  import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
8
8
  import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js';
@@ -59,9 +59,31 @@ export function filesCommand() {
59
59
  type: 'boolean',
60
60
  describe: 'Used with export, keeps zip file instead of unpacking it'
61
61
  })
62
+ .option('dangerouslyIncludeLogsAndCache', {
63
+ type: 'boolean',
64
+ describe: 'Includes log and cache folders during export. Risky and usually not recommended'
65
+ })
62
66
  .option('iamstupid', {
63
67
  type: 'boolean',
64
- describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
68
+ hidden: true,
69
+ describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache'
70
+ })
71
+ .option('delete', {
72
+ alias: 'd',
73
+ type: 'boolean',
74
+ describe: 'Deletes the file or directory at [dirPath]. Detects type from path (use --asFile/--asDirectory to override)'
75
+ })
76
+ .option('empty', {
77
+ type: 'boolean',
78
+ describe: 'Used with --delete, empties a directory instead of deleting it'
79
+ })
80
+ .option('copy', {
81
+ type: 'string',
82
+ describe: 'Copies the file or directory at [dirPath] to the given destination path'
83
+ })
84
+ .option('move', {
85
+ type: 'string',
86
+ describe: 'Moves the file or directory at [dirPath] to the given destination path'
65
87
  })
66
88
  .option('asFile', {
67
89
  type: 'boolean',
@@ -75,64 +97,136 @@ export function filesCommand() {
75
97
  describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
76
98
  conflicts: 'asFile'
77
99
  })
100
+ .option('output', {
101
+ choices: ['json'],
102
+ describe: 'Outputs a single JSON response for automation-friendly parsing'
103
+ })
104
+ .option('json', {
105
+ type: 'boolean',
106
+ hidden: true,
107
+ describe: 'Deprecated alias for --output json'
108
+ })
78
109
  },
79
110
  handler: async (argv) => {
80
- if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
81
- await handleFiles(argv)
111
+ if (argv.json && !argv.output) {
112
+ argv.output = 'json';
113
+ console.warn('Warning: --json is deprecated and will be removed in a future release. Use --output json instead.');
114
+ }
115
+ if (argv.iamstupid && !argv.dangerouslyIncludeLogsAndCache) {
116
+ argv.dangerouslyIncludeLogsAndCache = true;
117
+ console.warn('Warning: --iamstupid is deprecated and will be removed in a future release. Use --dangerouslyIncludeLogsAndCache instead.');
118
+ }
119
+ const output = createFilesOutput(argv);
120
+
121
+ try {
122
+ await handleFiles(argv, output);
123
+ } catch (err) {
124
+ output.fail(err);
125
+ process.exitCode = 1;
126
+ } finally {
127
+ output.finish();
128
+ }
82
129
  }
83
130
  }
84
131
  }
85
132
 
86
- async function handleFiles(argv) {
87
- let env = await setupEnv(argv);
133
+ async function handleFiles(argv, output) {
134
+ let env = await setupEnv(argv, output);
88
135
  let user = await setupUser(argv, env);
89
136
 
90
137
  if (argv.list) {
138
+ output.verboseLog(`Listing directory at: ${argv.dirPath}`);
91
139
  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);
140
+ output.setStatus(200);
141
+ output.addData(files);
142
+ if (!output.json) {
143
+ output.log(files.name);
144
+ let hasFiles = files.files?.data && files.files?.data.length !== 0;
145
+ resolveTree(files.directories, '', hasFiles, output);
146
+ resolveTree(files.files?.data ?? [], '', false, output);
147
+ }
96
148
  }
97
149
 
98
150
  if (argv.export) {
99
151
  if (argv.dirPath) {
100
152
 
101
- const isFile = argv.asFile || argv.asDirectory
102
- ? argv.asFile
103
- : path.extname(argv.dirPath) !== '';
153
+ const isFile = isFilePath(argv, argv.dirPath);
104
154
 
105
155
  if (isFile) {
106
156
  let parentDirectory = path.dirname(argv.dirPath);
107
157
  parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
108
158
 
109
- await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true);
159
+ await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.dangerouslyIncludeLogsAndCache, [argv.dirPath], true, output);
110
160
  } else {
111
- await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
161
+ await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output);
112
162
  }
113
163
  } else {
114
- await interactiveConfirm('Are you sure you want a full export of files?', async () => {
115
- console.log('Full export is starting')
164
+ const fullExport = async () => {
165
+ output.log('Full export is starting');
116
166
  let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
117
167
  let dirs = filesStructure.directories;
118
168
  for (let id = 0; id < dirs.length; id++) {
119
169
  const dir = dirs[id];
120
- await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
170
+ await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output);
121
171
  }
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
- })
172
+ await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.dangerouslyIncludeLogsAndCache, Array.from(filesStructure.files.data, f => f.name), false, output);
173
+ 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');
174
+ };
175
+
176
+ if (output.json) {
177
+ await fullExport();
178
+ } else {
179
+ await interactiveConfirm('Are you sure you want a full export of files?', fullExport);
180
+ }
125
181
  }
126
182
  } else if (argv.import) {
127
183
  if (argv.dirPath && argv.outPath) {
128
184
  let resolvedPath = path.resolve(argv.dirPath);
129
185
  if (argv.recursive) {
130
- await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite);
186
+ await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output);
131
187
  } else {
132
188
  let filesInDir = getFilesInDirectory(resolvedPath);
133
- await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite);
189
+ await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
134
190
  }
135
191
  }
192
+ } else if (argv.delete) {
193
+ if (!argv.dirPath) {
194
+ throw createCommandError('A path is required for delete operations.', 400);
195
+ }
196
+
197
+ const isFile = isFilePath(argv, argv.dirPath);
198
+
199
+ if (argv.empty && isFile) {
200
+ throw createCommandError('--empty can only be used with directories.', 400);
201
+ }
202
+
203
+ const shouldConfirm = !output.json;
204
+
205
+ if (shouldConfirm) {
206
+ const action = argv.empty
207
+ ? `empty directory "${argv.dirPath}"`
208
+ : isFile
209
+ ? `delete file "${argv.dirPath}"`
210
+ : `delete directory "${argv.dirPath}"`;
211
+
212
+ await interactiveConfirm(`Are you sure you want to ${action}?`, async () => {
213
+ await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output);
214
+ });
215
+ } else {
216
+ await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output);
217
+ }
218
+ } else if (argv.copy) {
219
+ if (!argv.dirPath) {
220
+ throw createCommandError('A source path [dirPath] is required for copy operations.', 400);
221
+ }
222
+
223
+ await copyRemote(env, user, argv.dirPath, argv.copy, output);
224
+ } else if (argv.move) {
225
+ if (!argv.dirPath) {
226
+ throw createCommandError('A source path [dirPath] is required for move operations.', 400);
227
+ }
228
+
229
+ await moveRemote(env, user, argv.dirPath, argv.move, argv.overwrite, output);
136
230
  }
137
231
  }
138
232
 
@@ -142,20 +236,20 @@ function getFilesInDirectory(dirPath) {
142
236
  .filter(file => fs.statSync(file).isFile());
143
237
  }
144
238
 
145
- async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) {
239
+ async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
146
240
  let filesInDir = getFilesInDirectory(dirPath);
147
241
  if (filesInDir.length > 0)
148
- await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite);
242
+ await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
149
243
 
150
244
  const subDirectories = fs.readdirSync(dirPath)
151
245
  .map(subDir => path.join(dirPath, subDir))
152
246
  .filter(subDir => fs.statSync(subDir).isDirectory());
153
247
  for (let subDir of subDirectories) {
154
- await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite);
248
+ await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
155
249
  }
156
250
  }
157
251
 
158
- function resolveTree(dirs, indentLevel, parentHasFiles) {
252
+ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
159
253
  let end = `└──`
160
254
  let mid = `├──`
161
255
  for (let id = 0; id < dirs.length; id++) {
@@ -163,35 +257,35 @@ function resolveTree(dirs, indentLevel, parentHasFiles) {
163
257
  let indentPipe = true;
164
258
  if (dirs.length == 1) {
165
259
  if (parentHasFiles) {
166
- console.log(indentLevel + mid, dir.name)
260
+ output.log(indentLevel + mid, dir.name)
167
261
  } else {
168
- console.log(indentLevel + end, dir.name)
262
+ output.log(indentLevel + end, dir.name)
169
263
  indentPipe = false;
170
264
  }
171
265
  } else if (id != dirs.length - 1) {
172
- console.log(indentLevel + mid, dir.name)
266
+ output.log(indentLevel + mid, dir.name)
173
267
  } else {
174
268
  if (parentHasFiles) {
175
- console.log(indentLevel + mid, dir.name)
269
+ output.log(indentLevel + mid, dir.name)
176
270
  } else {
177
- console.log(indentLevel + end, dir.name)
271
+ output.log(indentLevel + end, dir.name)
178
272
  indentPipe = false;
179
273
  }
180
274
  }
181
275
  let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
182
276
  if (indentPipe) {
183
- resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
184
- resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
277
+ resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output);
278
+ resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
185
279
  } else {
186
- resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
187
- resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
280
+ resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
281
+ resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
188
282
  }
189
283
  }
190
284
  }
191
285
 
192
- async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
286
+ async function download(env, user, dirPath, outPath, recursive, outname, raw, dangerouslyIncludeLogsAndCache, fileNames, singleFileMode, output) {
193
287
  let excludeDirectories = '';
194
- if (!iamstupid) {
288
+ if (!dangerouslyIncludeLogsAndCache) {
195
289
  excludeDirectories = 'system/log';
196
290
  if (dirPath === 'cache.net') {
197
291
  return;
@@ -200,7 +294,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
200
294
 
201
295
  const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
202
296
 
203
- displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
297
+ displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output);
204
298
 
205
299
  const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
206
300
  method: 'POST',
@@ -212,27 +306,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
212
306
  agent: getAgent(env.protocol)
213
307
  });
214
308
 
215
- const filename = outname || tryGetFileNameFromResponse(res, dirPath);
309
+ const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose);
216
310
  if (!filename) return;
217
311
 
218
312
  const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
219
- const updater = createThrottledStatusUpdater();
313
+ const updater = output.json ? null : createThrottledStatusUpdater();
220
314
 
221
315
  await downloadWithProgress(res, filePath, {
222
316
  onData: (received) => {
223
- updater.update(`Received:\t${formatBytes(received)}`);
317
+ if (updater) {
318
+ updater.update(`Received:\t${formatBytes(received)}`);
319
+ }
224
320
  }
225
321
  });
226
322
 
227
- updater.stop();
323
+ if (updater) {
324
+ updater.stop();
325
+ }
228
326
 
229
327
  if (singleFileMode) {
230
- console.log(`Successfully downloaded: ${filename}`);
328
+ output.log(`Successfully downloaded: ${filename}`);
231
329
  } else {
232
- console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
330
+ output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
233
331
  }
234
332
 
235
- await extractArchive(filename, filePath, outPath, raw);
333
+ output.addData({
334
+ type: 'download',
335
+ directoryPath: dirPath,
336
+ filename,
337
+ outPath: path.resolve(outPath),
338
+ recursive,
339
+ raw
340
+ });
341
+
342
+ await extractArchive(filename, filePath, outPath, raw, output);
236
343
  }
237
344
 
238
345
  function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
@@ -249,10 +356,10 @@ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames
249
356
  return { endpoint: 'FileDownload', data };
250
357
  }
251
358
 
252
- function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
359
+ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) {
253
360
  if (singleFileMode) {
254
361
  const fileName = path.basename(fileNames[0] || 'unknown');
255
- console.log('Downloading file: ' + fileName);
362
+ output.log('Downloading file: ' + fileName);
256
363
 
257
364
  return;
258
365
  }
@@ -261,32 +368,40 @@ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileM
261
368
  ? 'Base'
262
369
  : directoryPath;
263
370
 
264
- console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
371
+ output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
265
372
  }
266
373
 
267
- async function extractArchive(filename, filePath, outPath, raw) {
374
+ async function extractArchive(filename, filePath, outPath, raw, output) {
268
375
  if (raw) {
269
376
  return;
270
377
  }
271
378
 
272
- console.log(`\nExtracting ${filename} to ${outPath}`);
379
+ output.log(`\nExtracting ${filename} to ${outPath}`);
273
380
  let destinationFilename = filename.replace('.zip', '');
274
381
  if (destinationFilename === 'Base')
275
382
  destinationFilename = '';
276
383
 
277
384
  const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
278
- const updater = createThrottledStatusUpdater();
385
+ const updater = output.json ? null : createThrottledStatusUpdater();
279
386
 
280
387
  await extractWithProgress(filePath, destinationPath, {
281
388
  onEntry: (processedEntries, totalEntries, percent) => {
282
- updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
389
+ if (updater) {
390
+ updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
391
+ }
283
392
  }
284
393
  });
285
394
 
286
- updater.stop();
287
- console.log(`Finished extracting ${filename} to ${outPath}\n`);
395
+ if (updater) {
396
+ updater.stop();
397
+ }
398
+ output.log(`Finished extracting ${filename} to ${outPath}\n`);
288
399
 
289
- fs.unlink(filePath, function(err) {});
400
+ fs.unlink(filePath, function(err) {
401
+ if (err) {
402
+ output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`);
403
+ }
404
+ });
290
405
  }
291
406
 
292
407
  async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
@@ -300,14 +415,132 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
300
415
  if (res.ok) {
301
416
  return await res.json();
302
417
  } else {
303
- console.log(res);
304
- console.log(await res.json());
305
- process.exit(1);
418
+ throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
419
+ }
420
+ }
421
+
422
+ async function deleteRemote(env, user, remotePath, isFile, empty, output) {
423
+ let endpoint;
424
+ let mode;
425
+ let data;
426
+
427
+ if (isFile) {
428
+ endpoint = 'FileDelete';
429
+ mode = 'file';
430
+ const parentDir = path.posix.dirname(remotePath);
431
+ data = {
432
+ DirectoryPath: parentDir === '.' ? '/' : parentDir,
433
+ Ids: [remotePath]
434
+ };
435
+ } else if (empty) {
436
+ endpoint = 'DirectoryEmpty';
437
+ mode = 'empty';
438
+ data = { Path: remotePath };
439
+ } else {
440
+ endpoint = 'DirectoryDelete';
441
+ mode = 'directory';
442
+ data = { Path: remotePath };
443
+ }
444
+
445
+ output.log(`${mode === 'empty' ? 'Emptying' : 'Deleting'} ${mode === 'file' ? 'file' : 'directory'}: ${remotePath}`);
446
+
447
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
448
+ method: 'POST',
449
+ body: JSON.stringify(data),
450
+ headers: {
451
+ 'Authorization': `Bearer ${user.apiKey}`,
452
+ 'Content-Type': 'application/json'
453
+ },
454
+ agent: getAgent(env.protocol)
455
+ });
456
+
457
+ if (!res.ok) {
458
+ throw createCommandError(`Failed to ${mode === 'empty' ? 'empty' : 'delete'} "${remotePath}".`, res.status, await parseJsonSafe(res));
306
459
  }
460
+
461
+ const body = await parseJsonSafe(res);
462
+
463
+ output.setStatus(200);
464
+ output.addData({
465
+ type: 'delete',
466
+ path: remotePath,
467
+ mode,
468
+ response: body
469
+ });
470
+
471
+ output.log(`Successfully ${mode === 'empty' ? 'emptied' : 'deleted'}: ${remotePath}`);
307
472
  }
308
473
 
309
- export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
310
- console.log('Uploading files')
474
+ async function copyRemote(env, user, sourcePath, destination, output) {
475
+ output.log(`Copying ${sourcePath} to ${destination}`);
476
+
477
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetCopy`, {
478
+ method: 'POST',
479
+ body: JSON.stringify({
480
+ Destination: destination,
481
+ Ids: [sourcePath]
482
+ }),
483
+ headers: {
484
+ 'Authorization': `Bearer ${user.apiKey}`,
485
+ 'Content-Type': 'application/json'
486
+ },
487
+ agent: getAgent(env.protocol)
488
+ });
489
+
490
+ if (!res.ok) {
491
+ throw createCommandError(`Failed to copy "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
492
+ }
493
+
494
+ const body = await parseJsonSafe(res);
495
+
496
+ output.setStatus(200);
497
+ output.addData({
498
+ type: 'copy',
499
+ sourcePath,
500
+ destination,
501
+ response: body
502
+ });
503
+
504
+ output.log(`Successfully copied ${sourcePath} to ${destination}`);
505
+ }
506
+
507
+ async function moveRemote(env, user, sourcePath, destination, overwrite, output) {
508
+ output.log(`Moving ${sourcePath} to ${destination}`);
509
+
510
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetMove`, {
511
+ method: 'POST',
512
+ body: JSON.stringify({
513
+ Destination: destination,
514
+ Overwrite: Boolean(overwrite),
515
+ Ids: [sourcePath]
516
+ }),
517
+ headers: {
518
+ 'Authorization': `Bearer ${user.apiKey}`,
519
+ 'Content-Type': 'application/json'
520
+ },
521
+ agent: getAgent(env.protocol)
522
+ });
523
+
524
+ if (!res.ok) {
525
+ throw createCommandError(`Failed to move "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
526
+ }
527
+
528
+ const body = await parseJsonSafe(res);
529
+
530
+ output.setStatus(200);
531
+ output.addData({
532
+ type: 'move',
533
+ sourcePath,
534
+ destination,
535
+ overwrite: Boolean(overwrite),
536
+ response: body
537
+ });
538
+
539
+ output.log(`Successfully moved ${sourcePath} to ${destination}`);
540
+ }
541
+
542
+ export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = createFilesOutput({})) {
543
+ output.log('Uploading files')
311
544
 
312
545
  const chunkSize = 300;
313
546
  const chunks = [];
@@ -316,26 +549,37 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
316
549
  chunks.push(localFilePaths.slice(i, i + chunkSize));
317
550
  }
318
551
 
552
+ output.mergeMeta((meta) => ({
553
+ filesProcessed: (meta.filesProcessed || 0) + localFilePaths.length,
554
+ chunks: (meta.chunks || 0) + chunks.length
555
+ }));
556
+
319
557
  for (let i = 0; i < chunks.length; i++) {
320
- console.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
558
+ output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
321
559
 
322
560
  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}`);
561
+ const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output);
562
+ output.addData({
563
+ type: 'upload',
564
+ destinationPath,
565
+ files: chunk.map(filePath => path.resolve(filePath)),
566
+ response: body
567
+ });
568
+
569
+ output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
326
570
  }
327
571
 
328
- console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
572
+ output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
329
573
  }
330
574
 
331
- async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) {
575
+ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
332
576
  const form = new FormData();
333
577
  form.append('path', destinationPath);
334
578
  form.append('skipExistingFiles', String(!overwrite));
335
579
  form.append('allowOverwrite', String(overwrite));
336
580
 
337
581
  filePathsChunk.forEach(fileToUpload => {
338
- console.log(`${fileToUpload}`)
582
+ output.log(`${fileToUpload}`)
339
583
  form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
340
584
  });
341
585
 
@@ -349,12 +593,10 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
349
593
  });
350
594
 
351
595
  if (res.ok) {
352
- console.log(await res.json())
596
+ return await res.json();
353
597
  }
354
598
  else {
355
- console.log(res)
356
- console.log(await res.json())
357
- process.exit(1);
599
+ throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
358
600
  }
359
601
  }
360
602
 
@@ -364,15 +606,111 @@ export function resolveFilePath(filePath) {
364
606
  let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
365
607
  if (resolvedPath === undefined)
366
608
  {
367
- console.log('Could not find any files with the name ' + filePath);
368
- process.exit(1);
609
+ throw createCommandError(`Could not find any files with the name ${filePath}`, 1);
369
610
  }
370
611
  return path.join(p.dir, resolvedPath);
371
612
  }
372
613
 
373
614
 
615
+ function isFilePath(argv, dirPath) {
616
+ if (argv.asFile || argv.asDirectory) {
617
+ return argv.asFile;
618
+ }
619
+ return path.extname(dirPath) !== '';
620
+ }
621
+
374
622
  function wildcardToRegExp(wildcard) {
375
623
  const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
376
624
  return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
377
625
  }
378
626
 
627
+ function createFilesOutput(argv) {
628
+ const response = {
629
+ ok: true,
630
+ command: 'files',
631
+ operation: getFilesOperation(argv),
632
+ status: 200,
633
+ data: [],
634
+ errors: [],
635
+ meta: {}
636
+ };
637
+
638
+ return {
639
+ json: argv.output === 'json' || Boolean(argv.json),
640
+ verbose: Boolean(argv.verbose),
641
+ response,
642
+ log(...args) {
643
+ if (!this.json) {
644
+ console.log(...args);
645
+ }
646
+ },
647
+ verboseLog(...args) {
648
+ if (this.verbose && !this.json) {
649
+ console.info(...args);
650
+ }
651
+ },
652
+ addData(entry) {
653
+ response.data.push(entry);
654
+ },
655
+ mergeMeta(metaOrFn) {
656
+ const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn;
657
+ response.meta = {
658
+ ...response.meta,
659
+ ...meta
660
+ };
661
+ },
662
+ setStatus(status) {
663
+ response.status = status;
664
+ },
665
+ fail(err) {
666
+ response.ok = false;
667
+ response.status = err?.status || 1;
668
+ response.errors.push({
669
+ message: err?.message || 'Unknown files command error.',
670
+ details: err?.details ?? null
671
+ });
672
+ },
673
+ finish() {
674
+ if (this.json) {
675
+ console.log(JSON.stringify(response, null, 2));
676
+ }
677
+ }
678
+ };
679
+ }
680
+
681
+ function getFilesOperation(argv) {
682
+ if (argv.list) {
683
+ return 'list';
684
+ }
685
+
686
+ if (argv.export) {
687
+ return 'export';
688
+ }
689
+
690
+ if (argv.import) {
691
+ return 'import';
692
+ }
693
+
694
+ if (argv.delete) {
695
+ return 'delete';
696
+ }
697
+
698
+ if (argv.copy) {
699
+ return 'copy';
700
+ }
701
+
702
+ if (argv.move) {
703
+ return 'move';
704
+ }
705
+
706
+ return 'unknown';
707
+ }
708
+
709
+
710
+ async function parseJsonSafe(res) {
711
+ try {
712
+ return await res.json();
713
+ } catch {
714
+ return null;
715
+ }
716
+ }