@arela/uploader 0.2.7 → 0.2.9
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 +623 -156
- package/src/services/upload/ApiUploadService.js +70 -7
- package/src/services/upload/SupabaseUploadService.js +2 -2
|
@@ -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
|
|
@@ -601,32 +615,66 @@ export class DatabaseService {
|
|
|
601
615
|
: existingPath + '/';
|
|
602
616
|
|
|
603
617
|
// Find all files with the same base path that don't have arela_path yet
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
618
|
+
// Use pagination to handle cases with more than 1000 related files
|
|
619
|
+
let allRelatedFiles = [];
|
|
620
|
+
let from = 0;
|
|
621
|
+
const pageSize = 1000;
|
|
622
|
+
let hasMoreData = true;
|
|
623
|
+
let paginationError = null;
|
|
610
624
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
625
|
+
logger.info(`Searching for related files in base path: ${basePath}`);
|
|
626
|
+
|
|
627
|
+
while (hasMoreData) {
|
|
628
|
+
const { data: relatedFilesPage, error: relatedError } = await supabase
|
|
629
|
+
.from('uploader')
|
|
630
|
+
.select('id, filename, original_path')
|
|
631
|
+
.like('original_path', `${basePath}%`)
|
|
632
|
+
.is('arela_path', null)
|
|
633
|
+
.neq('id', pedimento.id) // Exclude the pedimento itself
|
|
634
|
+
.range(from, from + pageSize - 1);
|
|
635
|
+
|
|
636
|
+
if (relatedError) {
|
|
637
|
+
logger.error(
|
|
638
|
+
`Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
|
|
639
|
+
);
|
|
640
|
+
paginationError = relatedError;
|
|
641
|
+
totalErrors++;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (relatedFilesPage && relatedFilesPage.length > 0) {
|
|
646
|
+
allRelatedFiles = allRelatedFiles.concat(relatedFilesPage);
|
|
647
|
+
|
|
648
|
+
// Check if we got a full page, indicating there might be more data
|
|
649
|
+
if (relatedFilesPage.length < pageSize) {
|
|
650
|
+
hasMoreData = false;
|
|
651
|
+
} else {
|
|
652
|
+
from += pageSize;
|
|
653
|
+
logger.info(
|
|
654
|
+
`Fetched ${relatedFilesPage.length} files, continuing pagination (total so far: ${allRelatedFiles.length})`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
hasMoreData = false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Continue with error handling if pagination failed
|
|
663
|
+
if (paginationError) {
|
|
616
664
|
continue;
|
|
617
665
|
}
|
|
618
666
|
|
|
619
|
-
if (!
|
|
667
|
+
if (!allRelatedFiles || allRelatedFiles.length === 0) {
|
|
620
668
|
logger.info(`No related files found for ${pedimento.filename}`);
|
|
621
669
|
continue;
|
|
622
670
|
}
|
|
623
671
|
|
|
624
672
|
logger.info(
|
|
625
|
-
`Found ${
|
|
673
|
+
`Found ${allRelatedFiles.length} related files to update for ${pedimento.filename}`,
|
|
626
674
|
);
|
|
627
675
|
|
|
628
676
|
// Process files in batches
|
|
629
|
-
const fileIds =
|
|
677
|
+
const fileIds = allRelatedFiles.map((f) => f.id);
|
|
630
678
|
|
|
631
679
|
for (let i = 0; i < fileIds.length; i += BATCH_SIZE) {
|
|
632
680
|
const batchIds = fileIds.slice(i, i + BATCH_SIZE);
|
|
@@ -640,9 +688,9 @@ export class DatabaseService {
|
|
|
640
688
|
try {
|
|
641
689
|
const { error: updateError } = await supabase
|
|
642
690
|
.from('uploader')
|
|
643
|
-
.update({
|
|
691
|
+
.update({
|
|
644
692
|
arela_path: folderArelaPath,
|
|
645
|
-
year: pedimento.year
|
|
693
|
+
year: pedimento.year,
|
|
646
694
|
})
|
|
647
695
|
.in('id', batchIds);
|
|
648
696
|
|
|
@@ -885,133 +933,54 @@ export class DatabaseService {
|
|
|
885
933
|
console.log(`📋 Total files to upload: ${allRelatedFiles.length}`);
|
|
886
934
|
logger.info(`Total files to upload: ${allRelatedFiles.length}`);
|
|
887
935
|
|
|
888
|
-
// Step 4: Upload all related files
|
|
936
|
+
// Step 4: Upload all related files using concurrent batch processing
|
|
889
937
|
let totalProcessed = 0;
|
|
890
938
|
let totalUploaded = 0;
|
|
891
939
|
let totalErrors = 0;
|
|
892
940
|
const batchSize = parseInt(options.batchSize) || 10;
|
|
893
941
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
for (const file of batch) {
|
|
898
|
-
try {
|
|
899
|
-
totalProcessed++;
|
|
942
|
+
// Import performance configuration
|
|
943
|
+
const { performance: perfConfig } = appConfig;
|
|
944
|
+
const maxConcurrency = perfConfig?.maxApiConnections || 3;
|
|
900
945
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
}
|
|
946
|
+
console.log(
|
|
947
|
+
`🚀 Starting batch upload: ${allRelatedFiles.length} files in batches of ${batchSize}`,
|
|
948
|
+
);
|
|
949
|
+
console.log(
|
|
950
|
+
`⚡ Concurrent processing: up to ${maxConcurrency} parallel operations`,
|
|
951
|
+
);
|
|
959
952
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
contentType: 'application/octet-stream',
|
|
966
|
-
},
|
|
967
|
-
],
|
|
968
|
-
{
|
|
969
|
-
folderStructure: fullFolderStructure,
|
|
970
|
-
},
|
|
971
|
-
);
|
|
972
|
-
}
|
|
953
|
+
// Process files in batches with concurrent processing
|
|
954
|
+
for (let i = 0; i < allRelatedFiles.length; i += batchSize) {
|
|
955
|
+
const batch = allRelatedFiles.slice(i, i + batchSize);
|
|
956
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
957
|
+
const totalBatches = Math.ceil(allRelatedFiles.length / batchSize);
|
|
973
958
|
|
|
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);
|
|
959
|
+
console.log(
|
|
960
|
+
`📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} files)`,
|
|
961
|
+
);
|
|
983
962
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
})
|
|
993
|
-
.eq('id', file.id);
|
|
963
|
+
// Process batch using concurrent processing similar to UploadCommand
|
|
964
|
+
const batchResults = await this.#processRfcBatch(
|
|
965
|
+
batch,
|
|
966
|
+
uploadService,
|
|
967
|
+
supabase,
|
|
968
|
+
options,
|
|
969
|
+
maxConcurrency,
|
|
970
|
+
);
|
|
994
971
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
totalErrors++;
|
|
1002
|
-
logger.error(
|
|
1003
|
-
`Error processing file ${file.filename}: ${error.message}`,
|
|
1004
|
-
);
|
|
972
|
+
totalProcessed += batchResults.processed;
|
|
973
|
+
totalUploaded += batchResults.uploaded;
|
|
974
|
+
totalErrors += batchResults.errors;
|
|
1005
975
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
976
|
+
// Progress update
|
|
977
|
+
const progress = (
|
|
978
|
+
((i + batch.length) / allRelatedFiles.length) *
|
|
979
|
+
100
|
|
980
|
+
).toFixed(1);
|
|
981
|
+
console.log(
|
|
982
|
+
`📊 Batch ${batchNum} complete - Progress: ${progress}% (${totalUploaded}/${allRelatedFiles.length} uploaded)`,
|
|
983
|
+
);
|
|
1015
984
|
}
|
|
1016
985
|
|
|
1017
986
|
const result = {
|
|
@@ -1106,22 +1075,44 @@ export class DatabaseService {
|
|
|
1106
1075
|
for (let i = 0; i < uniqueArelaPaths.length; i += chunkSize) {
|
|
1107
1076
|
const pathChunk = uniqueArelaPaths.slice(i, i + chunkSize);
|
|
1108
1077
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
.in('arela_path', pathChunk)
|
|
1115
|
-
.neq('status', 'file-uploaded')
|
|
1116
|
-
.not('original_path', 'is', null);
|
|
1078
|
+
// Query with pagination to get all results (Supabase default limit is 1000)
|
|
1079
|
+
let chunkFiles = [];
|
|
1080
|
+
let from = 0;
|
|
1081
|
+
const pageSize = 1000;
|
|
1082
|
+
let hasMoreData = true;
|
|
1117
1083
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1084
|
+
while (hasMoreData) {
|
|
1085
|
+
const { data: pageData, error: chunkError } = await supabase
|
|
1086
|
+
.from('uploader')
|
|
1087
|
+
.select(
|
|
1088
|
+
'id, original_path, arela_path, filename, rfc, document_type, status',
|
|
1089
|
+
)
|
|
1090
|
+
.in('arela_path', pathChunk)
|
|
1091
|
+
.neq('status', 'file-uploaded')
|
|
1092
|
+
.not('original_path', 'is', null)
|
|
1093
|
+
.range(from, from + pageSize - 1);
|
|
1094
|
+
|
|
1095
|
+
if (chunkError) {
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`Error querying files for arela_paths chunk: ${chunkError.message}`,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (pageData && pageData.length > 0) {
|
|
1102
|
+
chunkFiles = chunkFiles.concat(pageData);
|
|
1103
|
+
|
|
1104
|
+
// Check if we got a full page, indicating there might be more data
|
|
1105
|
+
if (pageData.length < pageSize) {
|
|
1106
|
+
hasMoreData = false;
|
|
1107
|
+
} else {
|
|
1108
|
+
from += pageSize;
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
hasMoreData = false;
|
|
1112
|
+
}
|
|
1122
1113
|
}
|
|
1123
1114
|
|
|
1124
|
-
if (chunkFiles
|
|
1115
|
+
if (chunkFiles.length > 0) {
|
|
1125
1116
|
allReadyFiles = allReadyFiles.concat(chunkFiles);
|
|
1126
1117
|
}
|
|
1127
1118
|
}
|
|
@@ -1158,6 +1149,482 @@ export class DatabaseService {
|
|
|
1158
1149
|
|
|
1159
1150
|
return readyFiles || [];
|
|
1160
1151
|
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Process a batch of files using concurrent processing for RFC uploads
|
|
1155
|
+
* @param {Array} files - Files to process in this batch
|
|
1156
|
+
* @param {Object} uploadService - Upload service instance
|
|
1157
|
+
* @param {Object} supabase - Supabase client
|
|
1158
|
+
* @param {Object} options - Upload options
|
|
1159
|
+
* @param {number} maxConcurrency - Maximum concurrent operations
|
|
1160
|
+
* @returns {Promise<Object>} Batch processing results
|
|
1161
|
+
*/
|
|
1162
|
+
async #processRfcBatch(
|
|
1163
|
+
files,
|
|
1164
|
+
uploadService,
|
|
1165
|
+
supabase,
|
|
1166
|
+
options,
|
|
1167
|
+
maxConcurrency,
|
|
1168
|
+
) {
|
|
1169
|
+
const fs = (await import('fs')).default;
|
|
1170
|
+
|
|
1171
|
+
let processed = 0;
|
|
1172
|
+
let uploaded = 0;
|
|
1173
|
+
let errors = 0;
|
|
1174
|
+
|
|
1175
|
+
// For Supabase, process files individually (required by service)
|
|
1176
|
+
if (uploadService.getServiceName() === 'Supabase') {
|
|
1177
|
+
// Process files in concurrent chunks within the batch
|
|
1178
|
+
const chunks = [];
|
|
1179
|
+
for (let i = 0; i < files.length; i += maxConcurrency) {
|
|
1180
|
+
chunks.push(files.slice(i, i + maxConcurrency));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Process each chunk concurrently
|
|
1184
|
+
for (const chunk of chunks) {
|
|
1185
|
+
const chunkPromises = chunk.map(async (file) => {
|
|
1186
|
+
return await this.#processRfcSingleFile(
|
|
1187
|
+
file,
|
|
1188
|
+
uploadService,
|
|
1189
|
+
supabase,
|
|
1190
|
+
options,
|
|
1191
|
+
fs,
|
|
1192
|
+
);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// Wait for all files in this chunk to complete
|
|
1196
|
+
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
1197
|
+
|
|
1198
|
+
// Count results
|
|
1199
|
+
for (const result of chunkResults) {
|
|
1200
|
+
processed++;
|
|
1201
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
1202
|
+
uploaded++;
|
|
1203
|
+
} else {
|
|
1204
|
+
errors++;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
// For API service, use true batch processing (multiple files per API call)
|
|
1210
|
+
const apiChunks = [];
|
|
1211
|
+
const apiChunkSize = Math.min(
|
|
1212
|
+
5,
|
|
1213
|
+
Math.ceil(files.length / maxConcurrency),
|
|
1214
|
+
); // 5 files per API call, or distribute evenly
|
|
1215
|
+
|
|
1216
|
+
for (let i = 0; i < files.length; i += apiChunkSize) {
|
|
1217
|
+
apiChunks.push(files.slice(i, i + apiChunkSize));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
console.log(
|
|
1221
|
+
` 🚀 Processing ${apiChunks.length} API calls with ${apiChunkSize} files each (max ${maxConcurrency} concurrent)`,
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
// Process API chunks with controlled concurrency
|
|
1225
|
+
const concurrentChunks = [];
|
|
1226
|
+
for (let i = 0; i < apiChunks.length; i += maxConcurrency) {
|
|
1227
|
+
concurrentChunks.push(apiChunks.slice(i, i + maxConcurrency));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
for (const concurrentSet of concurrentChunks) {
|
|
1231
|
+
const batchPromises = concurrentSet.map(async (chunk) => {
|
|
1232
|
+
return await this.#processRfcApiBatch(
|
|
1233
|
+
chunk,
|
|
1234
|
+
uploadService,
|
|
1235
|
+
supabase,
|
|
1236
|
+
options,
|
|
1237
|
+
fs,
|
|
1238
|
+
);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Wait for all concurrent batches to complete
|
|
1242
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
1243
|
+
|
|
1244
|
+
// Count results
|
|
1245
|
+
for (const result of batchResults) {
|
|
1246
|
+
if (result.status === 'fulfilled') {
|
|
1247
|
+
processed += result.value.processed;
|
|
1248
|
+
uploaded += result.value.uploaded;
|
|
1249
|
+
errors += result.value.errors;
|
|
1250
|
+
} else {
|
|
1251
|
+
errors += result.value?.processed || 0;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return { processed, uploaded, errors };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Process a single file for RFC upload (Supabase mode)
|
|
1262
|
+
*/
|
|
1263
|
+
async #processRfcSingleFile(file, uploadService, supabase, options, fs) {
|
|
1264
|
+
try {
|
|
1265
|
+
// Check if file exists
|
|
1266
|
+
if (!fs.existsSync(file.original_path)) {
|
|
1267
|
+
logger.warn(
|
|
1268
|
+
`File not found: ${file.filename} at ${file.original_path}`,
|
|
1269
|
+
);
|
|
1270
|
+
await supabase
|
|
1271
|
+
.from('uploader')
|
|
1272
|
+
.update({
|
|
1273
|
+
status: 'file-not-found',
|
|
1274
|
+
message: 'File no longer exists at original path',
|
|
1275
|
+
})
|
|
1276
|
+
.eq('id', file.id);
|
|
1277
|
+
return { success: false, error: 'File not found' };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Supabase requires single file upload with uploadPath
|
|
1281
|
+
let uploadPath;
|
|
1282
|
+
if (options.folderStructure && file.arela_path) {
|
|
1283
|
+
uploadPath = `uploads/${options.folderStructure}/${file.arela_path}${file.filename}`;
|
|
1284
|
+
} else if (file.arela_path) {
|
|
1285
|
+
uploadPath = `uploads/${file.arela_path}${file.filename}`;
|
|
1286
|
+
} else {
|
|
1287
|
+
uploadPath = `uploads/${file.rfc}/${file.filename}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const uploadResult = await uploadService.upload(
|
|
1291
|
+
[
|
|
1292
|
+
{
|
|
1293
|
+
path: file.original_path,
|
|
1294
|
+
name: file.filename,
|
|
1295
|
+
contentType: 'application/octet-stream',
|
|
1296
|
+
},
|
|
1297
|
+
],
|
|
1298
|
+
{ uploadPath: uploadPath },
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
// Check upload result before updating database status
|
|
1302
|
+
if (uploadResult.success) {
|
|
1303
|
+
await supabase
|
|
1304
|
+
.from('uploader')
|
|
1305
|
+
.update({
|
|
1306
|
+
status: 'file-uploaded',
|
|
1307
|
+
message: 'Successfully uploaded to Supabase',
|
|
1308
|
+
})
|
|
1309
|
+
.eq('id', file.id);
|
|
1310
|
+
|
|
1311
|
+
logger.info(`✅ Uploaded: ${file.filename}`);
|
|
1312
|
+
return { success: true, filename: file.filename };
|
|
1313
|
+
} else {
|
|
1314
|
+
await supabase
|
|
1315
|
+
.from('uploader')
|
|
1316
|
+
.update({
|
|
1317
|
+
status: 'upload-error',
|
|
1318
|
+
message: `Upload failed: ${uploadResult.error}`,
|
|
1319
|
+
})
|
|
1320
|
+
.eq('id', file.id);
|
|
1321
|
+
|
|
1322
|
+
logger.error(
|
|
1323
|
+
`❌ Upload failed: ${file.filename} - ${uploadResult.error}`,
|
|
1324
|
+
);
|
|
1325
|
+
return {
|
|
1326
|
+
success: false,
|
|
1327
|
+
error: uploadResult.error,
|
|
1328
|
+
filename: file.filename,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
logger.error(
|
|
1333
|
+
`❌ Error processing file ${file.filename}: ${error.message}`,
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
await supabase
|
|
1337
|
+
.from('uploader')
|
|
1338
|
+
.update({
|
|
1339
|
+
status: 'upload-error',
|
|
1340
|
+
message: `Processing error: ${error.message}`,
|
|
1341
|
+
})
|
|
1342
|
+
.eq('id', file.id);
|
|
1343
|
+
|
|
1344
|
+
return { success: false, error: error.message, filename: file.filename };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Process multiple files in a single API batch call (API service mode)
|
|
1350
|
+
*/
|
|
1351
|
+
async #processRfcApiBatch(files, uploadService, supabase, options, fs) {
|
|
1352
|
+
let processed = 0;
|
|
1353
|
+
let uploaded = 0;
|
|
1354
|
+
let errors = 0;
|
|
1355
|
+
|
|
1356
|
+
try {
|
|
1357
|
+
// Prepare files for batch upload
|
|
1358
|
+
const validFiles = [];
|
|
1359
|
+
const invalidFiles = [];
|
|
1360
|
+
|
|
1361
|
+
for (const file of files) {
|
|
1362
|
+
processed++;
|
|
1363
|
+
|
|
1364
|
+
if (!fs.existsSync(file.original_path)) {
|
|
1365
|
+
logger.warn(
|
|
1366
|
+
`File not found: ${file.filename} at ${file.original_path}`,
|
|
1367
|
+
);
|
|
1368
|
+
invalidFiles.push(file);
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
validFiles.push({
|
|
1373
|
+
fileData: {
|
|
1374
|
+
path: file.original_path,
|
|
1375
|
+
name: file.filename,
|
|
1376
|
+
contentType: 'application/octet-stream',
|
|
1377
|
+
},
|
|
1378
|
+
dbRecord: file,
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Update invalid files in database
|
|
1383
|
+
for (const file of invalidFiles) {
|
|
1384
|
+
await supabase
|
|
1385
|
+
.from('uploader')
|
|
1386
|
+
.update({
|
|
1387
|
+
status: 'file-not-found',
|
|
1388
|
+
message: 'File no longer exists at original path',
|
|
1389
|
+
})
|
|
1390
|
+
.eq('id', file.id);
|
|
1391
|
+
errors++;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Process valid files in batch if any exist
|
|
1395
|
+
if (validFiles.length > 0) {
|
|
1396
|
+
// Determine folder structure (all files in this batch should have same arela_path)
|
|
1397
|
+
const sampleFile = validFiles[0].dbRecord;
|
|
1398
|
+
let fullFolderStructure;
|
|
1399
|
+
if (options.folderStructure && sampleFile.arela_path) {
|
|
1400
|
+
fullFolderStructure = `${options.folderStructure}/${sampleFile.arela_path}`;
|
|
1401
|
+
} else if (sampleFile.arela_path) {
|
|
1402
|
+
fullFolderStructure = sampleFile.arela_path;
|
|
1403
|
+
} else {
|
|
1404
|
+
fullFolderStructure = `${sampleFile.rfc}/`;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Make single API call with multiple files
|
|
1408
|
+
const uploadResult = await uploadService.upload(
|
|
1409
|
+
validFiles.map((f) => f.fileData),
|
|
1410
|
+
{ folderStructure: fullFolderStructure },
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
if (uploadResult.success && uploadResult.data) {
|
|
1414
|
+
const apiResult = uploadResult.data;
|
|
1415
|
+
|
|
1416
|
+
logger.info(
|
|
1417
|
+
`📋 Processing API response: ${apiResult.uploaded?.length || 0} uploaded, ${apiResult.errors?.length || 0} errors`,
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
// Debug logging to understand API response structure
|
|
1421
|
+
logger.debug(
|
|
1422
|
+
`🔍 API Response structure: ${JSON.stringify(apiResult, null, 2)}`,
|
|
1423
|
+
);
|
|
1424
|
+
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
1425
|
+
logger.debug(
|
|
1426
|
+
`🔍 First uploaded file structure: ${JSON.stringify(apiResult.uploaded[0], null, 2)}`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Create filename to file mapping for quick lookup
|
|
1431
|
+
const fileNameToRecord = new Map();
|
|
1432
|
+
validFiles.forEach((f) => {
|
|
1433
|
+
fileNameToRecord.set(f.fileData.name, f.dbRecord);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Debug: Log expected filenames
|
|
1437
|
+
logger.debug(
|
|
1438
|
+
`🔍 Expected filenames: ${Array.from(fileNameToRecord.keys()).join(', ')}`,
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
// Handle successfully uploaded files
|
|
1442
|
+
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
1443
|
+
const successfulFileIds = [];
|
|
1444
|
+
const matchedFilenames = [];
|
|
1445
|
+
|
|
1446
|
+
apiResult.uploaded.forEach((uploadedFile) => {
|
|
1447
|
+
// Try multiple possible property names for filename
|
|
1448
|
+
const possibleFilename =
|
|
1449
|
+
uploadedFile.fileName ||
|
|
1450
|
+
uploadedFile.filename ||
|
|
1451
|
+
uploadedFile.name ||
|
|
1452
|
+
uploadedFile.file_name ||
|
|
1453
|
+
uploadedFile.originalName ||
|
|
1454
|
+
uploadedFile.original_name;
|
|
1455
|
+
|
|
1456
|
+
logger.debug(
|
|
1457
|
+
`🔍 Trying to match uploaded file: ${JSON.stringify(uploadedFile)}`,
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
const dbRecord = fileNameToRecord.get(possibleFilename);
|
|
1461
|
+
if (dbRecord) {
|
|
1462
|
+
successfulFileIds.push(dbRecord.id);
|
|
1463
|
+
matchedFilenames.push(possibleFilename);
|
|
1464
|
+
logger.debug(`✅ Matched file: ${possibleFilename}`);
|
|
1465
|
+
} else {
|
|
1466
|
+
logger.warn(
|
|
1467
|
+
`⚠️ Could not match uploaded file with any known filename: ${JSON.stringify(uploadedFile)}`,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
// If no individual files matched but API indicates success, use fallback
|
|
1473
|
+
if (
|
|
1474
|
+
successfulFileIds.length === 0 &&
|
|
1475
|
+
apiResult.uploaded.length > 0
|
|
1476
|
+
) {
|
|
1477
|
+
logger.warn(
|
|
1478
|
+
`🔄 Fallback: No individual file matches found, but API indicates ${apiResult.uploaded.length} uploads. Marking all ${validFiles.length} batch files as uploaded.`,
|
|
1479
|
+
);
|
|
1480
|
+
validFiles.forEach((f) => successfulFileIds.push(f.dbRecord.id));
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (successfulFileIds.length > 0) {
|
|
1484
|
+
await supabase
|
|
1485
|
+
.from('uploader')
|
|
1486
|
+
.update({
|
|
1487
|
+
status: 'file-uploaded',
|
|
1488
|
+
message: 'Successfully uploaded to Arela API (batch)',
|
|
1489
|
+
})
|
|
1490
|
+
.in('id', successfulFileIds);
|
|
1491
|
+
|
|
1492
|
+
uploaded += successfulFileIds.length;
|
|
1493
|
+
logger.info(
|
|
1494
|
+
`✅ Batch upload successful: ${successfulFileIds.length} files uploaded`,
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Handle failed files
|
|
1500
|
+
if (apiResult.errors && apiResult.errors.length > 0) {
|
|
1501
|
+
const failedFileIds = [];
|
|
1502
|
+
|
|
1503
|
+
apiResult.errors.forEach((errorInfo) => {
|
|
1504
|
+
// Try multiple possible property names for filename in errors
|
|
1505
|
+
const possibleFilename =
|
|
1506
|
+
errorInfo.fileName ||
|
|
1507
|
+
errorInfo.filename ||
|
|
1508
|
+
errorInfo.name ||
|
|
1509
|
+
errorInfo.file_name ||
|
|
1510
|
+
errorInfo.originalName ||
|
|
1511
|
+
errorInfo.original_name;
|
|
1512
|
+
|
|
1513
|
+
const dbRecord = fileNameToRecord.get(possibleFilename);
|
|
1514
|
+
if (dbRecord) {
|
|
1515
|
+
failedFileIds.push(dbRecord.id);
|
|
1516
|
+
} else {
|
|
1517
|
+
logger.warn(
|
|
1518
|
+
`⚠️ Could not match error file: ${JSON.stringify(errorInfo)}`,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
if (failedFileIds.length > 0) {
|
|
1524
|
+
await supabase
|
|
1525
|
+
.from('uploader')
|
|
1526
|
+
.update({
|
|
1527
|
+
status: 'upload-error',
|
|
1528
|
+
message: `Upload failed: ${apiResult.errors[0].error}`,
|
|
1529
|
+
})
|
|
1530
|
+
.in('id', failedFileIds);
|
|
1531
|
+
|
|
1532
|
+
errors += failedFileIds.length;
|
|
1533
|
+
logger.error(
|
|
1534
|
+
`❌ Batch upload errors: ${failedFileIds.length} files failed`,
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Handle any remaining files that weren't in uploaded or errors arrays
|
|
1540
|
+
// Use robust filename extraction for both uploaded and error files
|
|
1541
|
+
const extractFilename = (fileObj) => {
|
|
1542
|
+
return (
|
|
1543
|
+
fileObj.fileName ||
|
|
1544
|
+
fileObj.filename ||
|
|
1545
|
+
fileObj.name ||
|
|
1546
|
+
fileObj.file_name ||
|
|
1547
|
+
fileObj.originalName ||
|
|
1548
|
+
fileObj.original_name
|
|
1549
|
+
);
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
const processedFileNames = new Set([
|
|
1553
|
+
...(apiResult.uploaded || []).map(extractFilename).filter(Boolean),
|
|
1554
|
+
...(apiResult.errors || []).map(extractFilename).filter(Boolean),
|
|
1555
|
+
]);
|
|
1556
|
+
|
|
1557
|
+
const unprocessedFiles = validFiles.filter(
|
|
1558
|
+
(f) => !processedFileNames.has(f.fileData.name),
|
|
1559
|
+
);
|
|
1560
|
+
if (unprocessedFiles.length > 0) {
|
|
1561
|
+
// Only mark as unprocessed if we haven't already handled all files through fallback logic
|
|
1562
|
+
// If we used fallback (all files marked as uploaded), don't mark any as unprocessed
|
|
1563
|
+
const alreadyHandledCount = uploaded + errors;
|
|
1564
|
+
const shouldMarkUnprocessed =
|
|
1565
|
+
alreadyHandledCount < validFiles.length;
|
|
1566
|
+
|
|
1567
|
+
if (shouldMarkUnprocessed) {
|
|
1568
|
+
const unprocessedIds = unprocessedFiles.map((f) => f.dbRecord.id);
|
|
1569
|
+
await supabase
|
|
1570
|
+
.from('uploader')
|
|
1571
|
+
.update({
|
|
1572
|
+
status: 'upload-error',
|
|
1573
|
+
message: 'File not found in API response',
|
|
1574
|
+
})
|
|
1575
|
+
.in('id', unprocessedIds);
|
|
1576
|
+
|
|
1577
|
+
errors += unprocessedIds.length;
|
|
1578
|
+
logger.warn(
|
|
1579
|
+
`⚠️ Unprocessed files: ${unprocessedIds.length} files not found in API response`,
|
|
1580
|
+
);
|
|
1581
|
+
logger.debug(
|
|
1582
|
+
`🔍 API response uploaded array: ${JSON.stringify(apiResult.uploaded)}`,
|
|
1583
|
+
);
|
|
1584
|
+
logger.debug(
|
|
1585
|
+
`🔍 Expected filenames: ${validFiles.map((f) => f.fileData.name).join(', ')}`,
|
|
1586
|
+
);
|
|
1587
|
+
} else {
|
|
1588
|
+
logger.debug(
|
|
1589
|
+
`✅ All files already handled (uploaded: ${uploaded}, errors: ${errors}), skipping unprocessed marking`,
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
} else {
|
|
1594
|
+
// Complete batch failure - mark all files as failed
|
|
1595
|
+
const fileIds = validFiles.map((f) => f.dbRecord.id);
|
|
1596
|
+
await supabase
|
|
1597
|
+
.from('uploader')
|
|
1598
|
+
.update({
|
|
1599
|
+
status: 'upload-error',
|
|
1600
|
+
message: uploadResult.error || 'Batch upload failed',
|
|
1601
|
+
})
|
|
1602
|
+
.in('id', fileIds);
|
|
1603
|
+
|
|
1604
|
+
errors += validFiles.length;
|
|
1605
|
+
logger.error(
|
|
1606
|
+
`❌ Complete batch failure: ${validFiles.length} files - ${uploadResult.error}`,
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
logger.error(`❌ Error processing batch: ${error.message}`);
|
|
1612
|
+
|
|
1613
|
+
// Mark all files as failed
|
|
1614
|
+
const fileIds = files.map((f) => f.id);
|
|
1615
|
+
await supabase
|
|
1616
|
+
.from('uploader')
|
|
1617
|
+
.update({
|
|
1618
|
+
status: 'upload-error',
|
|
1619
|
+
message: `Batch processing error: ${error.message}`,
|
|
1620
|
+
})
|
|
1621
|
+
.in('id', fileIds);
|
|
1622
|
+
|
|
1623
|
+
errors += files.length;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return { processed, uploaded, errors };
|
|
1627
|
+
}
|
|
1161
1628
|
}
|
|
1162
1629
|
|
|
1163
1630
|
// Export singleton instance
|