sfdx-hardis 6.4.1 → 6.4.3

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/defaults/templates/files/LargeFilesOnly.json +11 -0
  3. package/lib/commands/hardis/git/pull-requests/extract.js +1 -1
  4. package/lib/commands/hardis/git/pull-requests/extract.js.map +1 -1
  5. package/lib/commands/hardis/lint/access.js +1 -1
  6. package/lib/commands/hardis/lint/access.js.map +1 -1
  7. package/lib/commands/hardis/lint/metadatastatus.js +1 -1
  8. package/lib/commands/hardis/lint/metadatastatus.js.map +1 -1
  9. package/lib/commands/hardis/lint/missingattributes.js +1 -1
  10. package/lib/commands/hardis/lint/missingattributes.js.map +1 -1
  11. package/lib/commands/hardis/lint/unusedmetadatas.js +1 -1
  12. package/lib/commands/hardis/lint/unusedmetadatas.js.map +1 -1
  13. package/lib/commands/hardis/misc/servicenow-report.js +1 -1
  14. package/lib/commands/hardis/misc/servicenow-report.js.map +1 -1
  15. package/lib/commands/hardis/org/configure/files.js +2 -0
  16. package/lib/commands/hardis/org/configure/files.js.map +1 -1
  17. package/lib/commands/hardis/org/diagnose/audittrail.js +1 -1
  18. package/lib/commands/hardis/org/diagnose/audittrail.js.map +1 -1
  19. package/lib/commands/hardis/org/diagnose/legacyapi.js +2 -2
  20. package/lib/commands/hardis/org/diagnose/legacyapi.js.map +1 -1
  21. package/lib/commands/hardis/org/diagnose/licenses.js +1 -1
  22. package/lib/commands/hardis/org/diagnose/licenses.js.map +1 -1
  23. package/lib/commands/hardis/org/diagnose/releaseupdates.js +1 -1
  24. package/lib/commands/hardis/org/diagnose/releaseupdates.js.map +1 -1
  25. package/lib/commands/hardis/org/diagnose/unused-apex-classes.js +1 -1
  26. package/lib/commands/hardis/org/diagnose/unused-apex-classes.js.map +1 -1
  27. package/lib/commands/hardis/org/diagnose/unused-connected-apps.js +1 -1
  28. package/lib/commands/hardis/org/diagnose/unused-connected-apps.js.map +1 -1
  29. package/lib/commands/hardis/org/files/export.js +14 -2
  30. package/lib/commands/hardis/org/files/export.js.map +1 -1
  31. package/lib/commands/hardis/org/files/import.js +4 -2
  32. package/lib/commands/hardis/org/files/import.js.map +1 -1
  33. package/lib/commands/hardis/org/monitor/backup.js +1 -1
  34. package/lib/commands/hardis/org/monitor/backup.js.map +1 -1
  35. package/lib/commands/hardis/org/monitor/limits.js +1 -1
  36. package/lib/commands/hardis/org/monitor/limits.js.map +1 -1
  37. package/lib/commands/hardis/org/multi-org-query.js +1 -1
  38. package/lib/commands/hardis/org/multi-org-query.js.map +1 -1
  39. package/lib/commands/hardis/project/audit/callincallout.js +4 -1
  40. package/lib/commands/hardis/project/audit/callincallout.js.map +1 -1
  41. package/lib/commands/hardis/project/audit/remotesites.js +4 -1
  42. package/lib/commands/hardis/project/audit/remotesites.js.map +1 -1
  43. package/lib/commands/hardis/work/save.js +1 -1
  44. package/lib/commands/hardis/work/save.js.map +1 -1
  45. package/lib/common/utils/filesUtils.d.ts +52 -14
  46. package/lib/common/utils/filesUtils.js +456 -101
  47. package/lib/common/utils/filesUtils.js.map +1 -1
  48. package/lib/common/utils/index.d.ts +4 -0
  49. package/lib/common/utils/index.js +9 -0
  50. package/lib/common/utils/index.js.map +1 -1
  51. package/lib/common/websocketClient.d.ts +3 -0
  52. package/lib/common/websocketClient.js +23 -0
  53. package/lib/common/websocketClient.js.map +1 -1
  54. package/oclif.lock +39 -39
  55. package/oclif.manifest.json +497 -497
  56. package/package.json +6 -6
