@arela/uploader 0.2.7 → 0.2.8
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/.env.template +67 -13
- package/package.json +1 -1
- package/src/commands/UploadCommand.js +156 -49
- package/src/config/config.js +8 -3
- package/src/services/DatabaseService.js +356 -129
- package/src/services/upload/ApiUploadService.js +61 -6
package/.env.template
CHANGED
|
@@ -1,20 +1,74 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copy this to .env and
|
|
1
|
+
# Arela Uploader Environment Configuration
|
|
2
|
+
# Copy this to your .env file and adjust values for your setup
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
SUPABASE_BUCKET=your-bucket-name
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# BASIC CONFIGURATION
|
|
6
|
+
# =============================================================================
|
|
8
7
|
|
|
9
8
|
# Arela API Configuration
|
|
10
9
|
ARELA_API_URL=https://your-arela-api-url.com
|
|
11
|
-
ARELA_API_TOKEN=your-api-token
|
|
10
|
+
ARELA_API_TOKEN=your-api-token-here
|
|
11
|
+
|
|
12
|
+
# Supabase Configuration (fallback)
|
|
13
|
+
SUPABASE_URL=https://your-supabase-url.supabase.co
|
|
14
|
+
SUPABASE_KEY=your-supabase-key-here
|
|
15
|
+
SUPABASE_BUCKET=your-bucket-name
|
|
12
16
|
|
|
13
|
-
# Upload
|
|
14
|
-
UPLOAD_BASE_PATH=/
|
|
17
|
+
# Upload Sources (separate with |)
|
|
18
|
+
UPLOAD_BASE_PATH=/path/to/your/upload/base
|
|
15
19
|
UPLOAD_SOURCES=folder1|folder2|folder3
|
|
20
|
+
UPLOAD_RFCS=rfc1|rfc2|rfc3
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# PERFORMANCE OPTIMIZATION FOR MULTIPLE API REPLICAS
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
# API Connection Configuration
|
|
27
|
+
# Set this to match your number of API replicas (e.g., if you have 10 API instances, set to 10)
|
|
28
|
+
MAX_API_CONNECTIONS=10
|
|
29
|
+
|
|
30
|
+
# API Connection Timeout (milliseconds)
|
|
31
|
+
API_CONNECTION_TIMEOUT=60000
|
|
32
|
+
|
|
33
|
+
# Batch Processing Configuration
|
|
34
|
+
# Files processed concurrently per batch (should be >= MAX_API_CONNECTIONS for best performance)
|
|
35
|
+
BATCH_SIZE=100
|
|
36
|
+
|
|
37
|
+
# Delay between batches (0 for maximum speed)
|
|
38
|
+
BATCH_DELAY=0
|
|
39
|
+
|
|
40
|
+
# Source Processing Concurrency
|
|
41
|
+
# Number of upload sources/folders to process simultaneously
|
|
42
|
+
MAX_CONCURRENT_SOURCES=2
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# EXAMPLE CONFIGURATIONS FOR DIFFERENT SCENARIOS
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
# For 10 API Replicas (High Performance Setup):
|
|
49
|
+
# MAX_API_CONNECTIONS=10
|
|
50
|
+
# BATCH_SIZE=100
|
|
51
|
+
# MAX_CONCURRENT_SOURCES=3
|
|
52
|
+
# BATCH_DELAY=0
|
|
53
|
+
|
|
54
|
+
# For 5 API Replicas (Medium Performance Setup):
|
|
55
|
+
# MAX_API_CONNECTIONS=5
|
|
56
|
+
# BATCH_SIZE=50
|
|
57
|
+
# MAX_CONCURRENT_SOURCES=2
|
|
58
|
+
# BATCH_DELAY=0
|
|
59
|
+
|
|
60
|
+
# For 1 API Instance (Single Instance Setup):
|
|
61
|
+
# MAX_API_CONNECTIONS=5
|
|
62
|
+
# BATCH_SIZE=20
|
|
63
|
+
# MAX_CONCURRENT_SOURCES=1
|
|
64
|
+
# BATCH_DELAY=100
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# LOGGING AND MONITORING
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
# Progress bar update frequency
|
|
71
|
+
PROGRESS_UPDATE_INTERVAL=10
|
|
16
72
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
# Example: MMJ0810145N1|ABC1234567XY|DEF9876543ZZ
|
|
20
|
-
UPLOAD_RFCS=RFC1|RFC2|RFC3
|
|
73
|
+
# Enable verbose logging (true/false)
|
|
74
|
+
VERBOSE_LOGGING=false
|
package/package.json
CHANGED
|
@@ -50,7 +50,7 @@ export class UploadCommand {
|
|
|
50
50
|
logger.info('Log file cleared');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Process each source
|
|
53
|
+
// Process each source with configurable concurrency
|
|
54
54
|
let globalResults = {
|
|
55
55
|
successCount: 0,
|
|
56
56
|
detectedCount: 0,
|
|
@@ -59,28 +59,88 @@ export class UploadCommand {
|
|
|
59
59
|
skippedCount: 0,
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
files,
|
|
72
|
-
options,
|
|
73
|
-
uploadService,
|
|
74
|
-
basePath,
|
|
75
|
-
source,
|
|
76
|
-
sourcePath,
|
|
77
|
-
);
|
|
62
|
+
// Determine processing strategy based on configuration
|
|
63
|
+
const maxConcurrentSources =
|
|
64
|
+
appConfig.performance?.maxConcurrentSources || 1;
|
|
65
|
+
|
|
66
|
+
if (maxConcurrentSources > 1 && sources.length > 1) {
|
|
67
|
+
// Parallel source processing
|
|
68
|
+
logger.info(
|
|
69
|
+
`Processing ${sources.length} sources with concurrency: ${maxConcurrentSources}`,
|
|
70
|
+
);
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
// Process sources in batches to control concurrency
|
|
73
|
+
for (let i = 0; i < sources.length; i += maxConcurrentSources) {
|
|
74
|
+
const sourceBatch = sources.slice(i, i + maxConcurrentSources);
|
|
75
|
+
|
|
76
|
+
const sourcePromises = sourceBatch.map(async (source) => {
|
|
77
|
+
const sourcePath = path
|
|
78
|
+
.resolve(basePath, source)
|
|
79
|
+
.replace(/\\/g, '/');
|
|
80
|
+
logger.info(`Processing folder: ${sourcePath}`);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const files = await this.#discoverFiles(sourcePath);
|
|
84
|
+
logger.info(`Found ${files.length} files in ${source}`);
|
|
85
|
+
|
|
86
|
+
const result = await this.#processFilesInBatches(
|
|
87
|
+
files,
|
|
88
|
+
options,
|
|
89
|
+
uploadService,
|
|
90
|
+
basePath,
|
|
91
|
+
source,
|
|
92
|
+
sourcePath,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
this.#logSourceSummary(source, result, options);
|
|
96
|
+
return { success: true, source, result };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.errorHandler.handleError(error, { source, sourcePath });
|
|
99
|
+
return { success: false, source, error: error.message };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Wait for this batch of sources to complete
|
|
104
|
+
const results = await Promise.allSettled(sourcePromises);
|
|
105
|
+
|
|
106
|
+
results.forEach((result) => {
|
|
107
|
+
if (result.status === 'fulfilled') {
|
|
108
|
+
const sourceResult = result.value;
|
|
109
|
+
if (sourceResult.success) {
|
|
110
|
+
this.#updateGlobalResults(globalResults, sourceResult.result);
|
|
111
|
+
} else {
|
|
112
|
+
globalResults.failureCount++;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
globalResults.failureCount++;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// Sequential source processing (original behavior)
|
|
121
|
+
for (const source of sources) {
|
|
122
|
+
const sourcePath = path.resolve(basePath, source).replace(/\\/g, '/');
|
|
123
|
+
logger.info(`Processing folder: ${sourcePath}`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const files = await this.#discoverFiles(sourcePath);
|
|
127
|
+
logger.info(`Found ${files.length} files to process`);
|
|
128
|
+
|
|
129
|
+
const result = await this.#processFilesInBatches(
|
|
130
|
+
files,
|
|
131
|
+
options,
|
|
132
|
+
uploadService,
|
|
133
|
+
basePath,
|
|
134
|
+
source,
|
|
135
|
+
sourcePath,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
this.#updateGlobalResults(globalResults, result);
|
|
139
|
+
this.#logSourceSummary(source, result, options);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.errorHandler.handleError(error, { source, sourcePath });
|
|
142
|
+
globalResults.failureCount++;
|
|
143
|
+
}
|
|
84
144
|
}
|
|
85
145
|
}
|
|
86
146
|
|
|
@@ -164,7 +224,8 @@ export class UploadCommand {
|
|
|
164
224
|
source,
|
|
165
225
|
sourcePath,
|
|
166
226
|
) {
|
|
167
|
-
const batchSize =
|
|
227
|
+
const batchSize =
|
|
228
|
+
parseInt(options.batchSize) || appConfig.performance.batchSize || 50;
|
|
168
229
|
const results = {
|
|
169
230
|
successCount: 0,
|
|
170
231
|
detectedCount: 0,
|
|
@@ -184,6 +245,9 @@ export class UploadCommand {
|
|
|
184
245
|
barCompleteChar: '█',
|
|
185
246
|
barIncompleteChar: '░',
|
|
186
247
|
hideCursor: true,
|
|
248
|
+
clearOnComplete: false,
|
|
249
|
+
stopOnComplete: true,
|
|
250
|
+
stream: process.stderr, // Use stderr to separate from stdout logging
|
|
187
251
|
});
|
|
188
252
|
|
|
189
253
|
progressBar.start(files.length, 0, { success: 0, errors: 0 });
|
|
@@ -265,22 +329,65 @@ export class UploadCommand {
|
|
|
265
329
|
throw new Error(`Failed to insert stats: ${error.message}`);
|
|
266
330
|
}
|
|
267
331
|
} else {
|
|
268
|
-
// Upload mode: process files
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
332
|
+
// Upload mode: process files with controlled concurrency to match API replicas
|
|
333
|
+
const maxConcurrentApiCalls =
|
|
334
|
+
appConfig.performance?.maxApiConnections || 10;
|
|
335
|
+
|
|
336
|
+
// Process batch in chunks to respect API replica limits
|
|
337
|
+
const allResults = [];
|
|
338
|
+
for (let i = 0; i < batch.length; i += maxConcurrentApiCalls) {
|
|
339
|
+
const chunk = batch.slice(i, i + maxConcurrentApiCalls);
|
|
340
|
+
|
|
341
|
+
// Process this chunk concurrently (up to API replica count)
|
|
342
|
+
const chunkPromises = chunk.map(async (filePath) => {
|
|
343
|
+
try {
|
|
344
|
+
const result = await this.#processFile(
|
|
345
|
+
filePath,
|
|
346
|
+
options,
|
|
347
|
+
uploadService,
|
|
348
|
+
basePath,
|
|
349
|
+
processedPaths,
|
|
350
|
+
);
|
|
351
|
+
return { success: true, filePath, result };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.errorHandler.handleError(error, { filePath });
|
|
354
|
+
return { success: false, filePath, error: error.message };
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Wait for this chunk to complete before starting the next
|
|
359
|
+
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
360
|
+
allResults.push(...chunkResults);
|
|
361
|
+
|
|
362
|
+
// Small delay between chunks to prevent overwhelming API
|
|
363
|
+
if (i + maxConcurrentApiCalls < batch.length) {
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
282
365
|
}
|
|
283
366
|
}
|
|
367
|
+
|
|
368
|
+
// Process all results and update batch results
|
|
369
|
+
allResults.forEach((result) => {
|
|
370
|
+
if (result.status === 'fulfilled') {
|
|
371
|
+
const fileResult = result.value;
|
|
372
|
+
if (fileResult.success) {
|
|
373
|
+
if (fileResult.result && fileResult.result.skipped) {
|
|
374
|
+
batchResults.skippedCount++;
|
|
375
|
+
} else {
|
|
376
|
+
batchResults.successCount++;
|
|
377
|
+
if (fileResult.result && fileResult.result.detectedCount) {
|
|
378
|
+
batchResults.detectedCount += fileResult.result.detectedCount;
|
|
379
|
+
}
|
|
380
|
+
if (fileResult.result && fileResult.result.organizedCount) {
|
|
381
|
+
batchResults.organizedCount += fileResult.result.organizedCount;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
batchResults.failureCount++;
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
batchResults.failureCount++;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
284
391
|
}
|
|
285
392
|
|
|
286
393
|
return batchResults;
|
|
@@ -296,12 +403,10 @@ export class UploadCommand {
|
|
|
296
403
|
uploadService,
|
|
297
404
|
basePath,
|
|
298
405
|
processedPaths,
|
|
299
|
-
batchResults,
|
|
300
406
|
) {
|
|
301
407
|
// Skip if already processed
|
|
302
408
|
if (processedPaths.has(filePath)) {
|
|
303
|
-
|
|
304
|
-
return;
|
|
409
|
+
return { skipped: true };
|
|
305
410
|
}
|
|
306
411
|
|
|
307
412
|
// Prepare file for upload
|
|
@@ -325,24 +430,25 @@ export class UploadCommand {
|
|
|
325
430
|
};
|
|
326
431
|
|
|
327
432
|
// Upload based on service type
|
|
433
|
+
let result = { successCount: 1 };
|
|
434
|
+
|
|
328
435
|
if (uploadService.getServiceName() === 'Arela API') {
|
|
329
|
-
|
|
436
|
+
result = await uploadService.upload([fileObject], {
|
|
330
437
|
...options,
|
|
331
438
|
uploadPath,
|
|
332
439
|
});
|
|
333
|
-
|
|
334
|
-
batchResults.successCount++;
|
|
335
|
-
if (result.detectedCount)
|
|
336
|
-
batchResults.detectedCount += result.detectedCount;
|
|
337
|
-
if (result.organizedCount)
|
|
338
|
-
batchResults.organizedCount += result.organizedCount;
|
|
339
440
|
} else {
|
|
340
441
|
// Supabase direct upload
|
|
341
442
|
await uploadService.upload([fileObject], { uploadPath });
|
|
342
|
-
batchResults.successCount++;
|
|
343
443
|
}
|
|
344
444
|
|
|
345
445
|
logger.info(`SUCCESS: ${path.basename(filePath)} -> ${uploadPath}`);
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
skipped: false,
|
|
449
|
+
detectedCount: result.detectedCount || 0,
|
|
450
|
+
organizedCount: result.organizedCount || 0,
|
|
451
|
+
};
|
|
346
452
|
}
|
|
347
453
|
|
|
348
454
|
/**
|
|
@@ -428,7 +534,8 @@ export class UploadCommand {
|
|
|
428
534
|
// Phase 2: PDF Detection
|
|
429
535
|
console.log('\n🔍 === PHASE 2: PDF Detection ===');
|
|
430
536
|
const detectionResult = await databaseService.detectPedimentosInDatabase({
|
|
431
|
-
batchSize:
|
|
537
|
+
batchSize:
|
|
538
|
+
parseInt(options.batchSize) || appConfig.performance.batchSize || 50,
|
|
432
539
|
});
|
|
433
540
|
console.log(
|
|
434
541
|
`✅ Phase 2 Complete: ${detectionResult.detectedCount} detected, ${detectionResult.errorCount} errors`,
|
package/src/config/config.js
CHANGED
|
@@ -28,10 +28,10 @@ class Config {
|
|
|
28
28
|
const __dirname = path.dirname(__filename);
|
|
29
29
|
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
30
30
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
31
|
-
return packageJson.version || '0.2.
|
|
31
|
+
return packageJson.version || '0.2.8';
|
|
32
32
|
} catch (error) {
|
|
33
33
|
console.warn('⚠️ Could not read package.json version, using fallback');
|
|
34
|
-
return '0.2.
|
|
34
|
+
return '0.2.8';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -85,7 +85,12 @@ class Config {
|
|
|
85
85
|
*/
|
|
86
86
|
#loadPerformanceConfig() {
|
|
87
87
|
return {
|
|
88
|
-
batchDelay: parseInt(process.env.BATCH_DELAY) ||
|
|
88
|
+
batchDelay: parseInt(process.env.BATCH_DELAY) || 0, // Removed default delay
|
|
89
|
+
batchSize: parseInt(process.env.BATCH_SIZE) || 50, // Increased from 10 to 50
|
|
90
|
+
maxConcurrentSources: parseInt(process.env.MAX_CONCURRENT_SOURCES) || 2,
|
|
91
|
+
maxApiConnections: parseInt(process.env.MAX_API_CONNECTIONS) || 10, // New: API replica support
|
|
92
|
+
apiConnectionTimeout:
|
|
93
|
+
parseInt(process.env.API_CONNECTION_TIMEOUT) || 60000, // New: API timeout
|
|
89
94
|
progressUpdateInterval:
|
|
90
95
|
parseInt(process.env.PROGRESS_UPDATE_INTERVAL) || 10,
|
|
91
96
|
logBufferSize: 100,
|
|
@@ -109,7 +109,7 @@ export class DatabaseService {
|
|
|
109
109
|
.toLowerCase()
|
|
110
110
|
.replace('.', '');
|
|
111
111
|
const filename = file.originalName || path.basename(file.path);
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
const record = {
|
|
114
114
|
document_type: null,
|
|
115
115
|
size: stats.size,
|
|
@@ -121,8 +121,8 @@ export class DatabaseService {
|
|
|
121
121
|
rfc: null,
|
|
122
122
|
message: null,
|
|
123
123
|
file_extension: fileExtension,
|
|
124
|
-
is_like_simplificado:
|
|
125
|
-
filename.toLowerCase().includes('simp'),
|
|
124
|
+
is_like_simplificado:
|
|
125
|
+
fileExtension === 'pdf' && filename.toLowerCase().includes('simp'),
|
|
126
126
|
year: null,
|
|
127
127
|
created_at: new Date().toISOString(),
|
|
128
128
|
modified_at: stats.mtime.toISOString(),
|
|
@@ -226,8 +226,11 @@ export class DatabaseService {
|
|
|
226
226
|
file_extension: fileExtension,
|
|
227
227
|
created_at: new Date().toISOString(),
|
|
228
228
|
modified_at: stats.mtime.toISOString(),
|
|
229
|
-
is_like_simplificado:
|
|
230
|
-
|
|
229
|
+
is_like_simplificado:
|
|
230
|
+
fileExtension === 'pdf' &&
|
|
231
|
+
(file.originalName || path.basename(file.path))
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.includes('simp'),
|
|
231
234
|
year: null,
|
|
232
235
|
};
|
|
233
236
|
|
|
@@ -277,9 +280,15 @@ export class DatabaseService {
|
|
|
277
280
|
existingPaths.has(r.original_path),
|
|
278
281
|
);
|
|
279
282
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
// Only log every 10th batch to reduce noise
|
|
284
|
+
if (
|
|
285
|
+
(Math.floor(i / batchSize) + 1) % 10 === 0 ||
|
|
286
|
+
Math.floor(i / batchSize) + 1 === 1
|
|
287
|
+
) {
|
|
288
|
+
logger.info(
|
|
289
|
+
`Batch ${Math.floor(i / batchSize) + 1}: ${newRecords.length} new, ${updateRecords.length} updates`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
283
292
|
|
|
284
293
|
// Insert new records
|
|
285
294
|
if (newRecords.length > 0) {
|
|
@@ -291,7 +300,7 @@ export class DatabaseService {
|
|
|
291
300
|
logger.error(`Error inserting new records: ${insertError.message}`);
|
|
292
301
|
} else {
|
|
293
302
|
totalInserted += newRecords.length;
|
|
294
|
-
|
|
303
|
+
// Only log the batch insertion, not the summary (which comes at the end)
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
306
|
|
|
@@ -316,7 +325,10 @@ export class DatabaseService {
|
|
|
316
325
|
}
|
|
317
326
|
}
|
|
318
327
|
totalUpdated += batchUpdated;
|
|
319
|
-
|
|
328
|
+
// Reduce logging noise - only log when there are updates
|
|
329
|
+
if (batchUpdated > 0) {
|
|
330
|
+
logger.info(`Updated ${batchUpdated} existing records`);
|
|
331
|
+
}
|
|
320
332
|
}
|
|
321
333
|
} catch (error) {
|
|
322
334
|
logger.error(
|
|
@@ -545,7 +557,9 @@ export class DatabaseService {
|
|
|
545
557
|
const supabase = await this.#getSupabaseClient();
|
|
546
558
|
|
|
547
559
|
logger.info('Phase 3: Starting arela_path and year propagation process...');
|
|
548
|
-
console.log(
|
|
560
|
+
console.log(
|
|
561
|
+
'🔍 Finding pedimento_simplificado records with arela_path and year...',
|
|
562
|
+
);
|
|
549
563
|
|
|
550
564
|
// Get all pedimento_simplificado records that have arela_path
|
|
551
565
|
const { data: pedimentoRecords, error: pedimentoError } = await supabase
|
|
@@ -640,9 +654,9 @@ export class DatabaseService {
|
|
|
640
654
|
try {
|
|
641
655
|
const { error: updateError } = await supabase
|
|
642
656
|
.from('uploader')
|
|
643
|
-
.update({
|
|
657
|
+
.update({
|
|
644
658
|
arela_path: folderArelaPath,
|
|
645
|
-
year: pedimento.year
|
|
659
|
+
year: pedimento.year,
|
|
646
660
|
})
|
|
647
661
|
.in('id', batchIds);
|
|
648
662
|
|
|
@@ -885,133 +899,54 @@ export class DatabaseService {
|
|
|
885
899
|
console.log(`📋 Total files to upload: ${allRelatedFiles.length}`);
|
|
886
900
|
logger.info(`Total files to upload: ${allRelatedFiles.length}`);
|
|
887
901
|
|
|
888
|
-
// Step 4: Upload all related files
|
|
902
|
+
// Step 4: Upload all related files using concurrent batch processing
|
|
889
903
|
let totalProcessed = 0;
|
|
890
904
|
let totalUploaded = 0;
|
|
891
905
|
let totalErrors = 0;
|
|
892
906
|
const batchSize = parseInt(options.batchSize) || 10;
|
|
893
907
|
|
|
894
|
-
|
|
895
|
-
|
|
908
|
+
// Import performance configuration
|
|
909
|
+
const { performance: perfConfig } = appConfig;
|
|
910
|
+
const maxConcurrency = perfConfig?.maxApiConnections || 3;
|
|
896
911
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
logger.warn(
|
|
904
|
-
`File not found: ${file.filename} at ${file.original_path}`,
|
|
905
|
-
);
|
|
906
|
-
await supabase
|
|
907
|
-
.from('uploader')
|
|
908
|
-
.update({
|
|
909
|
-
status: 'file-not-found',
|
|
910
|
-
message: 'File no longer exists at original path',
|
|
911
|
-
})
|
|
912
|
-
.eq('id', file.id);
|
|
913
|
-
totalErrors++;
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Upload the file (handle both API and Supabase services)
|
|
918
|
-
let uploadResult;
|
|
919
|
-
if (uploadService.getServiceName() === 'Supabase') {
|
|
920
|
-
// Supabase requires single file upload with uploadPath
|
|
921
|
-
let uploadPath;
|
|
922
|
-
if (options.folderStructure && file.arela_path) {
|
|
923
|
-
// Combine folder structure with arela_path: palco/RFC/Year/Patente/Aduana/Pedimento/filename
|
|
924
|
-
uploadPath = `uploads/${options.folderStructure}/${file.arela_path}${file.filename}`;
|
|
925
|
-
} else if (file.arela_path) {
|
|
926
|
-
// Use existing arela_path: RFC/Year/Patente/Aduana/Pedimento/filename
|
|
927
|
-
uploadPath = `uploads/${file.arela_path}${file.filename}`;
|
|
928
|
-
} else {
|
|
929
|
-
// Fallback to RFC folder
|
|
930
|
-
uploadPath = `uploads/${file.rfc}/${file.filename}`;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
uploadResult = await uploadService.upload(
|
|
934
|
-
[
|
|
935
|
-
{
|
|
936
|
-
path: file.original_path,
|
|
937
|
-
name: file.filename,
|
|
938
|
-
contentType: 'application/octet-stream',
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
{
|
|
942
|
-
uploadPath: uploadPath,
|
|
943
|
-
},
|
|
944
|
-
);
|
|
945
|
-
uploadResult = { success: true, data: uploadResult };
|
|
946
|
-
} else {
|
|
947
|
-
// API service supports batch uploads and returns normalized response
|
|
948
|
-
let fullFolderStructure;
|
|
949
|
-
if (options.folderStructure && file.arela_path) {
|
|
950
|
-
// Combine folder structure with arela_path: palco/RFC/Year/Patente/Aduana/Pedimento/
|
|
951
|
-
fullFolderStructure = `${options.folderStructure}/${file.arela_path}`;
|
|
952
|
-
} else if (file.arela_path) {
|
|
953
|
-
// Use existing arela_path: RFC/Year/Patente/Aduana/Pedimento/
|
|
954
|
-
fullFolderStructure = file.arela_path;
|
|
955
|
-
} else {
|
|
956
|
-
// Fallback to RFC folder
|
|
957
|
-
fullFolderStructure = `${file.rfc}/`;
|
|
958
|
-
}
|
|
912
|
+
console.log(
|
|
913
|
+
`🚀 Starting batch upload: ${allRelatedFiles.length} files in batches of ${batchSize}`,
|
|
914
|
+
);
|
|
915
|
+
console.log(
|
|
916
|
+
`⚡ Concurrent processing: up to ${maxConcurrency} parallel operations`,
|
|
917
|
+
);
|
|
959
918
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
contentType: 'application/octet-stream',
|
|
966
|
-
},
|
|
967
|
-
],
|
|
968
|
-
{
|
|
969
|
-
folderStructure: fullFolderStructure,
|
|
970
|
-
},
|
|
971
|
-
);
|
|
972
|
-
}
|
|
919
|
+
// Process files in batches with concurrent processing
|
|
920
|
+
for (let i = 0; i < allRelatedFiles.length; i += batchSize) {
|
|
921
|
+
const batch = allRelatedFiles.slice(i, i + batchSize);
|
|
922
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
923
|
+
const totalBatches = Math.ceil(allRelatedFiles.length / batchSize);
|
|
973
924
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
.from('uploader')
|
|
978
|
-
.update({
|
|
979
|
-
status: 'file-uploaded',
|
|
980
|
-
message: 'Successfully uploaded to Arela API',
|
|
981
|
-
})
|
|
982
|
-
.eq('id', file.id);
|
|
925
|
+
console.log(
|
|
926
|
+
`📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} files)`,
|
|
927
|
+
);
|
|
983
928
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
})
|
|
993
|
-
.eq('id', file.id);
|
|
929
|
+
// Process batch using concurrent processing similar to UploadCommand
|
|
930
|
+
const batchResults = await this.#processRfcBatch(
|
|
931
|
+
batch,
|
|
932
|
+
uploadService,
|
|
933
|
+
supabase,
|
|
934
|
+
options,
|
|
935
|
+
maxConcurrency,
|
|
936
|
+
);
|
|
994
937
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
totalErrors++;
|
|
1002
|
-
logger.error(
|
|
1003
|
-
`Error processing file ${file.filename}: ${error.message}`,
|
|
1004
|
-
);
|
|
938
|
+
totalProcessed += batchResults.processed;
|
|
939
|
+
totalUploaded += batchResults.uploaded;
|
|
940
|
+
totalErrors += batchResults.errors;
|
|
1005
941
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
942
|
+
// Progress update
|
|
943
|
+
const progress = (
|
|
944
|
+
((i + batch.length) / allRelatedFiles.length) *
|
|
945
|
+
100
|
|
946
|
+
).toFixed(1);
|
|
947
|
+
console.log(
|
|
948
|
+
`📊 Batch ${batchNum} complete - Progress: ${progress}% (${totalUploaded}/${allRelatedFiles.length} uploaded)`,
|
|
949
|
+
);
|
|
1015
950
|
}
|
|
1016
951
|
|
|
1017
952
|
const result = {
|
|
@@ -1158,6 +1093,298 @@ export class DatabaseService {
|
|
|
1158
1093
|
|
|
1159
1094
|
return readyFiles || [];
|
|
1160
1095
|
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Process a batch of files using concurrent processing for RFC uploads
|
|
1099
|
+
* @param {Array} files - Files to process in this batch
|
|
1100
|
+
* @param {Object} uploadService - Upload service instance
|
|
1101
|
+
* @param {Object} supabase - Supabase client
|
|
1102
|
+
* @param {Object} options - Upload options
|
|
1103
|
+
* @param {number} maxConcurrency - Maximum concurrent operations
|
|
1104
|
+
* @returns {Promise<Object>} Batch processing results
|
|
1105
|
+
*/
|
|
1106
|
+
async #processRfcBatch(
|
|
1107
|
+
files,
|
|
1108
|
+
uploadService,
|
|
1109
|
+
supabase,
|
|
1110
|
+
options,
|
|
1111
|
+
maxConcurrency,
|
|
1112
|
+
) {
|
|
1113
|
+
const fs = (await import('fs')).default;
|
|
1114
|
+
|
|
1115
|
+
let processed = 0;
|
|
1116
|
+
let uploaded = 0;
|
|
1117
|
+
let errors = 0;
|
|
1118
|
+
|
|
1119
|
+
// For Supabase, process files individually (required by service)
|
|
1120
|
+
if (uploadService.getServiceName() === 'Supabase') {
|
|
1121
|
+
// Process files in concurrent chunks within the batch
|
|
1122
|
+
const chunks = [];
|
|
1123
|
+
for (let i = 0; i < files.length; i += maxConcurrency) {
|
|
1124
|
+
chunks.push(files.slice(i, i + maxConcurrency));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Process each chunk concurrently
|
|
1128
|
+
for (const chunk of chunks) {
|
|
1129
|
+
const chunkPromises = chunk.map(async (file) => {
|
|
1130
|
+
return await this.#processRfcSingleFile(
|
|
1131
|
+
file,
|
|
1132
|
+
uploadService,
|
|
1133
|
+
supabase,
|
|
1134
|
+
options,
|
|
1135
|
+
fs,
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Wait for all files in this chunk to complete
|
|
1140
|
+
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
1141
|
+
|
|
1142
|
+
// Count results
|
|
1143
|
+
for (const result of chunkResults) {
|
|
1144
|
+
processed++;
|
|
1145
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
1146
|
+
uploaded++;
|
|
1147
|
+
} else {
|
|
1148
|
+
errors++;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
// For API service, use true batch processing (multiple files per API call)
|
|
1154
|
+
const apiChunks = [];
|
|
1155
|
+
const apiChunkSize = Math.min(
|
|
1156
|
+
5,
|
|
1157
|
+
Math.ceil(files.length / maxConcurrency),
|
|
1158
|
+
); // 5 files per API call, or distribute evenly
|
|
1159
|
+
|
|
1160
|
+
for (let i = 0; i < files.length; i += apiChunkSize) {
|
|
1161
|
+
apiChunks.push(files.slice(i, i + apiChunkSize));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
console.log(
|
|
1165
|
+
` 🚀 Processing ${apiChunks.length} API calls with ${apiChunkSize} files each (max ${maxConcurrency} concurrent)`,
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
// Process API chunks with controlled concurrency
|
|
1169
|
+
const concurrentChunks = [];
|
|
1170
|
+
for (let i = 0; i < apiChunks.length; i += maxConcurrency) {
|
|
1171
|
+
concurrentChunks.push(apiChunks.slice(i, i + maxConcurrency));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
for (const concurrentSet of concurrentChunks) {
|
|
1175
|
+
const batchPromises = concurrentSet.map(async (chunk) => {
|
|
1176
|
+
return await this.#processRfcApiBatch(
|
|
1177
|
+
chunk,
|
|
1178
|
+
uploadService,
|
|
1179
|
+
supabase,
|
|
1180
|
+
options,
|
|
1181
|
+
fs,
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// Wait for all concurrent batches to complete
|
|
1186
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
1187
|
+
|
|
1188
|
+
// Count results
|
|
1189
|
+
for (const result of batchResults) {
|
|
1190
|
+
if (result.status === 'fulfilled') {
|
|
1191
|
+
processed += result.value.processed;
|
|
1192
|
+
uploaded += result.value.uploaded;
|
|
1193
|
+
errors += result.value.errors;
|
|
1194
|
+
} else {
|
|
1195
|
+
errors += result.value?.processed || 0;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return { processed, uploaded, errors };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Process a single file for RFC upload (Supabase mode)
|
|
1206
|
+
*/
|
|
1207
|
+
async #processRfcSingleFile(file, uploadService, supabase, options, fs) {
|
|
1208
|
+
try {
|
|
1209
|
+
// Check if file exists
|
|
1210
|
+
if (!fs.existsSync(file.original_path)) {
|
|
1211
|
+
logger.warn(
|
|
1212
|
+
`File not found: ${file.filename} at ${file.original_path}`,
|
|
1213
|
+
);
|
|
1214
|
+
await supabase
|
|
1215
|
+
.from('uploader')
|
|
1216
|
+
.update({
|
|
1217
|
+
status: 'file-not-found',
|
|
1218
|
+
message: 'File no longer exists at original path',
|
|
1219
|
+
})
|
|
1220
|
+
.eq('id', file.id);
|
|
1221
|
+
return { success: false, error: 'File not found' };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Supabase requires single file upload with uploadPath
|
|
1225
|
+
let uploadPath;
|
|
1226
|
+
if (options.folderStructure && file.arela_path) {
|
|
1227
|
+
uploadPath = `uploads/${options.folderStructure}/${file.arela_path}${file.filename}`;
|
|
1228
|
+
} else if (file.arela_path) {
|
|
1229
|
+
uploadPath = `uploads/${file.arela_path}${file.filename}`;
|
|
1230
|
+
} else {
|
|
1231
|
+
uploadPath = `uploads/${file.rfc}/${file.filename}`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const uploadResult = await uploadService.upload(
|
|
1235
|
+
[
|
|
1236
|
+
{
|
|
1237
|
+
path: file.original_path,
|
|
1238
|
+
name: file.filename,
|
|
1239
|
+
contentType: 'application/octet-stream',
|
|
1240
|
+
},
|
|
1241
|
+
],
|
|
1242
|
+
{ uploadPath: uploadPath },
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
// Update database status
|
|
1246
|
+
await supabase
|
|
1247
|
+
.from('uploader')
|
|
1248
|
+
.update({
|
|
1249
|
+
status: 'file-uploaded',
|
|
1250
|
+
message: 'Successfully uploaded to Supabase',
|
|
1251
|
+
})
|
|
1252
|
+
.eq('id', file.id);
|
|
1253
|
+
|
|
1254
|
+
logger.info(`✅ Uploaded: ${file.filename}`);
|
|
1255
|
+
return { success: true, filename: file.filename };
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
logger.error(
|
|
1258
|
+
`❌ Error processing file ${file.filename}: ${error.message}`,
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
await supabase
|
|
1262
|
+
.from('uploader')
|
|
1263
|
+
.update({
|
|
1264
|
+
status: 'upload-error',
|
|
1265
|
+
message: `Processing error: ${error.message}`,
|
|
1266
|
+
})
|
|
1267
|
+
.eq('id', file.id);
|
|
1268
|
+
|
|
1269
|
+
return { success: false, error: error.message, filename: file.filename };
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Process multiple files in a single API batch call (API service mode)
|
|
1275
|
+
*/
|
|
1276
|
+
async #processRfcApiBatch(files, uploadService, supabase, options, fs) {
|
|
1277
|
+
let processed = 0;
|
|
1278
|
+
let uploaded = 0;
|
|
1279
|
+
let errors = 0;
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
// Prepare files for batch upload
|
|
1283
|
+
const validFiles = [];
|
|
1284
|
+
const invalidFiles = [];
|
|
1285
|
+
|
|
1286
|
+
for (const file of files) {
|
|
1287
|
+
processed++;
|
|
1288
|
+
|
|
1289
|
+
if (!fs.existsSync(file.original_path)) {
|
|
1290
|
+
logger.warn(
|
|
1291
|
+
`File not found: ${file.filename} at ${file.original_path}`,
|
|
1292
|
+
);
|
|
1293
|
+
invalidFiles.push(file);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
validFiles.push({
|
|
1298
|
+
fileData: {
|
|
1299
|
+
path: file.original_path,
|
|
1300
|
+
name: file.filename,
|
|
1301
|
+
contentType: 'application/octet-stream',
|
|
1302
|
+
},
|
|
1303
|
+
dbRecord: file,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Update invalid files in database
|
|
1308
|
+
for (const file of invalidFiles) {
|
|
1309
|
+
await supabase
|
|
1310
|
+
.from('uploader')
|
|
1311
|
+
.update({
|
|
1312
|
+
status: 'file-not-found',
|
|
1313
|
+
message: 'File no longer exists at original path',
|
|
1314
|
+
})
|
|
1315
|
+
.eq('id', file.id);
|
|
1316
|
+
errors++;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Process valid files in batch if any exist
|
|
1320
|
+
if (validFiles.length > 0) {
|
|
1321
|
+
// Determine folder structure (all files in this batch should have same arela_path)
|
|
1322
|
+
const sampleFile = validFiles[0].dbRecord;
|
|
1323
|
+
let fullFolderStructure;
|
|
1324
|
+
if (options.folderStructure && sampleFile.arela_path) {
|
|
1325
|
+
fullFolderStructure = `${options.folderStructure}/${sampleFile.arela_path}`;
|
|
1326
|
+
} else if (sampleFile.arela_path) {
|
|
1327
|
+
fullFolderStructure = sampleFile.arela_path;
|
|
1328
|
+
} else {
|
|
1329
|
+
fullFolderStructure = `${sampleFile.rfc}/`;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Make single API call with multiple files
|
|
1333
|
+
const uploadResult = await uploadService.upload(
|
|
1334
|
+
validFiles.map((f) => f.fileData),
|
|
1335
|
+
{ folderStructure: fullFolderStructure },
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
if (uploadResult.success) {
|
|
1339
|
+
// Update all files as uploaded
|
|
1340
|
+
const fileIds = validFiles.map((f) => f.dbRecord.id);
|
|
1341
|
+
await supabase
|
|
1342
|
+
.from('uploader')
|
|
1343
|
+
.update({
|
|
1344
|
+
status: 'file-uploaded',
|
|
1345
|
+
message: 'Successfully uploaded to Arela API (batch)',
|
|
1346
|
+
})
|
|
1347
|
+
.in('id', fileIds);
|
|
1348
|
+
|
|
1349
|
+
uploaded += validFiles.length;
|
|
1350
|
+
logger.info(
|
|
1351
|
+
`✅ Batch uploaded: ${validFiles.length} files to ${fullFolderStructure}`,
|
|
1352
|
+
);
|
|
1353
|
+
} else {
|
|
1354
|
+
// Update all files as failed
|
|
1355
|
+
const fileIds = validFiles.map((f) => f.dbRecord.id);
|
|
1356
|
+
await supabase
|
|
1357
|
+
.from('uploader')
|
|
1358
|
+
.update({
|
|
1359
|
+
status: 'upload-error',
|
|
1360
|
+
message: uploadResult.error || 'Batch upload failed',
|
|
1361
|
+
})
|
|
1362
|
+
.in('id', fileIds);
|
|
1363
|
+
|
|
1364
|
+
errors += validFiles.length;
|
|
1365
|
+
logger.error(
|
|
1366
|
+
`❌ Batch upload failed: ${validFiles.length} files - ${uploadResult.error}`,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
logger.error(`❌ Error processing batch: ${error.message}`);
|
|
1372
|
+
|
|
1373
|
+
// Mark all files as failed
|
|
1374
|
+
const fileIds = files.map((f) => f.id);
|
|
1375
|
+
await supabase
|
|
1376
|
+
.from('uploader')
|
|
1377
|
+
.update({
|
|
1378
|
+
status: 'upload-error',
|
|
1379
|
+
message: `Batch processing error: ${error.message}`,
|
|
1380
|
+
})
|
|
1381
|
+
.in('id', fileIds);
|
|
1382
|
+
|
|
1383
|
+
errors += files.length;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return { processed, uploaded, errors };
|
|
1387
|
+
}
|
|
1161
1388
|
}
|
|
1162
1389
|
|
|
1163
1390
|
// Export singleton instance
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Blob } from 'buffer';
|
|
2
2
|
import { FormData } from 'formdata-node';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
import { Agent } from 'http';
|
|
5
|
+
import { Agent as HttpsAgent } from 'https';
|
|
4
6
|
import fetch from 'node-fetch';
|
|
5
7
|
import path from 'path';
|
|
6
8
|
|
|
@@ -16,6 +18,36 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
16
18
|
super();
|
|
17
19
|
this.baseUrl = appConfig.api.baseUrl;
|
|
18
20
|
this.token = appConfig.api.token;
|
|
21
|
+
|
|
22
|
+
// Get API connection settings from config/environment
|
|
23
|
+
const maxApiConnections = parseInt(process.env.MAX_API_CONNECTIONS) || 10;
|
|
24
|
+
const connectionTimeout =
|
|
25
|
+
parseInt(process.env.API_CONNECTION_TIMEOUT) || 60000;
|
|
26
|
+
|
|
27
|
+
// Initialize HTTP agents optimized for multiple API replicas
|
|
28
|
+
this.httpAgent = new Agent({
|
|
29
|
+
keepAlive: true,
|
|
30
|
+
keepAliveMsecs: 30000,
|
|
31
|
+
maxSockets: maxApiConnections, // Match your API replica count
|
|
32
|
+
maxFreeSockets: Math.ceil(maxApiConnections / 2),
|
|
33
|
+
maxTotalSockets: maxApiConnections + 5, // Buffer for peak usage
|
|
34
|
+
timeout: connectionTimeout,
|
|
35
|
+
scheduling: 'fifo', // First-in-first-out scheduling
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.httpsAgent = new HttpsAgent({
|
|
39
|
+
keepAlive: true,
|
|
40
|
+
keepAliveMsecs: 30000,
|
|
41
|
+
maxSockets: maxApiConnections, // Match your API replica count
|
|
42
|
+
maxFreeSockets: Math.ceil(maxApiConnections / 2),
|
|
43
|
+
maxTotalSockets: maxApiConnections + 5, // Buffer for peak usage
|
|
44
|
+
timeout: connectionTimeout,
|
|
45
|
+
scheduling: 'fifo', // First-in-first-out scheduling
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
`🔗 HTTP Agent configured for ${maxApiConnections} concurrent API connections`,
|
|
50
|
+
);
|
|
19
51
|
}
|
|
20
52
|
|
|
21
53
|
/**
|
|
@@ -27,12 +59,31 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
27
59
|
async upload(files, options) {
|
|
28
60
|
const formData = new FormData();
|
|
29
61
|
|
|
30
|
-
// Add files to form data
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
// Add files to form data asynchronously
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
try {
|
|
65
|
+
// Check file size for streaming vs buffer approach
|
|
66
|
+
const stats = await fs.promises.stat(file.path);
|
|
67
|
+
const fileSizeThreshold = 10 * 1024 * 1024; // 10MB threshold
|
|
68
|
+
|
|
69
|
+
if (stats.size > fileSizeThreshold) {
|
|
70
|
+
// Use streaming for large files
|
|
71
|
+
const fileStream = fs.createReadStream(file.path);
|
|
72
|
+
formData.append('files', fileStream, {
|
|
73
|
+
filename: file.name,
|
|
74
|
+
contentType: file.contentType,
|
|
75
|
+
knownLength: stats.size,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
// Use buffer for smaller files
|
|
79
|
+
const fileBuffer = await fs.promises.readFile(file.path);
|
|
80
|
+
const blob = new Blob([fileBuffer], { type: file.contentType });
|
|
81
|
+
formData.append('files', blob, file.name);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(`Failed to read file ${file.path}: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
36
87
|
|
|
37
88
|
// Add configuration parameters
|
|
38
89
|
if (appConfig.supabase.bucket) {
|
|
@@ -61,6 +112,7 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
61
112
|
formData.append('clientVersion', appConfig.packageVersion);
|
|
62
113
|
|
|
63
114
|
try {
|
|
115
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
64
116
|
const response = await fetch(
|
|
65
117
|
`${this.baseUrl}/api/storage/batch-upload-and-process`,
|
|
66
118
|
{
|
|
@@ -69,6 +121,7 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
69
121
|
'x-api-key': this.token,
|
|
70
122
|
},
|
|
71
123
|
body: formData,
|
|
124
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
72
125
|
},
|
|
73
126
|
);
|
|
74
127
|
|
|
@@ -99,10 +152,12 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
99
152
|
}
|
|
100
153
|
|
|
101
154
|
try {
|
|
155
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
102
156
|
const response = await fetch(`${this.baseUrl}/api/health`, {
|
|
103
157
|
headers: {
|
|
104
158
|
'x-api-key': this.token,
|
|
105
159
|
},
|
|
160
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
106
161
|
});
|
|
107
162
|
|
|
108
163
|
return response.ok;
|