@dynamicweb/cli 1.1.2 → 2.0.0-beta.2

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, argv.verbose);
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, argv.verbose);
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, argv.verbose);
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, argv.verbose);
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,7 +236,7 @@ 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, output = console) {
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
242
  await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
@@ -155,7 +249,7 @@ async function processDirectory(env, user, dirPath, outPath, originalDir, create
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, verbose = false) {
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,30 +306,43 @@ 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, verbose);
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
- function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
345
+ export function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
239
346
  const data = {
240
347
  'DirectoryPath': directoryPath ?? '/',
241
348
  'ExcludeDirectories': [excludeDirectories],
@@ -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,38 @@ 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
+ await fs.promises.unlink(filePath).catch(err => {
401
+ output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`);
402
+ });
290
403
  }
291
404
 
292
405
  async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
@@ -300,14 +413,131 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
300
413
  if (res.ok) {
301
414
  return await res.json();
302
415
  } else {
303
- console.log(res);
304
- console.log(await res.json());
305
- process.exit(1);
416
+ throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
306
417
  }
307
418
  }
308
419
 
309
- export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
310
- output = resolveUploadOutput(output);
420
+ async function deleteRemote(env, user, remotePath, isFile, empty, output) {
421
+ let endpoint;
422
+ let mode;
423
+ let data;
424
+
425
+ if (isFile) {
426
+ endpoint = 'FileDelete';
427
+ mode = 'file';
428
+ const parentDir = path.posix.dirname(remotePath);
429
+ data = {
430
+ DirectoryPath: parentDir === '.' ? '/' : parentDir,
431
+ Ids: [remotePath]
432
+ };
433
+ } else if (empty) {
434
+ endpoint = 'DirectoryEmpty';
435
+ mode = 'empty';
436
+ data = { Path: remotePath };
437
+ } else {
438
+ endpoint = 'DirectoryDelete';
439
+ mode = 'directory';
440
+ data = { Path: remotePath };
441
+ }
442
+
443
+ output.log(`${mode === 'empty' ? 'Emptying' : 'Deleting'} ${mode === 'file' ? 'file' : 'directory'}: ${remotePath}`);
444
+
445
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
446
+ method: 'POST',
447
+ body: JSON.stringify(data),
448
+ headers: {
449
+ 'Authorization': `Bearer ${user.apiKey}`,
450
+ 'Content-Type': 'application/json'
451
+ },
452
+ agent: getAgent(env.protocol)
453
+ });
454
+
455
+ if (!res.ok) {
456
+ throw createCommandError(`Failed to ${mode === 'empty' ? 'empty' : 'delete'} "${remotePath}".`, res.status, await parseJsonSafe(res));
457
+ }
458
+
459
+ const body = await parseJsonSafe(res);
460
+
461
+ output.setStatus(200);
462
+ output.addData({
463
+ type: 'delete',
464
+ path: remotePath,
465
+ mode,
466
+ response: body
467
+ });
468
+
469
+ output.log(`Successfully ${mode === 'empty' ? 'emptied' : 'deleted'}: ${remotePath}`);
470
+ }
471
+
472
+ async function copyRemote(env, user, sourcePath, destination, output) {
473
+ output.log(`Copying ${sourcePath} to ${destination}`);
474
+
475
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetCopy`, {
476
+ method: 'POST',
477
+ body: JSON.stringify({
478
+ Destination: destination,
479
+ Ids: [sourcePath]
480
+ }),
481
+ headers: {
482
+ 'Authorization': `Bearer ${user.apiKey}`,
483
+ 'Content-Type': 'application/json'
484
+ },
485
+ agent: getAgent(env.protocol)
486
+ });
487
+
488
+ if (!res.ok) {
489
+ throw createCommandError(`Failed to copy "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
490
+ }
491
+
492
+ const body = await parseJsonSafe(res);
493
+
494
+ output.setStatus(200);
495
+ output.addData({
496
+ type: 'copy',
497
+ sourcePath,
498
+ destination,
499
+ response: body
500
+ });
501
+
502
+ output.log(`Successfully copied ${sourcePath} to ${destination}`);
503
+ }
504
+
505
+ async function moveRemote(env, user, sourcePath, destination, overwrite, output) {
506
+ output.log(`Moving ${sourcePath} to ${destination}`);
507
+
508
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetMove`, {
509
+ method: 'POST',
510
+ body: JSON.stringify({
511
+ Destination: destination,
512
+ Overwrite: Boolean(overwrite),
513
+ Ids: [sourcePath]
514
+ }),
515
+ headers: {
516
+ 'Authorization': `Bearer ${user.apiKey}`,
517
+ 'Content-Type': 'application/json'
518
+ },
519
+ agent: getAgent(env.protocol)
520
+ });
521
+
522
+ if (!res.ok) {
523
+ throw createCommandError(`Failed to move "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
524
+ }
525
+
526
+ const body = await parseJsonSafe(res);
527
+
528
+ output.setStatus(200);
529
+ output.addData({
530
+ type: 'move',
531
+ sourcePath,
532
+ destination,
533
+ overwrite: Boolean(overwrite),
534
+ response: body
535
+ });
536
+
537
+ output.log(`Successfully moved ${sourcePath} to ${destination}`);
538
+ }
539
+
540
+ export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = createFilesOutput({})) {
311
541
  output.log('Uploading files')
312
542
 
313
543
  const chunkSize = 300;
@@ -317,10 +547,10 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
317
547
  chunks.push(localFilePaths.slice(i, i + chunkSize));
318
548
  }
319
549
 
320
- output.mergeMeta({
321
- filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
322
- chunks: (output.response.meta.chunks || 0) + chunks.length
323
- });
550
+ output.mergeMeta((meta) => ({
551
+ filesProcessed: (meta.filesProcessed || 0) + localFilePaths.length,
552
+ chunks: (meta.chunks || 0) + chunks.length
553
+ }));
324
554
 
325
555
  for (let i = 0; i < chunks.length; i++) {
326
556
  output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
@@ -340,7 +570,7 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
340
570
  output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
341
571
  }
342
572
 
343
- async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output = console) {
573
+ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
344
574
  const form = new FormData();
345
575
  form.append('path', destinationPath);
346
576
  form.append('skipExistingFiles', String(!overwrite));
@@ -361,50 +591,119 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
361
591
  });
362
592
 
363
593
  if (res.ok) {
364
- return await res.json()
594
+ return await res.json();
365
595
  }
366
596
  else {
367
- if (output.structured) {
368
- throw createUploadError('File upload failed.', res.status, await parseJsonSafe(res));
369
- }
597
+ throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
598
+ }
599
+ }
600
+
601
+ export function resolveFilePath(filePath) {
602
+ let p = path.parse(path.resolve(filePath))
603
+ let regex = wildcardToRegExp(p.base);
604
+ let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
605
+ if (resolvedPath === undefined) {
606
+ throw createCommandError(`Could not find any files with the name ${filePath}`, 1);
607
+ }
608
+ return path.join(p.dir, resolvedPath);
609
+ }
610
+
370
611
 
371
- output.log(res)
372
- output.log(await parseJsonSafe(res))
373
- process.exit(1);
612
+ export function isFilePath(argv, dirPath) {
613
+ if (argv.asFile || argv.asDirectory) {
614
+ return Boolean(argv.asFile);
374
615
  }
616
+ return path.extname(dirPath) !== '';
375
617
  }
376
618
 
377
- export function resolveUploadOutput(output) {
378
- const response = output?.response ?? {};
379
- response.meta = response.meta ?? {};
619
+ export function wildcardToRegExp(wildcard) {
620
+ const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
621
+ return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
622
+ }
623
+
624
+ function createFilesOutput(argv) {
625
+ const response = {
626
+ ok: true,
627
+ command: 'files',
628
+ operation: getFilesOperation(argv),
629
+ status: 200,
630
+ data: [],
631
+ errors: [],
632
+ meta: {}
633
+ };
380
634
 
381
635
  return {
382
- structured: Boolean(output),
636
+ json: argv.output === 'json' || Boolean(argv.json),
637
+ verbose: Boolean(argv.verbose),
383
638
  response,
384
- log: typeof output?.log === 'function'
385
- ? output.log.bind(output)
386
- : (...args) => console.log(...args),
387
- addData: typeof output?.addData === 'function'
388
- ? output.addData.bind(output)
389
- : () => { },
390
- mergeMeta: typeof output?.mergeMeta === 'function'
391
- ? output.mergeMeta.bind(output)
392
- : (meta) => {
393
- response.meta = {
394
- ...response.meta,
395
- ...meta
396
- };
639
+ log(...args) {
640
+ if (!this.json) {
641
+ console.log(...args);
642
+ }
643
+ },
644
+ verboseLog(...args) {
645
+ if (this.verbose && !this.json) {
646
+ console.info(...args);
647
+ }
648
+ },
649
+ addData(entry) {
650
+ response.data.push(entry);
651
+ },
652
+ mergeMeta(metaOrFn) {
653
+ const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn;
654
+ response.meta = {
655
+ ...response.meta,
656
+ ...meta
657
+ };
658
+ },
659
+ setStatus(status) {
660
+ response.status = status;
661
+ },
662
+ fail(err) {
663
+ response.ok = false;
664
+ response.status = err?.status || 1;
665
+ response.errors.push({
666
+ message: err?.message || 'Unknown files command error.',
667
+ details: err?.details ?? null
668
+ });
669
+ },
670
+ finish() {
671
+ if (this.json) {
672
+ console.log(JSON.stringify(response, null, 2));
397
673
  }
674
+ }
398
675
  };
399
676
  }
400
677
 
401
- function createUploadError(message, status, details = null) {
402
- const error = new Error(message);
403
- error.status = status;
404
- error.details = details;
405
- return error;
678
+ export function getFilesOperation(argv) {
679
+ if (argv.list) {
680
+ return 'list';
681
+ }
682
+
683
+ if (argv.export) {
684
+ return 'export';
685
+ }
686
+
687
+ if (argv.import) {
688
+ return 'import';
689
+ }
690
+
691
+ if (argv.delete) {
692
+ return 'delete';
693
+ }
694
+
695
+ if (argv.copy) {
696
+ return 'copy';
697
+ }
698
+
699
+ if (argv.move) {
700
+ return 'move';
701
+ }
702
+
703
+ return 'unknown';
406
704
  }
407
705
 
706
+
408
707
  async function parseJsonSafe(res) {
409
708
  try {
410
709
  return await res.json();
@@ -412,19 +711,3 @@ async function parseJsonSafe(res) {
412
711
  return null;
413
712
  }
414
713
  }
415
-
416
- export function resolveFilePath(filePath) {
417
- let p = path.parse(path.resolve(filePath))
418
- let regex = wildcardToRegExp(p.base);
419
- let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
420
- if (resolvedPath === undefined) {
421
- throw new Error('Could not find any files with the name ' + filePath);
422
- }
423
- return path.join(p.dir, resolvedPath);
424
- }
425
-
426
-
427
- function wildcardToRegExp(wildcard) {
428
- const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
429
- return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
430
- }