@@ -34,6 +34,7 @@ export class FilesExporter {
34
34
  queueInterval;
35
35
  bulkApiRecordsEnded = false;
36
36
  recordChunksNumber = 0;
37
+ logFile;
37
38
  totalSoqlRequests = 0;
38
39
  totalParentRecords = 0;
39
40
  parentRecordsWithFiles = 0;
@@ -42,6 +43,7 @@ export class FilesExporter {
42
43
  filesErrors = 0;
43
44
  filesIgnoredType = 0;
44
45
  filesIgnoredExisting = 0;
46
+ filesIgnoredSize = 0;
45
47
  apiUsedBefore = 0;
46
48
  apiLimit = 0;
47
49
  constructor(filesPath, conn, options, commandThis) {
@@ -61,16 +63,109 @@ export class FilesExporter {
61
63
  if (this.dtl === null) {
62
64
  this.dtl = await getFilesWorkspaceDetail(this.filesPath);
63
65
  }
64
- uxLog("action", this.commandThis, c.cyan(`Exporting files from ${c.green(this.dtl.full_label)} ...`));
66
+ uxLog("action", this.commandThis, c.cyan(`Initializing files export using workspace ${c.green(this.dtl.full_label)} ...`));
65
67
  uxLog("log", this.commandThis, c.italic(c.grey(this.dtl.description)));
66
68
  // Make sure export folder for files is existing
67
69
  this.exportedFilesFolder = path.join(this.filesPath, 'export');
68
70
  await fs.ensureDir(this.exportedFilesFolder);
69
71
  await this.calculateApiConsumption();
70
- this.startQueue();
71
- await this.processParentRecords();
72
+ const reportDir = await getReportDirectory();
73
+ const reportExportDir = path.join(reportDir, 'files-export-log');
74
+ const now = new Date();
75
+ const dateStr = now.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '');
76
+ this.logFile = path.join(reportExportDir, `files-export-log-${this.dtl.name}-${dateStr}.csv`);
77
+ // Initialize CSV log file with headers
78
+ await this.initializeCsvLog();
79
+ // Phase 1: Calculate total files count for accurate progress tracking
80
+ uxLog("action", this.commandThis, c.cyan("Estimating total files to download..."));
81
+ const totalFilesCount = await this.calculateTotalFilesCount();
82
+ uxLog("log", this.commandThis, c.grey(`Estimated ${totalFilesCount} files to download`));
83
+ // Phase 2: Process downloads with accurate progress tracking
84
+ await this.processDownloadsWithProgress(totalFilesCount);
85
+ const result = await this.buildResult();
86
+ return result;
87
+ }
88
+ // Phase 1: Calculate total files count using efficient COUNT() queries
89
+ async calculateTotalFilesCount() {
90
+ let totalFiles = 0;
91
+ // Get parent records count to estimate batching
92
+ const countSoqlQuery = this.dtl.soqlQuery.replace(/SELECT (.*) FROM/gi, 'SELECT COUNT() FROM');
93
+ this.totalSoqlRequests++;
94
+ const countSoqlQueryRes = await soqlQuery(countSoqlQuery, this.conn);
95
+ const totalParentRecords = countSoqlQueryRes.totalSize;
96
+ // Count Attachments - use COUNT() query with IN clause batching for memory efficiency
97
+ const attachmentBatchSize = 200;
98
+ // Estimate Attachments count by sampling
99
+ const sampleSize = Math.min(attachmentBatchSize, totalParentRecords);
100
+ if (sampleSize > 0) {
101
+ // Get sample of parent IDs
102
+ const sampleQuery = this.dtl.soqlQuery.replace(/SELECT (.*) FROM/gi, 'SELECT Id FROM') + ` LIMIT ${sampleSize}`;
103
+ this.totalSoqlRequests++;
104
+ const sampleParents = await soqlQuery(sampleQuery, this.conn);
105
+ if (sampleParents.records.length > 0) {
106
+ const sampleParentIds = sampleParents.records.map((record) => `'${record.Id}'`).join(',');
107
+ const attachmentCountQuery = `SELECT COUNT() FROM Attachment WHERE ParentId IN (${sampleParentIds})`;
108
+ this.totalSoqlRequests++;
109
+ const attachmentCountRes = await soqlQuery(attachmentCountQuery, this.conn);
110
+ // Extrapolate from sample
111
+ const avgAttachmentsPerRecord = attachmentCountRes.totalSize / sampleParents.records.length;
112
+ totalFiles += Math.round(avgAttachmentsPerRecord * totalParentRecords);
113
+ }
114
+ }
115
+ // Count ContentVersions - use COUNT() query with sampling for memory efficiency
116
+ if (sampleSize > 0) {
117
+ const sampleQuery = this.dtl.soqlQuery.replace(/SELECT (.*) FROM/gi, 'SELECT Id FROM') + ` LIMIT ${sampleSize}`;
118
+ const sampleParents = await soqlQuery(sampleQuery, this.conn);
119
+ if (sampleParents.records.length > 0) {
120
+ const sampleParentIds = sampleParents.records.map((record) => `'${record.Id}'`).join(',');
121
+ // Count ContentDocumentLinks for sample
122
+ const linkCountQuery = `SELECT COUNT() FROM ContentDocumentLink WHERE LinkedEntityId IN (${sampleParentIds})`;
123
+ this.totalSoqlRequests++;
124
+ const linkCountRes = await soqlQuery(linkCountQuery, this.conn);
125
+ // Extrapolate from sample (ContentVersions ≈ ContentDocumentLinks for latest versions)
126
+ const avgContentVersionsPerRecord = linkCountRes.totalSize / sampleParents.records.length;
127
+ totalFiles += Math.round(avgContentVersionsPerRecord * totalParentRecords);
128
+ }
129
+ }
130
+ return Math.max(totalFiles, 1); // Ensure at least 1 for progress tracking
131
+ }
132
+ // Phase 2: Process downloads with accurate file-based progress tracking
133
+ async processDownloadsWithProgress(estimatedFilesCount) {
134
+ let filesProcessed = 0;
135
+ let totalFilesDiscovered = 0; // Track actual files discovered
136
+ let actualTotalFiles = estimatedFilesCount; // Start with estimation, will be adjusted as we discover actual files
137
+ // Start progress tracking with estimated total files count
138
+ WebSocketClient.sendProgressStartMessage('Exporting files', actualTotalFiles);
139
+ // Progress callback function with total adjustment capability
140
+ const progressCallback = (filesCompleted, filesDiscoveredInChunk) => {
141
+ filesProcessed += filesCompleted;
142
+ // If we discovered files in this chunk, update our tracking
143
+ if (filesDiscoveredInChunk !== undefined) {
144
+ totalFilesDiscovered += filesDiscoveredInChunk;
145
+ // Update total to use actual discovered count + remaining estimation
146
+ const processedChunks = this.recordChunksNumber;
147
+ const totalChunks = this.chunksNumber;
148
+ const remainingChunks = totalChunks - processedChunks;
149
+ if (remainingChunks > 0) {
150
+ // Estimate remaining files based on actual discovery rate
151
+ const avgFilesPerChunk = totalFilesDiscovered / processedChunks;
152
+ const estimatedRemainingFiles = Math.round(avgFilesPerChunk * remainingChunks);
153
+ actualTotalFiles = totalFilesDiscovered + estimatedRemainingFiles;
154
+ }
155
+ else {
156
+ // All chunks processed, use actual total
157
+ actualTotalFiles = totalFilesDiscovered;
158
+ }
159
+ uxLog("other", this, c.grey(`Discovered ${filesDiscoveredInChunk} files in chunk, updated total estimate to ${actualTotalFiles}`));
160
+ }
161
+ WebSocketClient.sendProgressStepMessage(filesProcessed, actualTotalFiles);
162
+ };
163
+ // Use modified queue system with progress tracking
164
+ this.startQueue(progressCallback);
165
+ await this.processParentRecords(progressCallback);
72
166
  await this.queueCompleted();
73
- return await this.buildResult();
167
+ // End progress tracking with final total
168
+ WebSocketClient.sendProgressEndMessage(actualTotalFiles);
74
169
  }
75
170
  // Calculate API consumption
76
171
  async calculateApiConsumption() {
@@ -104,12 +199,15 @@ export class FilesExporter {
104
199
  }
105
200
  }
106
201
  // Run chunks one by one, and don't wait to have all the records fetched to start it
107
- startQueue() {
202
+ startQueue(progressCallback) {
108
203
  this.queueInterval = setInterval(async () => {
109
204
  if (this.recordsChunkQueueRunning === false && this.recordsChunkQueue.length > 0) {
110
205
  this.recordsChunkQueueRunning = true;
111
- const recordChunk = this.recordsChunkQueue.shift();
112
- await this.processRecordsChunk(recordChunk);
206
+ const queueItem = this.recordsChunkQueue.shift();
207
+ // Handle both old format (array) and new format (object with records and progressCallback)
208
+ const recordChunk = Array.isArray(queueItem) ? queueItem : queueItem.records;
209
+ const chunkProgressCallback = Array.isArray(queueItem) ? progressCallback : queueItem.progressCallback;
210
+ await this.processRecordsChunk(recordChunk, chunkProgressCallback);
113
211
  this.recordsChunkQueueRunning = false;
114
212
  // Manage last chunk
115
213
  }
@@ -118,7 +216,7 @@ export class FilesExporter {
118
216
  this.recordsChunk.length > 0) {
119
217
  const recordsToProcess = [...this.recordsChunk];
120
218
  this.recordsChunk = [];
121
- this.recordsChunkQueue.push(recordsToProcess);
219
+ this.recordsChunkQueue.push({ records: recordsToProcess, progressCallback });
122
220
  }
123
221
  }, 1000);
124
222
  }
@@ -142,7 +240,7 @@ export class FilesExporter {
142
240
  clearInterval(this.queueInterval);
143
241
  this.queueInterval = null;
144
242
  }
145
- async processParentRecords() {
243
+ async processParentRecords(progressCallback) {
146
244
  // Query parent records using SOQL defined in export.json file
147
245
  this.totalSoqlRequests++;
148
246
  this.conn.bulk.pollTimeout = this.pollTimeout || 600000; // Increase timeout in case we are on a bad internet connection or if the bulk api batch is queued
@@ -156,25 +254,26 @@ export class FilesExporter {
156
254
  this.recordsIgnored++;
157
255
  continue;
158
256
  }
159
- await this.addToRecordsChunk(record);
257
+ await this.addToRecordsChunk(record, progressCallback);
160
258
  }
161
259
  this.bulkApiRecordsEnded = true;
162
260
  }
163
- async addToRecordsChunk(record) {
261
+ async addToRecordsChunk(record, progressCallback) {
164
262
  this.recordsChunk.push(record);
165
263
  // If chunk size is reached , process the chunk of records
166
264
  if (this.recordsChunk.length === this.recordsChunkSize) {
167
265
  const recordsToProcess = [...this.recordsChunk];
168
266
  this.recordsChunk = [];
169
- this.recordsChunkQueue.push(recordsToProcess);
267
+ this.recordsChunkQueue.push({ records: recordsToProcess, progressCallback });
170
268
  }
171
269
  }
172
- async processRecordsChunk(records) {
270
+ async processRecordsChunk(records, progressCallback) {
173
271
  this.recordChunksNumber++;
174
272
  if (this.recordChunksNumber < this.startChunkNumber) {
175
273
  uxLog("action", this, c.cyan(`Skip parent records chunk #${this.recordChunksNumber} because it is lesser than ${this.startChunkNumber}`));
176
274
  return;
177
275
  }
276
+ let actualFilesInChunk = 0;
178
277
  uxLog("action", this, c.cyan(`Processing parent records chunk #${this.recordChunksNumber} on ${this.chunksNumber} (${records.length} records) ...`));
179
278
  // Process records in batches of 200 for Attachments and 1000 for ContentVersions to avoid hitting the SOQL query limit
180
279
  const attachmentBatchSize = 200;
@@ -183,9 +282,10 @@ export class FilesExporter {
183
282
  const batch = records.slice(i, i + attachmentBatchSize);
184
283
  // Request all Attachment related to all records of the batch using REST API
185
284
  const parentIdIn = batch.map((record) => `'${record.Id}'`).join(',');
186
- const attachmentQuery = `SELECT Id, Name, ContentType, ParentId FROM Attachment WHERE ParentId IN (${parentIdIn})`;
285
+ const attachmentQuery = `SELECT Id, Name, ContentType, ParentId, BodyLength FROM Attachment WHERE ParentId IN (${parentIdIn})`;
187
286
  this.totalSoqlRequests++;
188
287
  const attachments = await this.conn.query(attachmentQuery);
288
+ actualFilesInChunk += attachments.records.length; // Count actual files discovered
189
289
  if (attachments.records.length > 0) {
190
290
  // Download attachments using REST API
191
291
  await PromisePool.withConcurrency(5)
@@ -193,10 +293,14 @@ export class FilesExporter {
193
293
  .process(async (attachment) => {
194
294
  try {
195
295
  await this.downloadAttachmentFile(attachment, batch);
296
+ // Call progress callback if available
297
+ if (progressCallback) {
298
+ progressCallback(1);
299
+ }
196
300
  }
197
301
  catch (e) {
198
302
  this.filesErrors++;
199
- uxLog("error", this, c.red('Download file error: ' + attachment.Name + '\n' + e));
303
+ uxLog("warning", this, c.red('Download file error: ' + attachment.Name + '\n' + e));
200
304
  }
201
305
  });
202
306
  }
@@ -220,7 +324,7 @@ export class FilesExporter {
220
324
  // Log the progression of contentDocIdBatch
221
325
  uxLog("action", this, c.cyan(`Processing ContentDocumentId chunk #${Math.ceil((j + 1) / contentVersionBatchSize)} on ${Math.ceil(contentDocIdIn.length / contentVersionBatchSize)}`));
222
326
  // Request all ContentVersion related to all records of the batch
223
- const contentVersionSoql = `SELECT Id,ContentDocumentId,Description,FileExtension,FileType,PathOnClient,Title FROM ContentVersion WHERE ContentDocumentId IN (${contentDocIdBatch}) AND IsLatest = true`;
327
+ const contentVersionSoql = `SELECT Id,ContentDocumentId,Description,FileExtension,FileType,PathOnClient,Title,ContentSize FROM ContentVersion WHERE ContentDocumentId IN (${contentDocIdBatch}) AND IsLatest = true`;
224
328
  this.totalSoqlRequests++;
225
329
  const contentVersions = await bulkQueryByChunks(contentVersionSoql, this.conn, this.parentRecordsChunkSize);
226
330
  // ContentDocument object can be linked to multiple other objects even with same type (for example: same attachment can be linked to multiple EmailMessage objects).
@@ -238,6 +342,7 @@ export class FilesExporter {
238
342
  }
239
343
  });
240
344
  });
345
+ actualFilesInChunk += versionsAndLinks.length; // Count actual ContentVersion files discovered
241
346
  uxLog("log", this, c.grey(`Downloading ${versionsAndLinks.length} found files...`));
242
347
  // Download files
243
348
  await PromisePool.withConcurrency(5)
@@ -245,10 +350,14 @@ export class FilesExporter {
245
350
  .process(async (versionAndLink) => {
246
351
  try {
247
352
  await this.downloadContentVersionFile(versionAndLink.contentVersion, batch, versionAndLink.contentDocumentLink);
353
+ // Call progress callback if available
354
+ if (progressCallback) {
355
+ progressCallback(1);
356
+ }
248
357
  }
249
358
  catch (e) {
250
359
  this.filesErrors++;
251
- uxLog("error", this, c.red('Download file error: ' + versionAndLink.contentVersion.Title + '\n' + e));
360
+ uxLog("warning", this, c.red('Download file error: ' + versionAndLink.contentVersion.Title + '\n' + e));
252
361
  }
253
362
  });
254
363
  }
@@ -257,17 +366,112 @@ export class FilesExporter {
257
366
  uxLog("log", this, c.grey('No ContentDocumentLinks found for the parent records in this batch'));
258
367
  }
259
368
  }
369
+ // At the end of chunk processing, report the actual files discovered in this chunk
370
+ if (progressCallback && actualFilesInChunk > 0) {
371
+ // This will help adjust the total progress based on actual discovered files
372
+ progressCallback(0, actualFilesInChunk); // Report actual files found in this chunk
373
+ }
374
+ }
375
+ // Initialize CSV log file with headers
376
+ async initializeCsvLog() {
377
+ await fs.ensureDir(path.dirname(this.logFile));
378
+ const headers = 'Status,Folder,File Name,Extension,File Size (KB),Error Detail,ContentDocument Id,ContentVersion Id,Attachment Id\n';
379
+ await fs.writeFile(this.logFile, headers, 'utf8');
380
+ uxLog("log", this, c.grey(`CSV log file initialized: ${this.logFile}`));
381
+ WebSocketClient.sendReportFileMessage(this.logFile, "Exported files report (CSV)", 'report');
382
+ }
383
+ // Helper method to extract file information from output path
384
+ extractFileInfo(outputFile) {
385
+ const fileName = path.basename(outputFile);
386
+ const extension = path.extname(fileName);
387
+ const folderPath = path.dirname(outputFile)
388
+ .replace(process.cwd(), '')
389
+ .replace(this.exportedFilesFolder, '')
390
+ .replace(/\\/g, '/')
391
+ .replace(/^\/+/, '');
392
+ return { fileName, extension, folderPath };
260
393
  }
261
- async downloadFile(fetchUrl, outputFile) {
394
+ // Helper method to log skipped files
395
+ async logSkippedFile(outputFile, errorDetail, contentDocumentId = '', contentVersionId = '', attachmentId = '') {
396
+ const { fileName, extension, folderPath } = this.extractFileInfo(outputFile);
397
+ await this.writeCsvLogEntry('skipped', folderPath, fileName, extension, 0, errorDetail, contentDocumentId, contentVersionId, attachmentId);
398
+ }
399
+ // Write a CSV entry for each file processed (fileSize in KB)
400
+ async writeCsvLogEntry(status, folder, fileName, extension, fileSizeKB, errorDetail = '', contentDocumentId = '', contentVersionId = '', attachmentId = '') {
401
+ try {
402
+ // Escape CSV values to handle commas, quotes, and newlines
403
+ const escapeCsvValue = (value) => {
404
+ const strValue = String(value);
405
+ if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
406
+ return `"${strValue.replace(/"/g, '""')}"`;
407
+ }
408
+ return strValue;
409
+ };
410
+ const csvLine = [
411
+ escapeCsvValue(status),
412
+ escapeCsvValue(folder),
413
+ escapeCsvValue(fileName),
414
+ escapeCsvValue(extension),
415
+ escapeCsvValue(fileSizeKB),
416
+ escapeCsvValue(errorDetail),
417
+ escapeCsvValue(contentDocumentId),
418
+ escapeCsvValue(contentVersionId),
419
+ escapeCsvValue(attachmentId)
420
+ ].join(',') + '\n';
421
+ await fs.appendFile(this.logFile, csvLine, 'utf8');
422
+ }
423
+ catch (e) {
424
+ uxLog("warning", this, c.yellow(`Error writing to CSV log: ${e.message}`));
425
+ }
426
+ }
427
+ async downloadFile(fetchUrl, outputFile, contentDocumentId = '', contentVersionId = '', attachmentId = '') {
262
428
  const downloadResult = await new FileDownloader(fetchUrl, { conn: this.conn, outputFile: outputFile, label: 'file' }).download();
429
+ // Extract file information for CSV logging
430
+ const { fileName, extension, folderPath } = this.extractFileInfo(outputFile);
431
+ let fileSizeKB = 0;
432
+ let errorDetail = '';
433
+ // Get file size if download was successful
434
+ if (downloadResult.success && fs.existsSync(outputFile)) {
435
+ try {
436
+ const stats = await fs.stat(outputFile);
437
+ fileSizeKB = Math.round(stats.size / 1024); // Convert bytes to KB
438
+ }
439
+ catch (e) {
440
+ uxLog("warning", this, c.yellow(`Could not get file size for ${fileName}: ${e.message}`));
441
+ }
442
+ }
443
+ else if (!downloadResult.success) {
444
+ errorDetail = downloadResult.error || 'Unknown download error';
445
+ }
446
+ // Use file folder and file name for log display
447
+ const fileDisplay = path.join(folderPath, fileName).replace(/\\/g, '/');
263
448
  if (downloadResult.success) {
449
+ uxLog("success", this, c.grey(`Downloaded ${fileDisplay}`));
264
450
  this.filesDownloaded++;
451
+ // Write success entry to CSV log with Salesforce IDs
452
+ await this.writeCsvLogEntry('success', folderPath, fileName, extension, fileSizeKB, '', contentDocumentId, contentVersionId, attachmentId);
265
453
  }
266
454
  else {
455
+ uxLog("warning", this, c.red(`Error ${fileDisplay}`));
267
456
  this.filesErrors++;
457
+ // Write failed entry to CSV log with Salesforce IDs
458
+ await this.writeCsvLogEntry('failed', folderPath, fileName, extension, fileSizeKB, errorDetail, contentDocumentId, contentVersionId, attachmentId);
268
459
  }
269
460
  }
270
461
  async downloadAttachmentFile(attachment, records) {
462
+ // Check file size filter (BodyLength is in bytes)
463
+ const fileSizeKB = attachment.BodyLength ? Math.round(attachment.BodyLength / 1024) : 0;
464
+ if (this.dtl.fileSizeMin && this.dtl.fileSizeMin > 0 && fileSizeKB < this.dtl.fileSizeMin) {
465
+ uxLog("log", this, c.grey(`Skipped - ${attachment.Name} - File size (${fileSizeKB} KB) below minimum (${this.dtl.fileSizeMin} KB)`));
466
+ this.filesIgnoredSize++;
467
+ // Log skipped file to CSV
468
+ const parentAttachment = records.filter((record) => record.Id === attachment.ParentId)[0];
469
+ const attachmentParentFolderName = (parentAttachment[this.dtl.outputFolderNameField] || parentAttachment.Id).replace(/[/\\?%*:|"<>]/g, '-');
470
+ const parentRecordFolderForFiles = path.resolve(path.join(this.exportedFilesFolder, attachmentParentFolderName));
471
+ const outputFile = path.join(parentRecordFolderForFiles, attachment.Name.replace(/[/\\?%*:|"<>]/g, '-'));
472
+ await this.logSkippedFile(outputFile, `File size (${fileSizeKB} KB) below minimum (${this.dtl.fileSizeMin} KB)`, '', '', attachment.Id);
473
+ return;
474
+ }
271
475
  // Retrieve initial record to build output files folder name
272
476
  const parentAttachment = records.filter((record) => record.Id === attachment.ParentId)[0];
273
477
  // Build record output files folder (if folder name contains slashes or antislashes, replace them by spaces)
@@ -279,9 +483,22 @@ export class FilesExporter {
279
483
  await fs.ensureDir(parentRecordFolderForFiles);
280
484
  // Download file locally
281
485
  const fetchUrl = `${this.conn.instanceUrl}/services/data/v${getApiVersion()}/sobjects/Attachment/${attachment.Id}/Body`;
282
- await this.downloadFile(fetchUrl, outputFile);
486
+ await this.downloadFile(fetchUrl, outputFile, '', '', attachment.Id);
283
487
  }
284
488
  async downloadContentVersionFile(contentVersion, records, contentDocumentLink) {
489
+ // Check file size filter (ContentSize is in bytes)
490
+ const fileSizeKB = contentVersion.ContentSize ? Math.round(contentVersion.ContentSize / 1024) : 0;
491
+ if (this.dtl.fileSizeMin && this.dtl.fileSizeMin > 0 && fileSizeKB < this.dtl.fileSizeMin) {
492
+ uxLog("log", this, c.grey(`Skipped - ${contentVersion.Title} - File size (${fileSizeKB} KB) below minimum (${this.dtl.fileSizeMin} KB)`));
493
+ this.filesIgnoredSize++;
494
+ // Log skipped file to CSV
495
+ const parentRecord = records.filter((record) => record.Id === contentDocumentLink.LinkedEntityId)[0];
496
+ const parentFolderName = (parentRecord[this.dtl.outputFolderNameField] || parentRecord.Id).replace(/[/\\?%*:|"<>]/g, '-');
497
+ const parentRecordFolderForFiles = path.resolve(path.join(this.exportedFilesFolder, parentFolderName));
498
+ const outputFile = path.join(parentRecordFolderForFiles, contentVersion.Title.replace(/[/\\?%*:|"<>]/g, '-'));
499
+ await this.logSkippedFile(outputFile, `File size (${fileSizeKB} KB) below minimum (${this.dtl.fileSizeMin} KB)`, contentVersion.ContentDocumentId, contentVersion.Id);
500
+ return;
501
+ }
285
502
  // Retrieve initial record to build output files folder name
286
503
  const parentRecord = records.filter((record) => record.Id === contentDocumentLink.LinkedEntityId)[0];
287
504
  // Build record output files folder (if folder name contains slashes or antislashes, replace them by spaces)
@@ -309,19 +526,23 @@ export class FilesExporter {
309
526
  if (this.dtl.fileTypes !== 'all' && !this.dtl.fileTypes.includes(contentVersion.FileType)) {
310
527
  uxLog("log", this, c.grey(`Skipped - ${outputFile.replace(this.exportedFilesFolder, '')} - File type ignored`));
311
528
  this.filesIgnoredType++;
529
+ // Log skipped file to CSV
530
+ await this.logSkippedFile(outputFile, 'File type ignored', contentVersion.ContentDocumentId, contentVersion.Id);
312
531
  return;
313
532
  }
314
533
  // Check file overwrite
315
534
  if (this.dtl.overwriteFiles !== true && fs.existsSync(outputFile)) {
316
535
  uxLog("warning", this, c.yellow(`Skipped - ${outputFile.replace(this.exportedFilesFolder, '')} - File already existing`));
317
536
  this.filesIgnoredExisting++;
537
+ // Log skipped file to CSV
538
+ await this.logSkippedFile(outputFile, 'File already exists', contentVersion.ContentDocumentId, contentVersion.Id);
318
539
  return;
319
540
  }
320
541
  // Create directory if not existing
321
542
  await fs.ensureDir(parentRecordFolderForFiles);
322
543
  // Download file locally
323
544
  const fetchUrl = `${this.conn.instanceUrl}/services/data/v${getApiVersion()}/sobjects/ContentVersion/${contentVersion.Id}/VersionData`;
324
- await this.downloadFile(fetchUrl, outputFile);
545
+ await this.downloadFile(fetchUrl, outputFile, contentVersion.ContentDocumentId, contentVersion.Id);
325
546
  }
326
547
  // Build stats & result
327
548
  async buildResult() {
@@ -329,31 +550,26 @@ export class FilesExporter {
329
550
  const apiCallsRemaining = connAny?.limitInfo?.apiUsage?.used
330
551
  ? (connAny?.limitInfo?.apiUsage?.limit || 0) - (connAny?.limitInfo?.apiUsage?.used || 0)
331
552
  : null;
332
- uxLog("action", this, c.cyan(`API limit: ${c.bold(connAny?.limitInfo?.apiUsage?.limit || null)}`));
333
- uxLog("action", this, c.cyan(`API used before process: ${c.bold(this.apiUsedBefore)}`));
334
- uxLog("action", this, c.cyan(`API used after process: ${c.bold(connAny?.limitInfo?.apiUsage?.used || null)}`));
335
- uxLog("action", this, c.cyan(`API calls remaining for today: ${c.bold(apiCallsRemaining)}`));
336
- uxLog("action", this, c.cyan(`Total SOQL requests: ${c.bold(this.totalSoqlRequests)}`));
337
- uxLog("action", this, c.cyan(`Total parent records found: ${c.bold(this.totalParentRecords)}`));
338
- uxLog("action", this, c.cyan(`Total parent records with files: ${c.bold(this.parentRecordsWithFiles)}`));
339
- uxLog("action", this, c.cyan(`Total parent records ignored because already existing: ${c.bold(this.recordsIgnored)}`));
340
- uxLog("action", this, c.cyan(`Total files downloaded: ${c.bold(this.filesDownloaded)}`));
341
- uxLog("action", this, c.cyan(`Total file download errors: ${c.bold(this.filesErrors)}`));
342
- uxLog("action", this, c.cyan(`Total file skipped because of type constraint: ${c.bold(this.filesIgnoredType)}`));
343
- uxLog("action", this, c.cyan(`Total file skipped because previously downloaded: ${c.bold(this.filesIgnoredExisting)}`));
344
- return {
345
- totalParentRecords: this.totalParentRecords,
346
- parentRecordsWithFiles: this.parentRecordsWithFiles,
347
- filesDownloaded: this.filesDownloaded,
348
- filesErrors: this.filesErrors,
349
- recordsIgnored: this.recordsIgnored,
350
- filesIgnoredType: this.filesIgnoredType,
351
- filesIgnoredExisting: this.filesIgnoredExisting,
352
- apiLimit: connAny?.limitInfo?.apiUsage?.limit || null,
353
- apiUsedBefore: this.apiUsedBefore,
354
- apiUsedAfter: connAny?.limitInfo?.apiUsage?.used || null,
355
- apiCallsRemaining,
553
+ const result = {
554
+ stats: {
555
+ filesDownloaded: this.filesDownloaded,
556
+ filesErrors: this.filesErrors,
557
+ filesIgnoredType: this.filesIgnoredType,
558
+ filesIgnoredExisting: this.filesIgnoredExisting,
559
+ filesIgnoredSize: this.filesIgnoredSize,
560
+ totalSoqlRequests: this.totalSoqlRequests,
561
+ totalParentRecords: this.totalParentRecords,
562
+ parentRecordsWithFiles: this.parentRecordsWithFiles,
563
+ recordsIgnored: this.recordsIgnored,
564
+ apiLimit: connAny?.limitInfo?.apiUsage?.limit || null,
565
+ apiUsedBefore: this.apiUsedBefore,
566
+ apiUsedAfter: connAny?.limitInfo?.apiUsage?.used || null,
567
+ apiCallsRemaining,
568
+ },
569
+ logFile: this.logFile
356
570
  };
571
+ await createXlsxFromCsv(this.logFile, { fileTitle: "Exported files report" }, result);
572
+ return result;
357
573
  }
358
574
  }
359
575
  export class FilesImporter {
@@ -363,6 +579,16 @@ export class FilesImporter {
363
579
  dtl = null; // export config
364
580
  exportedFilesFolder = '';
365
581
  handleOverwrite = false;
582
+ logFile;
583
+ // Statistics tracking
584
+ totalFolders = 0;
585
+ totalFiles = 0;
586
+ filesUploaded = 0;
587
+ filesOverwritten = 0;
588
+ filesErrors = 0;
589
+ filesSkipped = 0;
590
+ apiUsedBefore = 0;
591
+ apiLimit = 0;
366
592
  constructor(filesPath, conn, options, commandThis) {
367
593
  this.filesPath = filesPath;
368
594
  this.exportedFilesFolder = path.join(this.filesPath, 'export');
@@ -372,6 +598,49 @@ export class FilesImporter {
372
598
  if (options.exportConfig) {
373
599
  this.dtl = options.exportConfig;
374
600
  }
601
+ // Initialize log file path
602
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
603
+ this.logFile = path.join(this.filesPath, `import-log-${timestamp}.csv`);
604
+ }
605
+ // Initialize CSV log file with headers
606
+ async initializeCsvLog() {
607
+ await fs.ensureDir(path.dirname(this.logFile));
608
+ const headers = 'Status,Folder,File Name,Extension,File Size (KB),Error Detail,ContentVersion Id\n';
609
+ await fs.writeFile(this.logFile, headers, 'utf8');
610
+ uxLog("log", this.commandThis, c.grey(`CSV log file initialized: ${this.logFile}`));
611
+ WebSocketClient.sendReportFileMessage(this.logFile, "Imported files report (CSV)", 'report');
612
+ }
613
+ // Helper method to extract file information from file path
614
+ extractFileInfo(filePath, folderName) {
615
+ const fileName = path.basename(filePath);
616
+ const extension = path.extname(fileName);
617
+ return { fileName, extension, folderPath: folderName };
618
+ }
619
+ // Write a CSV entry for each file processed (fileSize in KB)
620
+ async writeCsvLogEntry(status, folder, fileName, extension, fileSizeKB, errorDetail = '', contentVersionId = '') {
621
+ try {
622
+ // Escape CSV values to handle commas, quotes, and newlines
623
+ const escapeCsvValue = (value) => {
624
+ const strValue = String(value);
625
+ if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
626
+ return `"${strValue.replace(/"/g, '""')}"`;
627
+ }
628
+ return strValue;
629
+ };
630
+ const csvLine = [
631
+ escapeCsvValue(status),
632
+ escapeCsvValue(folder),
633
+ escapeCsvValue(fileName),
634
+ escapeCsvValue(extension),
635
+ escapeCsvValue(fileSizeKB),
636
+ escapeCsvValue(errorDetail),
637
+ escapeCsvValue(contentVersionId)
638
+ ].join(',') + '\n';
639
+ await fs.appendFile(this.logFile, csvLine, 'utf8');
640
+ }
641
+ catch (e) {
642
+ uxLog("warning", this.commandThis, c.yellow(`Error writing to CSV log: ${e.message}`));
643
+ }
375
644
  }
376
645
  async processImport() {
377
646
  // Get config
@@ -384,18 +653,23 @@ export class FilesImporter {
384
653
  const allRecordFolders = fs.readdirSync(this.exportedFilesFolder).filter((file) => {
385
654
  return fs.statSync(path.join(this.exportedFilesFolder, file)).isDirectory();
386
655
  });
387
- let totalFilesNumber = 0;
656
+ this.totalFolders = allRecordFolders.length;
657
+ // Count total files
388
658
  for (const folder of allRecordFolders) {
389
- totalFilesNumber += fs.readdirSync(path.join(this.exportedFilesFolder, folder)).length;
659
+ this.totalFiles += fs.readdirSync(path.join(this.exportedFilesFolder, folder)).length;
390
660
  }
391
- await this.calculateApiConsumption(totalFilesNumber);
661
+ // Initialize API usage tracking with total file count
662
+ await this.calculateApiConsumption(this.totalFiles);
663
+ // Initialize CSV logging
664
+ await this.initializeCsvLog();
665
+ // Start progress tracking
666
+ WebSocketClient.sendProgressStartMessage("Importing files", this.totalFiles);
392
667
  // Query parent objects to find Ids corresponding to field value used as folder name
393
668
  const parentObjectsRes = await bulkQuery(this.dtl.soqlQuery, this.conn);
394
669
  const parentObjects = parentObjectsRes.records;
395
- let successNb = 0;
396
- let errorNb = 0;
670
+ let processedFiles = 0;
397
671
  for (const recordFolder of allRecordFolders) {
398
- uxLog("log", this, c.grey(`Processing record ${recordFolder} ...`));
672
+ uxLog("log", this.commandThis, c.grey(`Processing record ${recordFolder} ...`));
399
673
  const recordFolderPath = path.join(this.exportedFilesFolder, recordFolder);
400
674
  // List files in folder
401
675
  const files = fs.readdirSync(recordFolderPath).filter((file) => {
@@ -404,7 +678,18 @@ export class FilesImporter {
404
678
  // Find Id of parent object using folder name
405
679
  const parentRecordIds = parentObjects.filter((parentObj) => parentObj[this.dtl.outputFolderNameField] === recordFolder);
406
680
  if (parentRecordIds.length === 0) {
407
- uxLog("error", this, c.red(`Unable to find Id for ${this.dtl.outputFolderNameField}=${recordFolder}`));
681
+ uxLog("error", this.commandThis, c.red(`Unable to find Id for ${this.dtl.outputFolderNameField}=${recordFolder}`));
682
+ // Log all files in this folder as skipped
683
+ for (const file of files) {
684
+ const { fileName, extension } = this.extractFileInfo(file, recordFolder);
685
+ const filePath = path.join(recordFolderPath, file);
686
+ const fileSizeKB = fs.existsSync(filePath) ? Math.round(fs.statSync(filePath).size / 1024) : 0;
687
+ await this.writeCsvLogEntry('skipped', recordFolder, fileName, extension, fileSizeKB, 'Parent record not found', '');
688
+ this.filesSkipped++;
689
+ processedFiles++;
690
+ // Update progress
691
+ WebSocketClient.sendProgressStepMessage(processedFiles, this.totalFiles);
692
+ }
408
693
  continue;
409
694
  }
410
695
  const parentRecordId = parentRecordIds[0].Id;
@@ -416,45 +701,101 @@ export class FilesImporter {
416
701
  existingDocuments = existingDocsQueryRes.records;
417
702
  }
418
703
  for (const file of files) {
419
- const fileData = fs.readFileSync(path.join(recordFolderPath, file));
420
- const contentVersionParams = {
421
- Title: file,
422
- PathOnClient: file,
423
- VersionData: fileData.toString('base64'),
424
- };
425
- const matchingExistingDocs = existingDocuments.filter((doc) => doc.Title === file);
426
- if (matchingExistingDocs.length > 0) {
427
- contentVersionParams.ContentDocumentId = matchingExistingDocs[0].ContentDocumentId;
428
- uxLog("log", this, c.grey(`Overwriting file ${file} ...`));
429
- }
430
- else {
431
- contentVersionParams.FirstPublishLocationId = parentRecordId;
432
- uxLog("log", this, c.grey(`Uploading file ${file} ...`));
433
- }
704
+ const filePath = path.join(recordFolderPath, file);
705
+ const { fileName, extension } = this.extractFileInfo(file, recordFolder);
706
+ const fileSizeKB = fs.existsSync(filePath) ? Math.round(fs.statSync(filePath).size / 1024) : 0;
434
707
  try {
708
+ const fileData = fs.readFileSync(filePath);
709
+ const contentVersionParams = {
710
+ Title: file,
711
+ PathOnClient: file,
712
+ VersionData: fileData.toString('base64'),
713
+ };
714
+ const matchingExistingDocs = existingDocuments.filter((doc) => doc.Title === file);
715
+ let isOverwrite = false;
716
+ if (matchingExistingDocs.length > 0) {
717
+ contentVersionParams.ContentDocumentId = matchingExistingDocs[0].ContentDocumentId;
718
+ uxLog("log", this.commandThis, c.grey(`Overwriting file ${file} ...`));
719
+ isOverwrite = true;
720
+ }
721
+ else {
722
+ contentVersionParams.FirstPublishLocationId = parentRecordId;
723
+ uxLog("log", this.commandThis, c.grey(`Uploading file ${file} ...`));
724
+ }
435
725
  const insertResult = await this.conn.sobject('ContentVersion').create(contentVersionParams);
436
- if (insertResult.length === 0) {
437
- uxLog("error", this, c.red(`Unable to upload file ${file}`));
438
- errorNb++;
726
+ if (Array.isArray(insertResult) && insertResult.length === 0) {
727
+ uxLog("error", this.commandThis, c.red(`Unable to upload file ${file}`));
728
+ await this.writeCsvLogEntry('failed', recordFolder, fileName, extension, fileSizeKB, 'Upload failed', '');
729
+ this.filesErrors++;
730
+ }
731
+ else if (Array.isArray(insertResult) && !insertResult[0].success) {
732
+ uxLog("error", this.commandThis, c.red(`Unable to upload file ${file}`));
733
+ await this.writeCsvLogEntry('failed', recordFolder, fileName, extension, fileSizeKB, insertResult[0].errors?.join(', ') || 'Upload failed', '');
734
+ this.filesErrors++;
439
735
  }
440
736
  else {
441
- successNb++;
737
+ // Extract ContentVersion ID from successful insert result
738
+ const contentVersionId = Array.isArray(insertResult) && insertResult.length > 0
739
+ ? insertResult[0].id
740
+ : insertResult.id || '';
741
+ if (isOverwrite) {
742
+ uxLog("success", this.commandThis, c.grey(`Overwritten ${file}`));
743
+ await this.writeCsvLogEntry('overwritten', recordFolder, fileName, extension, fileSizeKB, '', contentVersionId);
744
+ this.filesOverwritten++;
745
+ }
746
+ else {
747
+ uxLog("success", this.commandThis, c.grey(`Uploaded ${file}`));
748
+ await this.writeCsvLogEntry('success', recordFolder, fileName, extension, fileSizeKB, '', contentVersionId);
749
+ this.filesUploaded++;
750
+ }
442
751
  }
443
752
  }
444
753
  catch (e) {
445
- uxLog("error", this, c.red(`Unable to upload file ${file}: ${e.message}`));
446
- errorNb++;
754
+ const errorDetail = e.message;
755
+ uxLog("error", this.commandThis, c.red(`Unable to upload file ${file}: ${errorDetail}`));
756
+ await this.writeCsvLogEntry('failed', recordFolder, fileName, extension, fileSizeKB, errorDetail, '');
757
+ this.filesErrors++;
447
758
  }
759
+ processedFiles++;
760
+ // Update progress
761
+ WebSocketClient.sendProgressStepMessage(processedFiles, this.totalFiles);
448
762
  }
449
763
  }
450
- uxLog("success", this, c.green(`Uploaded ${successNb} files`));
451
- if (errorNb > 0) {
452
- uxLog("warning", this, c.yellow(`Errors during the upload of ${successNb} files`));
453
- }
454
- return { successNb: successNb, errorNb: errorNb };
764
+ // End progress tracking
765
+ WebSocketClient.sendProgressEndMessage(this.totalFiles);
766
+ // Build and return result
767
+ return await this.buildResult();
768
+ }
769
+ // Build stats & result
770
+ async buildResult() {
771
+ const connAny = this.conn;
772
+ const apiCallsRemaining = connAny?.limitInfo?.apiUsage?.used
773
+ ? (connAny?.limitInfo?.apiUsage?.limit || 0) - (connAny?.limitInfo?.apiUsage?.used || 0)
774
+ : null;
775
+ const result = {
776
+ stats: {
777
+ filesUploaded: this.filesUploaded,
778
+ filesOverwritten: this.filesOverwritten,
779
+ filesErrors: this.filesErrors,
780
+ filesSkipped: this.filesSkipped,
781
+ totalFolders: this.totalFolders,
782
+ totalFiles: this.totalFiles,
783
+ apiLimit: this.apiLimit,
784
+ apiUsedBefore: this.apiUsedBefore,
785
+ apiUsedAfter: connAny?.limitInfo?.apiUsage?.used || null,
786
+ apiCallsRemaining,
787
+ },
788
+ logFile: this.logFile
789
+ };
790
+ await createXlsxFromCsv(this.logFile, { fileTitle: "Imported files report" }, result);
791
+ return result;
455
792
  }
456
793
  // Calculate API consumption
457
794
  async calculateApiConsumption(totalFilesNumber) {
795
+ // Track API usage before process
796
+ const connAny = this.conn;
797
+ this.apiUsedBefore = connAny?.limitInfo?.apiUsage?.used || 0;
798
+ this.apiLimit = connAny?.limitInfo?.apiUsage?.limit || 0;
458
799
  const bulkCallsNb = 1;
459
800
  if (this.handleOverwrite) {
460
801
  totalFilesNumber = totalFilesNumber * 2;
@@ -522,6 +863,7 @@ export async function getFilesWorkspaceDetail(filesWorkspace) {
522
863
  const outputFileNameFormat = exportFileJson.outputFileNameFormat || 'title';
523
864
  const overwriteParentRecords = exportFileJson.overwriteParentRecords === false ? false : exportFileJson.overwriteParentRecords || true;
524
865
  const overwriteFiles = exportFileJson.overwriteFiles || false;
866
+ const fileSizeMin = exportFileJson.fileSizeMin || 0;
525
867
  return {
526
868
  full_label: `[${folderName}]${folderName != hardisLabel ? `: ${hardisLabel}` : ''}`,
527
869
  label: hardisLabel,
@@ -532,6 +874,7 @@ export async function getFilesWorkspaceDetail(filesWorkspace) {
532
874
  outputFileNameFormat: outputFileNameFormat,
533
875
  overwriteParentRecords: overwriteParentRecords,
534
876
  overwriteFiles: overwriteFiles,
877
+ fileSizeMin: fileSizeMin,
535
878
  };
536
879
  }
537
880
  export async function promptFilesExportConfiguration(filesExportConfig, override = false) {
@@ -605,6 +948,15 @@ export async function promptFilesExportConfiguration(filesExportConfig, override
605
948
  description: 'Replace existing local files with newly downloaded versions',
606
949
  initial: filesExportConfig.overwriteFiles,
607
950
  },
951
+ {
952
+ type: 'number',
953
+ name: 'fileSizeMin',
954
+ message: 'Please input the minimum file size in KB (0 = no minimum)',
955
+ description: 'Only files with size greater than or equal to this value will be downloaded (in kilobytes)',
956
+ placeholder: 'Ex: 10',
957
+ initial: filesExportConfig.fileSizeMin || 0,
958
+ min: 0,
959
+ },
608
960
  ]);
609
961
  const resp = await prompts(questions);
610
962
  const filesConfig = Object.assign(filesExportConfig, {
@@ -616,6 +968,7 @@ export async function promptFilesExportConfiguration(filesExportConfig, override
616
968
  outputFileNameFormat: resp.outputFileNameFormat,
617
969
  overwriteParentRecords: resp.overwriteParentRecords,
618
970
  overwriteFiles: resp.overwriteFiles,
971
+ fileSizeMin: resp.fileSizeMin,
619
972
  });
620
973
  return filesConfig;
621
974
  }
@@ -695,30 +1048,7 @@ export async function generateCsvFile(data, outputPath, options) {
695
1048
  const csvFileTitle = options?.fileTitle ? `${options.fileTitle} (CSV)` : options?.csvFileTitle ?? "Report (CSV)";
696
1049
  WebSocketClient.sendReportFileMessage(outputPath, csvFileTitle, "report");
697
1050
  if (data.length > 0 && !options?.noExcel) {
698
- try {
699
- // Generate mirror XSLX file
700
- const xlsDirName = path.join(path.dirname(outputPath), 'xls');
701
- const xslFileName = path.basename(outputPath).replace('.csv', '.xlsx');
702
- const xslxFile = path.join(xlsDirName, xslFileName);
703
- await fs.ensureDir(xlsDirName);
704
- await csvToXls(outputPath, xslxFile);
705
- uxLog("action", this, c.cyan(c.italic(`Please see detailed XSLX log in ${c.bold(xslxFile)}`)));
706
- const xlsFileTitle = options?.fileTitle ? `${options.fileTitle} (XSLX)` : options?.xlsFileTitle ?? "Report (XSLX)";
707
- WebSocketClient.sendReportFileMessage(xslxFile, xlsFileTitle, "report");
708
- result.xlsxFile = xslxFile;
709
- if (!isCI && !(process.env.NO_OPEN === 'true') && !WebSocketClient.isAliveWithLwcUI()) {
710
- try {
711
- uxLog("other", this, c.italic(c.grey(`Opening XSLX file ${c.bold(xslxFile)}... (define NO_OPEN=true to disable this)`)));
712
- await open(xslxFile, { wait: false });
713
- }
714
- catch (e) {
715
- uxLog("warning", this, c.yellow('Error while opening XSLX file:\n' + e.message + '\n' + e.stack));
716
- }
717
- }
718
- }
719
- catch (e2) {
720
- uxLog("warning", this, c.yellow('Error while generating XSLX log file:\n' + e2.message + '\n' + e2.stack));
721
- }
1051
+ await createXlsxFromCsv(outputPath, options, result);
722
1052
  }
723
1053
  else {
724
1054
  uxLog("other", this, c.grey(`No XLS file generated as ${outputPath} is empty`));
@@ -729,6 +1059,31 @@ export async function generateCsvFile(data, outputPath, options) {
729
1059
  }
730
1060
  return result;
731
1061
  }
1062
+ async function createXlsxFromCsv(outputPath, options, result) {
1063
+ try {
1064
+ const xlsDirName = path.join(path.dirname(outputPath), 'xls');
1065
+ const xslFileName = path.basename(outputPath).replace('.csv', '.xlsx');
1066
+ const xslxFile = path.join(xlsDirName, xslFileName);
1067
+ await fs.ensureDir(xlsDirName);
1068
+ await csvToXls(outputPath, xslxFile);
1069
+ uxLog("action", this, c.cyan(c.italic(`Please see detailed XLSX log in ${c.bold(xslxFile)}`)));
1070
+ const xlsFileTitle = options?.fileTitle ? `${options.fileTitle} (XLSX)` : options?.xlsFileTitle ?? "Report (XLSX)";
1071
+ WebSocketClient.sendReportFileMessage(xslxFile, xlsFileTitle, "report");
1072
+ result.xlsxFile = xslxFile;
1073
+ if (!isCI && !(process.env.NO_OPEN === 'true') && !WebSocketClient.isAliveWithLwcUI()) {
1074
+ try {
1075
+ uxLog("other", this, c.italic(c.grey(`Opening XLSX file ${c.bold(xslxFile)}... (define NO_OPEN=true to disable this)`)));
1076
+ await open(xslxFile, { wait: false });
1077
+ }
1078
+ catch (e) {
1079
+ uxLog("warning", this, c.yellow('Error while opening XLSX file:\n' + e.message + '\n' + e.stack));
1080
+ }
1081
+ }
1082
+ }
1083
+ catch (e2) {
1084
+ uxLog("warning", this, c.yellow('Error while generating XLSX log file:\n' + e2.message + '\n' + e2.stack));
1085
+ }
1086
+ }
732
1087
  async function csvToXls(csvFile, xslxFile) {
733
1088
  const workbook = new ExcelJS.Workbook();
734
1089
  const worksheet = await workbook.csv.readFile(csvFile);