@arela/uploader 1.0.14 ā 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/commands/IdentifyCommand.js +36 -1
- package/src/commands/PollWorkerCommand.js +391 -0
- package/src/commands/PropagateCommand.js +38 -1
- package/src/commands/PushCommand.js +66 -5
- package/src/commands/ScanCommand.js +37 -1
- package/src/commands/WorkerCommand.js +334 -0
- package/src/config/config.js +86 -4
- package/src/index.js +42 -0
- package/src/services/PipelineApiService.js +249 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arela/uploader",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "CLI to upload files/directories to Arela",
|
|
5
5
|
"bin": {
|
|
6
6
|
"arela": "./src/index.js"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"homepage": "https://github.com/inspiraCode/arela-uploader#readme",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@supabase/supabase-js": "2.49.4",
|
|
34
|
+
"bullmq": "^5.71.0",
|
|
34
35
|
"chokidar": "^4.0.3",
|
|
35
36
|
"cli-progress": "3.12.0",
|
|
36
37
|
"commander": "13.1.0",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"form-data": "4.0.4",
|
|
39
40
|
"formdata-node": "^6.0.3",
|
|
40
41
|
"globby": "14.1.0",
|
|
42
|
+
"ioredis": "^5.10.0",
|
|
41
43
|
"mime-types": "3.0.1",
|
|
42
44
|
"node-fetch": "3.3.2",
|
|
43
45
|
"office-text-extractor": "3.0.3",
|
|
@@ -34,9 +34,11 @@ export class IdentifyCommand {
|
|
|
34
34
|
* @param {string} options.api - API target (default, agencia, cliente)
|
|
35
35
|
* @param {number} options.batchSize - Batch size for API operations
|
|
36
36
|
* @param {boolean} options.showStats - Show performance statistics
|
|
37
|
+
* @param {Function} options.onProgress - Optional callback for progress updates (percent, message)
|
|
37
38
|
*/
|
|
38
39
|
async execute(options = {}) {
|
|
39
40
|
const startTime = Date.now();
|
|
41
|
+
this.onProgress = options.onProgress || null;
|
|
40
42
|
|
|
41
43
|
try {
|
|
42
44
|
// Validate scan configuration (need same config as scan command)
|
|
@@ -88,8 +90,19 @@ export class IdentifyCommand {
|
|
|
88
90
|
pending: 0,
|
|
89
91
|
};
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
// Report initial progress
|
|
94
|
+
this.#reportProgress(
|
|
95
|
+
0,
|
|
96
|
+
`Starting identification on ${tables.length} tables`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < tables.length; i++) {
|
|
100
|
+
const table = tables[i];
|
|
92
101
|
logger.info(`\nš Processing table: ${table.tableName}`);
|
|
102
|
+
this.#reportProgress(
|
|
103
|
+
Math.round((i / tables.length) * 100),
|
|
104
|
+
`Processing table ${i + 1}/${tables.length}: ${table.tableName}`,
|
|
105
|
+
);
|
|
93
106
|
|
|
94
107
|
// Get detection statistics for this table
|
|
95
108
|
const stats = await this.#processTable(
|
|
@@ -108,6 +121,12 @@ export class IdentifyCommand {
|
|
|
108
121
|
const avgSpeed =
|
|
109
122
|
duration > 0 ? Math.round(totalStats.processed / duration) : 0;
|
|
110
123
|
|
|
124
|
+
// Report completion
|
|
125
|
+
this.#reportProgress(
|
|
126
|
+
100,
|
|
127
|
+
`Identification completed: ${totalStats.detected} pedimentos detected`,
|
|
128
|
+
);
|
|
129
|
+
|
|
111
130
|
logger.success(`\nā
Identification Complete!`);
|
|
112
131
|
logger.info(`\nš Total Results:`);
|
|
113
132
|
logger.info(` Tables Processed: ${tables.length}`);
|
|
@@ -479,6 +498,22 @@ export class IdentifyCommand {
|
|
|
479
498
|
` Heap Total: ${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
|
|
480
499
|
);
|
|
481
500
|
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Report progress to callback if available (for worker mode)
|
|
504
|
+
* @private
|
|
505
|
+
* @param {number} percent - Progress percentage (0-100)
|
|
506
|
+
* @param {string} message - Progress message
|
|
507
|
+
*/
|
|
508
|
+
#reportProgress(percent, message) {
|
|
509
|
+
if (this.onProgress && typeof this.onProgress === 'function') {
|
|
510
|
+
try {
|
|
511
|
+
this.onProgress(percent, message);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
logger.debug(`Progress callback error: ${error.message}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
482
517
|
}
|
|
483
518
|
|
|
484
519
|
// Export singleton instance
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import logger from '../services/LoggingService.js';
|
|
2
|
+
import { PipelineApiService } from '../services/PipelineApiService.js';
|
|
3
|
+
|
|
4
|
+
import appConfig from '../config/config.js';
|
|
5
|
+
import ErrorHandler from '../errors/ErrorHandler.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Poll Worker Command Handler
|
|
9
|
+
* Runs arela-uploader as an HTTP-polling worker, fetching jobs from the API.
|
|
10
|
+
* Alternative to BullMQ/Redis-based worker for environments without Redis access.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* arela worker --poll
|
|
14
|
+
* arela worker --poll --interval 10000
|
|
15
|
+
*
|
|
16
|
+
* Environment variables:
|
|
17
|
+
* WORKER_POLL_INTERVAL - Polling interval in ms (default: 5000)
|
|
18
|
+
* WORKER_POLL_API_TARGET - API target for polling (default: 'agencia')
|
|
19
|
+
* ARELA_SERVER_ID - Server identifier for job assignment
|
|
20
|
+
*/
|
|
21
|
+
export class PollWorkerCommand {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.errorHandler = new ErrorHandler(logger);
|
|
24
|
+
this.isShuttingDown = false;
|
|
25
|
+
this.currentJob = null;
|
|
26
|
+
this.pipelineApi = null;
|
|
27
|
+
this.pollTimer = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute the poll worker command
|
|
32
|
+
* @param {Object} options - Command options
|
|
33
|
+
* @param {number} options.interval - Polling interval in milliseconds
|
|
34
|
+
* @param {string} options.api - API target for polling
|
|
35
|
+
*/
|
|
36
|
+
async execute(options = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const workerConfig = appConfig.getWorkerConfig();
|
|
39
|
+
const pollConfig = workerConfig.poll;
|
|
40
|
+
|
|
41
|
+
// Get configuration
|
|
42
|
+
const interval = parseInt(options.interval) || pollConfig.interval;
|
|
43
|
+
const apiTarget = options.api || pollConfig.apiTarget;
|
|
44
|
+
const serverId = appConfig.getServerId();
|
|
45
|
+
|
|
46
|
+
if (!serverId) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'ARELA_SERVER_ID is required for poll mode. Set it in your .env file.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize Pipeline API service
|
|
53
|
+
this.pipelineApi = new PipelineApiService(apiTarget);
|
|
54
|
+
|
|
55
|
+
console.log('\nš§ Starting Arela Poll Worker');
|
|
56
|
+
console.log(`š” API Target: ${apiTarget}`);
|
|
57
|
+
console.log(`š„ļø Server ID: ${serverId}`);
|
|
58
|
+
console.log(`ā±ļø Poll Interval: ${interval}ms`);
|
|
59
|
+
|
|
60
|
+
logger.info('š§ Starting Arela Poll Worker');
|
|
61
|
+
logger.info(`š” API Target: ${apiTarget}`);
|
|
62
|
+
logger.info(`š„ļø Server ID: ${serverId}`);
|
|
63
|
+
logger.info(`ā±ļø Poll Interval: ${interval}ms`);
|
|
64
|
+
|
|
65
|
+
// Setup graceful shutdown handlers
|
|
66
|
+
this.#setupShutdownHandlers();
|
|
67
|
+
|
|
68
|
+
// Start polling loop
|
|
69
|
+
await this.#startPolling(serverId, interval);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error('ā Poll Worker failed to start:', error.message);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start the polling loop
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
async #startPolling(serverId, interval) {
|
|
81
|
+
console.log('\nā
Poll Worker is running. Press Ctrl+C to stop.\n');
|
|
82
|
+
logger.success('Poll Worker is running');
|
|
83
|
+
|
|
84
|
+
while (!this.isShuttingDown) {
|
|
85
|
+
try {
|
|
86
|
+
// Check for next job
|
|
87
|
+
const job = await this.pipelineApi.getNextJob(serverId);
|
|
88
|
+
|
|
89
|
+
if (job && job.id) {
|
|
90
|
+
console.log(`š„ Got job: ${job.type} for RFC ${job.rfc}`);
|
|
91
|
+
logger.info(`š„ Got job: ${job.id} - ${job.type} for RFC ${job.rfc}`);
|
|
92
|
+
await this.#processJob(job);
|
|
93
|
+
} else {
|
|
94
|
+
// Only log to file, not console (too noisy)
|
|
95
|
+
logger.debug('š No jobs available, waiting...');
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`ā Polling error: ${error.message}`);
|
|
99
|
+
logger.error(`ā Polling error: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Wait for next poll (unless shutting down)
|
|
103
|
+
if (!this.isShuttingDown) {
|
|
104
|
+
await this.#sleep(interval);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Process a job
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
async #processJob(job) {
|
|
114
|
+
this.currentJob = job;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
logger.info(`š Processing ${job.type} job ${job.id}`);
|
|
118
|
+
|
|
119
|
+
// Create progress callback that reports to API
|
|
120
|
+
const onProgress = async (percent, message) => {
|
|
121
|
+
await this.pipelineApi.updateProgress(
|
|
122
|
+
job.id,
|
|
123
|
+
percent,
|
|
124
|
+
message,
|
|
125
|
+
null,
|
|
126
|
+
job.type,
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Configure API targets for this job
|
|
131
|
+
this.#configureApiTargets(job);
|
|
132
|
+
|
|
133
|
+
// Execute the appropriate command
|
|
134
|
+
let result;
|
|
135
|
+
switch (job.type) {
|
|
136
|
+
case 'scan':
|
|
137
|
+
result = await this.#processScanJob(job, onProgress);
|
|
138
|
+
break;
|
|
139
|
+
case 'identify':
|
|
140
|
+
result = await this.#processIdentifyJob(job, onProgress);
|
|
141
|
+
break;
|
|
142
|
+
case 'propagate':
|
|
143
|
+
result = await this.#processPropagateJob(job, onProgress);
|
|
144
|
+
break;
|
|
145
|
+
case 'push':
|
|
146
|
+
result = await this.#processPushJob(job, onProgress);
|
|
147
|
+
break;
|
|
148
|
+
case 'full':
|
|
149
|
+
result = await this.#processFullPipeline(job, onProgress);
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown job type: ${job.type}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Mark job as completed
|
|
156
|
+
await this.pipelineApi.completeJob(job.id, result);
|
|
157
|
+
logger.success(`ā
Job ${job.id} completed successfully`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.error(`ā Job ${job.id} failed: ${error.message}`);
|
|
160
|
+
await this.pipelineApi.failJob(job.id, error.message);
|
|
161
|
+
} finally {
|
|
162
|
+
this.currentJob = null;
|
|
163
|
+
// Reset API configuration
|
|
164
|
+
this.#resetApiConfig();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Configure API targets for cross-tenant operations
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
#configureApiTargets(job) {
|
|
173
|
+
if (job.sourceApi && job.targetApi) {
|
|
174
|
+
appConfig.setCrossTenantTargets(job.sourceApi, job.targetApi);
|
|
175
|
+
} else {
|
|
176
|
+
appConfig.setApiTarget(job.sourceApi || 'agencia');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reset API configuration after job
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
#resetApiConfig() {
|
|
185
|
+
// Clear cross-tenant mode
|
|
186
|
+
appConfig.api.sourceTarget = null;
|
|
187
|
+
appConfig.api.targetTarget = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Override scan configuration from job data
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
#overrideScanConfig(job) {
|
|
195
|
+
if (job.scanConfig) {
|
|
196
|
+
if (job.scanConfig.companySlug) {
|
|
197
|
+
process.env.ARELA_COMPANY_SLUG = job.scanConfig.companySlug;
|
|
198
|
+
}
|
|
199
|
+
if (job.scanConfig.serverId) {
|
|
200
|
+
process.env.ARELA_SERVER_ID = job.scanConfig.serverId;
|
|
201
|
+
}
|
|
202
|
+
if (job.scanConfig.basePath) {
|
|
203
|
+
process.env.UPLOAD_BASE_PATH = job.scanConfig.basePath;
|
|
204
|
+
process.env.ARELA_BASE_PATH_LABEL = job.scanConfig.basePath;
|
|
205
|
+
}
|
|
206
|
+
if (job.scanConfig.directoryLevel !== undefined) {
|
|
207
|
+
process.env.SCAN_DIRECTORY_LEVEL = String(
|
|
208
|
+
job.scanConfig.directoryLevel,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Override scan directories if provided
|
|
214
|
+
if (job.scanDirectories && job.scanDirectories.length > 0) {
|
|
215
|
+
process.env.UPLOAD_SOURCES = job.scanDirectories.join('|');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Override file extensions if provided
|
|
219
|
+
if (job.fileExtensions && job.fileExtensions.length > 0) {
|
|
220
|
+
process.env.UPLOAD_FILE_EXTENSIONS = job.fileExtensions.join(',');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Process a scan job
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
async #processScanJob(job, onProgress) {
|
|
229
|
+
const { ScanCommand } = await import('./ScanCommand.js');
|
|
230
|
+
const scanCommand = new ScanCommand();
|
|
231
|
+
|
|
232
|
+
this.#overrideScanConfig(job);
|
|
233
|
+
|
|
234
|
+
const options = {
|
|
235
|
+
api: job.sourceApi || 'agencia',
|
|
236
|
+
countFirst: false,
|
|
237
|
+
stream: true,
|
|
238
|
+
onProgress,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return scanCommand.execute(options);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Process an identify job
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
async #processIdentifyJob(job, onProgress) {
|
|
249
|
+
const { IdentifyCommand } = await import('./IdentifyCommand.js');
|
|
250
|
+
const identifyCommand = new IdentifyCommand();
|
|
251
|
+
|
|
252
|
+
this.#overrideScanConfig(job);
|
|
253
|
+
|
|
254
|
+
const options = {
|
|
255
|
+
api: job.sourceApi || 'agencia',
|
|
256
|
+
batchSize: 100,
|
|
257
|
+
showStats: false,
|
|
258
|
+
onProgress,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return identifyCommand.execute(options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Process a propagate job
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
async #processPropagateJob(job, onProgress) {
|
|
269
|
+
const { PropagateCommand } = await import('./PropagateCommand.js');
|
|
270
|
+
|
|
271
|
+
this.#overrideScanConfig(job);
|
|
272
|
+
|
|
273
|
+
const options = {
|
|
274
|
+
api: job.sourceApi || 'agencia',
|
|
275
|
+
batchSize: 50,
|
|
276
|
+
showStats: false,
|
|
277
|
+
onProgress,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const propagateCommand = new PropagateCommand(options);
|
|
281
|
+
return propagateCommand.execute();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Process a push job
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
async #processPushJob(job, onProgress) {
|
|
289
|
+
const { PushCommand } = await import('./PushCommand.js');
|
|
290
|
+
const pushCommand = new PushCommand();
|
|
291
|
+
|
|
292
|
+
this.#overrideScanConfig(job);
|
|
293
|
+
|
|
294
|
+
const options = {
|
|
295
|
+
scanApi: job.sourceApi || 'agencia',
|
|
296
|
+
pushApi: job.targetApi || 'cliente',
|
|
297
|
+
batchSize: 100,
|
|
298
|
+
uploadBatchSize: 10,
|
|
299
|
+
folderStructure: job.folderStructure || 'pedimento',
|
|
300
|
+
autoOrganize: true,
|
|
301
|
+
showStats: false,
|
|
302
|
+
onProgress,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return pushCommand.execute(options);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Process full pipeline (scan ā identify ā propagate ā push)
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
async #processFullPipeline(job, onProgress) {
|
|
313
|
+
const results = {};
|
|
314
|
+
|
|
315
|
+
// Step 1: Scan (0-25%)
|
|
316
|
+
logger.info('š Step 1/4: Scanning files...');
|
|
317
|
+
const wrappedProgress1 = (pct, msg) =>
|
|
318
|
+
onProgress(pct * 0.25, `[Scan] ${msg}`);
|
|
319
|
+
results.scan = await this.#processScanJob(job, wrappedProgress1);
|
|
320
|
+
|
|
321
|
+
// Step 2: Identify (25-50%)
|
|
322
|
+
logger.info('š Step 2/4: Identifying documents...');
|
|
323
|
+
const wrappedProgress2 = (pct, msg) =>
|
|
324
|
+
onProgress(25 + pct * 0.25, `[Identify] ${msg}`);
|
|
325
|
+
results.identify = await this.#processIdentifyJob(job, wrappedProgress2);
|
|
326
|
+
|
|
327
|
+
// Step 3: Propagate (50-75%)
|
|
328
|
+
logger.info('š Step 3/4: Propagating paths...');
|
|
329
|
+
const wrappedProgress3 = (pct, msg) =>
|
|
330
|
+
onProgress(50 + pct * 0.25, `[Propagate] ${msg}`);
|
|
331
|
+
results.propagate = await this.#processPropagateJob(job, wrappedProgress3);
|
|
332
|
+
|
|
333
|
+
// Step 4: Push (75-100%)
|
|
334
|
+
logger.info('š¤ Step 4/4: Pushing to storage...');
|
|
335
|
+
const wrappedProgress4 = (pct, msg) =>
|
|
336
|
+
onProgress(75 + pct * 0.25, `[Push] ${msg}`);
|
|
337
|
+
results.push = await this.#processPushJob(job, wrappedProgress4);
|
|
338
|
+
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Sleep for specified milliseconds
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
#sleep(ms) {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
this.pollTimer = setTimeout(resolve, ms);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Setup graceful shutdown handlers
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
#setupShutdownHandlers() {
|
|
357
|
+
const shutdown = async (signal) => {
|
|
358
|
+
if (this.isShuttingDown) return;
|
|
359
|
+
this.isShuttingDown = true;
|
|
360
|
+
|
|
361
|
+
logger.info(`\nš Received ${signal}. Shutting down poll worker...`);
|
|
362
|
+
|
|
363
|
+
// Clear poll timer
|
|
364
|
+
if (this.pollTimer) {
|
|
365
|
+
clearTimeout(this.pollTimer);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Wait for current job to finish if any
|
|
369
|
+
if (this.currentJob) {
|
|
370
|
+
logger.info(
|
|
371
|
+
` ā³ Waiting for current job to finish: ${this.currentJob.id}`,
|
|
372
|
+
);
|
|
373
|
+
// Give it a moment to complete
|
|
374
|
+
await this.#sleep(2000);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Cleanup
|
|
378
|
+
if (this.pipelineApi) {
|
|
379
|
+
this.pipelineApi.destroy();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
logger.info('ā
Poll worker stopped gracefully');
|
|
383
|
+
process.exit(0);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
387
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default new PollWorkerCommand();
|
|
@@ -16,6 +16,7 @@ export class PropagateCommand {
|
|
|
16
16
|
batchSize: parseInt(options.batchSize) || 50, // Process 50 pedimentos at a time
|
|
17
17
|
showStats: options.showStats || false,
|
|
18
18
|
api: options.api || 'default',
|
|
19
|
+
onProgress: options.onProgress || null, // Progress callback for worker mode
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
this.scanApiService = null;
|
|
@@ -75,8 +76,19 @@ export class PropagateCommand {
|
|
|
75
76
|
directoriesProcessed: 0,
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// Report initial progress
|
|
80
|
+
this.#reportProgress(
|
|
81
|
+
0,
|
|
82
|
+
`Starting propagation on ${tables.length} tables`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < tables.length; i++) {
|
|
86
|
+
const table = tables[i];
|
|
79
87
|
console.log(`\nš Processing table: ${table.tableName}\n`);
|
|
88
|
+
this.#reportProgress(
|
|
89
|
+
Math.round((i / tables.length) * 100),
|
|
90
|
+
`Processing table ${i + 1}/${tables.length}: ${table.tableName}`,
|
|
91
|
+
);
|
|
80
92
|
this.tableName = table.tableName;
|
|
81
93
|
|
|
82
94
|
// Process this table
|
|
@@ -95,6 +107,12 @@ export class PropagateCommand {
|
|
|
95
107
|
? (totalStats.filesUpdated / parseFloat(duration)).toFixed(1)
|
|
96
108
|
: 0;
|
|
97
109
|
|
|
110
|
+
// Report completion
|
|
111
|
+
this.#reportProgress(
|
|
112
|
+
100,
|
|
113
|
+
`Propagation completed: ${totalStats.filesUpdated} files updated`,
|
|
114
|
+
);
|
|
115
|
+
|
|
98
116
|
console.log('\\nā
Propagation Complete!\\n');
|
|
99
117
|
console.log(`š Total Results:`);
|
|
100
118
|
console.log(` Tables Processed: ${tables.length}`);
|
|
@@ -473,6 +491,25 @@ export class PropagateCommand {
|
|
|
473
491
|
const used = process.memoryUsage();
|
|
474
492
|
return `${Math.round(used.heapUsed / 1024 / 1024)}MB`;
|
|
475
493
|
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Report progress to callback if available (for worker mode)
|
|
497
|
+
* @private
|
|
498
|
+
* @param {number} percent - Progress percentage (0-100)
|
|
499
|
+
* @param {string} message - Progress message
|
|
500
|
+
*/
|
|
501
|
+
#reportProgress(percent, message) {
|
|
502
|
+
if (
|
|
503
|
+
this.options.onProgress &&
|
|
504
|
+
typeof this.options.onProgress === 'function'
|
|
505
|
+
) {
|
|
506
|
+
try {
|
|
507
|
+
this.options.onProgress(percent, message);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
logger.debug(`Progress callback error: ${error.message}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
476
513
|
}
|
|
477
514
|
|
|
478
515
|
export default PropagateCommand;
|
|
@@ -17,13 +17,16 @@ export class PushCommand {
|
|
|
17
17
|
constructor() {
|
|
18
18
|
// ScanApiService will be initialized in execute() with proper API target
|
|
19
19
|
this.scanApiService = null;
|
|
20
|
+
this.onProgress = null; // Progress callback for worker mode
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Execute the push command
|
|
24
25
|
* @param {Object} options - Command options
|
|
26
|
+
* @param {Function} options.onProgress - Optional callback for progress updates (percent, message)
|
|
25
27
|
*/
|
|
26
28
|
async execute(options) {
|
|
29
|
+
this.onProgress = options.onProgress || null;
|
|
27
30
|
try {
|
|
28
31
|
console.log('\nš Starting arela push command\n');
|
|
29
32
|
|
|
@@ -127,8 +130,16 @@ export class PushCommand {
|
|
|
127
130
|
startTime: Date.now(),
|
|
128
131
|
};
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
// Report initial progress
|
|
134
|
+
this.#reportProgress(0, `Starting push on ${tables.length} tables`);
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < tables.length; i++) {
|
|
137
|
+
const table = tables[i];
|
|
131
138
|
console.log(`\\nš Processing table: ${table.tableName}\\n`);
|
|
139
|
+
this.#reportProgress(
|
|
140
|
+
Math.round((i / tables.length) * 100),
|
|
141
|
+
`Processing table ${i + 1}/${tables.length}: ${table.tableName}`,
|
|
142
|
+
);
|
|
132
143
|
|
|
133
144
|
// Get initial statistics for this table
|
|
134
145
|
const initialStats = await this.scanApiService.getPushStats(
|
|
@@ -175,6 +186,12 @@ export class PushCommand {
|
|
|
175
186
|
? (totalResults.processed / parseFloat(duration)).toFixed(0)
|
|
176
187
|
: 0;
|
|
177
188
|
|
|
189
|
+
// Report completion
|
|
190
|
+
this.#reportProgress(
|
|
191
|
+
100,
|
|
192
|
+
`Push completed: ${totalResults.uploaded} files uploaded`,
|
|
193
|
+
);
|
|
194
|
+
|
|
178
195
|
console.log('\\nā
Push Complete!\\n');
|
|
179
196
|
console.log('š Total Results:');
|
|
180
197
|
console.log(` Tables Processed: ${tables.length}`);
|
|
@@ -283,6 +300,11 @@ export class PushCommand {
|
|
|
283
300
|
// After each batch upload, those files are no longer "pending", so the next query
|
|
284
301
|
// at offset=0 will naturally return the next batch of unprocessed files
|
|
285
302
|
|
|
303
|
+
// Track seen file IDs to detect infinite loops (scan table update failures)
|
|
304
|
+
const seenFileIds = new Set();
|
|
305
|
+
let consecutiveRepeats = 0;
|
|
306
|
+
const MAX_CONSECUTIVE_REPEATS = 3;
|
|
307
|
+
|
|
286
308
|
// Start progress bar with known total
|
|
287
309
|
progressBar.start(totalToProcess, 0, {
|
|
288
310
|
speed: 0,
|
|
@@ -303,6 +325,23 @@ export class PushCommand {
|
|
|
303
325
|
break;
|
|
304
326
|
}
|
|
305
327
|
|
|
328
|
+
// Infinite loop protection: if the same files keep coming back,
|
|
329
|
+
// the scan table update is failing and they stay "pending" forever.
|
|
330
|
+
const allSeen = files.every((f) => seenFileIds.has(f.id));
|
|
331
|
+
if (allSeen) {
|
|
332
|
+
consecutiveRepeats++;
|
|
333
|
+
if (consecutiveRepeats >= MAX_CONSECUTIVE_REPEATS) {
|
|
334
|
+
const msg = `Aborting: same ${files.length} files returned ${MAX_CONSECUTIVE_REPEATS} times ā scan table updates are likely failing.`;
|
|
335
|
+
logger.error(msg);
|
|
336
|
+
console.error(`\nā ${msg}`);
|
|
337
|
+
hasMore = false;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
consecutiveRepeats = 0;
|
|
342
|
+
}
|
|
343
|
+
files.forEach((f) => seenFileIds.add(f.id));
|
|
344
|
+
|
|
306
345
|
// Upload files in smaller batches using new CLI upload endpoint
|
|
307
346
|
for (let i = 0; i < files.length; i += uploadBatchSize) {
|
|
308
347
|
const uploadBatch = files.slice(i, i + uploadBatchSize);
|
|
@@ -328,9 +367,9 @@ export class PushCommand {
|
|
|
328
367
|
try {
|
|
329
368
|
await this.scanApiService.batchUpdateUpload(tableName, batchResults);
|
|
330
369
|
} catch (updateError) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
);
|
|
370
|
+
const msg = `Failed to update scan table for batch: ${updateError.message}`;
|
|
371
|
+
logger.error(msg);
|
|
372
|
+
console.error(`\nā ${msg}`);
|
|
334
373
|
// Don't fail the entire process, just log the error
|
|
335
374
|
}
|
|
336
375
|
|
|
@@ -482,7 +521,13 @@ export class PushCommand {
|
|
|
482
521
|
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
483
522
|
const uploadedFile = apiResult.uploaded[0];
|
|
484
523
|
result.uploaded = true;
|
|
485
|
-
|
|
524
|
+
// Only assign storageId if it is a valid UUID; ignore placeholder values
|
|
525
|
+
const UUID_RE =
|
|
526
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
527
|
+
result.uploadedToStorageId =
|
|
528
|
+
uploadedFile.storageId && UUID_RE.test(uploadedFile.storageId)
|
|
529
|
+
? uploadedFile.storageId
|
|
530
|
+
: null;
|
|
486
531
|
logger.info(`ā Uploaded: ${file.file_name} ā ${uploadPath}`);
|
|
487
532
|
} else if (apiResult.errors && apiResult.errors.length > 0) {
|
|
488
533
|
const error = apiResult.errors[0];
|
|
@@ -674,6 +719,22 @@ export class PushCommand {
|
|
|
674
719
|
};
|
|
675
720
|
}
|
|
676
721
|
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Report progress to callback if available (for worker mode)
|
|
725
|
+
* @private
|
|
726
|
+
* @param {number} percent - Progress percentage (0-100)
|
|
727
|
+
* @param {string} message - Progress message
|
|
728
|
+
*/
|
|
729
|
+
#reportProgress(percent, message) {
|
|
730
|
+
if (this.onProgress && typeof this.onProgress === 'function') {
|
|
731
|
+
try {
|
|
732
|
+
this.onProgress(percent, message);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
logger.debug(`Progress callback error: ${error.message}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
677
738
|
}
|
|
678
739
|
|
|
679
740
|
export default PushCommand;
|