@arela/uploader 1.1.4 → 1.2.0
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/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/index.js +55 -0
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- package/src/services/ProfileManager.js +164 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import cliProgress from 'cli-progress';
|
|
2
|
+
import FormData from 'form-data';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import logger from '../services/LoggingService.js';
|
|
8
|
+
import ScanApiService from '../services/ScanApiService.js';
|
|
9
|
+
|
|
10
|
+
import appConfig from '../config/config.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Push Command Handler
|
|
14
|
+
* Uploads files with arela_path to storage API
|
|
15
|
+
*/
|
|
16
|
+
export class PushCommand {
|
|
17
|
+
constructor() {
|
|
18
|
+
// ScanApiService will be initialized in execute() with proper API target
|
|
19
|
+
this.scanApiService = null;
|
|
20
|
+
this.onProgress = null; // Progress callback for worker mode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute the push command
|
|
25
|
+
* @param {Object} options - Command options
|
|
26
|
+
* @param {Function} options.onProgress - Optional callback for progress updates (percent, message)
|
|
27
|
+
*/
|
|
28
|
+
async execute(options) {
|
|
29
|
+
this.onProgress = options.onProgress || null;
|
|
30
|
+
try {
|
|
31
|
+
console.log('\n🚀 Starting arela push command\n');
|
|
32
|
+
|
|
33
|
+
// Validate scan configuration (same config as scan/identify/propagate)
|
|
34
|
+
const errors = this.#validateConfig();
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
console.error('⚠️ Configuration errors:');
|
|
37
|
+
errors.forEach((err) => console.error(` - ${err}`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get configuration
|
|
42
|
+
const scanConfig = appConfig.getScanConfig();
|
|
43
|
+
const pushConfig = appConfig.getPushConfig();
|
|
44
|
+
|
|
45
|
+
// Override folderStructure from command option if provided
|
|
46
|
+
if (options.folderStructure) {
|
|
47
|
+
// Clean the folder structure: remove leading/trailing slashes
|
|
48
|
+
pushConfig.folderStructure = options.folderStructure
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^\/+/, '')
|
|
51
|
+
.replace(/\/+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine API targets for scan (read) and push (upload) operations
|
|
55
|
+
// Priority: explicit scan-api/push-api > source-api/target-api > api > default
|
|
56
|
+
const scanApiTarget =
|
|
57
|
+
options.scanApi || options.sourceApi || options.api || 'default';
|
|
58
|
+
const pushApiTarget =
|
|
59
|
+
options.pushApi || options.targetApi || options.api || scanApiTarget;
|
|
60
|
+
|
|
61
|
+
// Initialize ScanApiService with the scan API target
|
|
62
|
+
this.scanApiService = new ScanApiService(scanApiTarget);
|
|
63
|
+
|
|
64
|
+
// Get upload API configuration for the push target
|
|
65
|
+
const uploadApiConfig = appConfig.getApiConfig(pushApiTarget);
|
|
66
|
+
|
|
67
|
+
// Display API configuration
|
|
68
|
+
const isCrossTenant = scanApiTarget !== pushApiTarget;
|
|
69
|
+
if (isCrossTenant) {
|
|
70
|
+
console.log('🔗 Cross-tenant mode enabled:');
|
|
71
|
+
console.log(
|
|
72
|
+
` 📖 Scan API (read): ${scanApiTarget} → ${appConfig.getApiConfig(scanApiTarget).baseUrl}`,
|
|
73
|
+
);
|
|
74
|
+
console.log(
|
|
75
|
+
` 📝 Push API (upload): ${pushApiTarget} → ${uploadApiConfig.baseUrl}`,
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(
|
|
79
|
+
`🎯 API Target: ${scanApiTarget} → ${uploadApiConfig.baseUrl}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
console.log(`📦 Fetch Batch Size: ${options.batchSize}`);
|
|
83
|
+
console.log(`📤 Upload Batch Size: ${options.uploadBatchSize}`);
|
|
84
|
+
if (pushConfig.folderStructure) {
|
|
85
|
+
console.log(
|
|
86
|
+
`📁 Folder Structure Prefix: ${pushConfig.folderStructure}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Apply filters
|
|
91
|
+
const filters = {
|
|
92
|
+
rfcs: options.rfcs || pushConfig.rfcs || [],
|
|
93
|
+
years: options.years || pushConfig.years || [],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (filters.rfcs.length > 0) {
|
|
97
|
+
console.log(`🔍 RFC Filter: ${filters.rfcs.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
if (filters.years.length > 0) {
|
|
100
|
+
console.log(`🔍 Year Filter: ${filters.years.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fetch all tables for this instance
|
|
104
|
+
console.log('\\n📊 Fetching instance tables...');
|
|
105
|
+
const tables = await this.scanApiService.getInstanceTables(
|
|
106
|
+
scanConfig.companySlug,
|
|
107
|
+
scanConfig.serverId,
|
|
108
|
+
scanConfig.basePathFull,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (tables.length === 0) {
|
|
112
|
+
console.error(
|
|
113
|
+
'\\n❌ No tables found for this instance. Run \"arela scan\" first.\\n',
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
`📋 Found ${tables.length} table${tables.length === 1 ? '' : 's'} to process:`,
|
|
120
|
+
);
|
|
121
|
+
for (const table of tables) {
|
|
122
|
+
console.log(` - ${table.tableName}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Process each table
|
|
126
|
+
let totalResults = {
|
|
127
|
+
processed: 0,
|
|
128
|
+
uploaded: 0,
|
|
129
|
+
errors: 0,
|
|
130
|
+
startTime: Date.now(),
|
|
131
|
+
};
|
|
132
|
+
|
|
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];
|
|
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
|
+
);
|
|
143
|
+
|
|
144
|
+
// Get initial statistics for this table
|
|
145
|
+
const initialStats = await this.scanApiService.getPushStats(
|
|
146
|
+
table.tableName,
|
|
147
|
+
);
|
|
148
|
+
this.#displayStats(' Table Status', initialStats);
|
|
149
|
+
|
|
150
|
+
if (initialStats.pending === 0) {
|
|
151
|
+
console.log(' ✅ No files pending upload. Skipping.\\n');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
`\\n 🚀 Uploading ${initialStats.pending} pending files...\\n`,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Process files for this table
|
|
160
|
+
const results = await this.#processFiles(
|
|
161
|
+
table.tableName,
|
|
162
|
+
filters,
|
|
163
|
+
parseInt(options.batchSize),
|
|
164
|
+
parseInt(options.uploadBatchSize),
|
|
165
|
+
uploadApiConfig,
|
|
166
|
+
options,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
totalResults.processed += results.processed;
|
|
170
|
+
totalResults.uploaded += results.uploaded;
|
|
171
|
+
totalResults.errors += results.errors;
|
|
172
|
+
|
|
173
|
+
// Display results for this table
|
|
174
|
+
console.log('\\n 📊 Table Results:');
|
|
175
|
+
console.log(` Files Processed: ${results.processed}`);
|
|
176
|
+
console.log(` Uploaded: ${results.uploaded}`);
|
|
177
|
+
console.log(` Errors: ${results.errors}\\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Display combined results
|
|
181
|
+
const duration = ((Date.now() - totalResults.startTime) / 1000).toFixed(
|
|
182
|
+
1,
|
|
183
|
+
);
|
|
184
|
+
const speed =
|
|
185
|
+
totalResults.processed > 0
|
|
186
|
+
? (totalResults.processed / parseFloat(duration)).toFixed(0)
|
|
187
|
+
: 0;
|
|
188
|
+
|
|
189
|
+
// Report completion
|
|
190
|
+
this.#reportProgress(
|
|
191
|
+
100,
|
|
192
|
+
`Push completed: ${totalResults.uploaded} files uploaded`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
console.log('\\n✅ Push Complete!\\n');
|
|
196
|
+
console.log('📊 Total Results:');
|
|
197
|
+
console.log(` Tables Processed: ${tables.length}`);
|
|
198
|
+
console.log(` Files Processed: ${totalResults.processed}`);
|
|
199
|
+
console.log(` Uploaded: ${totalResults.uploaded}`);
|
|
200
|
+
console.log(` Errors: ${totalResults.errors}`);
|
|
201
|
+
console.log(` Duration: ${duration}s`);
|
|
202
|
+
console.log(` Speed: ${speed} files/sec\\n`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('\n❌ Push failed:', error.message);
|
|
205
|
+
logger.error('Push command error:', error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate configuration
|
|
212
|
+
* @private
|
|
213
|
+
* @returns {string[]} Array of error messages
|
|
214
|
+
*/
|
|
215
|
+
#validateConfig() {
|
|
216
|
+
const errors = [];
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
appConfig.validateScanConfig();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return [err.message];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const scanConfig = appConfig.getScanConfig();
|
|
225
|
+
// When basePathFull is '*' (cross-directory wildcard), tableName is intentionally null.
|
|
226
|
+
// The push command fetches tables dynamically via getInstanceTables, so a static
|
|
227
|
+
// tableName is not required — only companySlug, serverId, and basePathFull matter.
|
|
228
|
+
if (!scanConfig.tableName && scanConfig.basePathFull !== '*') {
|
|
229
|
+
errors.push('Could not generate table name from configuration');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return errors;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Display statistics
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
#displayStats(title, stats) {
|
|
240
|
+
console.log(`\n📈 ${title}:`);
|
|
241
|
+
console.log(` Total with arela_path: ${stats.totalWithArelaPath}`);
|
|
242
|
+
console.log(` Uploaded: ${stats.uploaded}`);
|
|
243
|
+
console.log(` Pending: ${stats.pending}`);
|
|
244
|
+
if (stats.errors > 0) {
|
|
245
|
+
console.log(` Errors: ${stats.errors}`);
|
|
246
|
+
}
|
|
247
|
+
if (stats.maxAttemptsReached > 0) {
|
|
248
|
+
console.log(` Max Attempts Reached: ${stats.maxAttemptsReached}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Show top RFCs if available
|
|
252
|
+
if (stats.byRfc && stats.byRfc.length > 0) {
|
|
253
|
+
console.log('\n 📊 Top RFCs:');
|
|
254
|
+
stats.byRfc.slice(0, 5).forEach((rfc) => {
|
|
255
|
+
const percent = ((rfc.uploaded / rfc.total) * 100).toFixed(1);
|
|
256
|
+
console.log(
|
|
257
|
+
` ${rfc.rfc}: ${rfc.uploaded}/${rfc.total} (${percent}%)`,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Process files in batches
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
async #processFiles(
|
|
268
|
+
tableName,
|
|
269
|
+
filters,
|
|
270
|
+
batchSize,
|
|
271
|
+
uploadBatchSize,
|
|
272
|
+
uploadApiConfig,
|
|
273
|
+
options = {},
|
|
274
|
+
) {
|
|
275
|
+
const results = {
|
|
276
|
+
processed: 0,
|
|
277
|
+
uploaded: 0,
|
|
278
|
+
errors: 0,
|
|
279
|
+
startTime: Date.now(),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Get total count first for accurate progress (filtered by RFC/year)
|
|
283
|
+
const initialStats = await this.scanApiService.getPushStats(
|
|
284
|
+
tableName,
|
|
285
|
+
filters,
|
|
286
|
+
);
|
|
287
|
+
const totalToProcess = initialStats.pending;
|
|
288
|
+
|
|
289
|
+
let hasMore = true;
|
|
290
|
+
|
|
291
|
+
// Create progress bar with known total
|
|
292
|
+
const progressBar = new cliProgress.SingleBar({
|
|
293
|
+
format:
|
|
294
|
+
'📤 |{bar}| {percentage}% | {value}/{total} | {speed}/s | ✓{uploaded} ✗{errors}',
|
|
295
|
+
barCompleteChar: '█',
|
|
296
|
+
barIncompleteChar: '░',
|
|
297
|
+
hideCursor: true,
|
|
298
|
+
clearOnComplete: false,
|
|
299
|
+
stopOnComplete: true,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Always use offset=0 because uploaded files are removed from pending query results
|
|
303
|
+
// After each batch upload, those files are no longer "pending", so the next query
|
|
304
|
+
// at offset=0 will naturally return the next batch of unprocessed files
|
|
305
|
+
|
|
306
|
+
// Track seen file IDs to detect infinite loops (scan table update failures)
|
|
307
|
+
const seenFileIds = new Set();
|
|
308
|
+
let consecutiveRepeats = 0;
|
|
309
|
+
const MAX_CONSECUTIVE_REPEATS = 3;
|
|
310
|
+
|
|
311
|
+
// Start progress bar with known total
|
|
312
|
+
progressBar.start(totalToProcess, 0, {
|
|
313
|
+
speed: 0,
|
|
314
|
+
uploaded: 0,
|
|
315
|
+
errors: 0,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
while (hasMore) {
|
|
319
|
+
// Fetch batch of files (always offset=0 since uploaded files are removed from pending)
|
|
320
|
+
const files = await this.scanApiService.fetchFilesForPush(tableName, {
|
|
321
|
+
...filters,
|
|
322
|
+
offset: 0,
|
|
323
|
+
limit: batchSize,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (files.length === 0) {
|
|
327
|
+
hasMore = false;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Infinite loop protection: if the same files keep coming back,
|
|
332
|
+
// the scan table update is failing and they stay "pending" forever.
|
|
333
|
+
const allSeen = files.every((f) => seenFileIds.has(f.id));
|
|
334
|
+
if (allSeen) {
|
|
335
|
+
consecutiveRepeats++;
|
|
336
|
+
if (consecutiveRepeats >= MAX_CONSECUTIVE_REPEATS) {
|
|
337
|
+
const msg = `Aborting: same ${files.length} files returned ${MAX_CONSECUTIVE_REPEATS} times — scan table updates are likely failing.`;
|
|
338
|
+
logger.error(msg);
|
|
339
|
+
console.error(`\n⚠ ${msg}`);
|
|
340
|
+
hasMore = false;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
consecutiveRepeats = 0;
|
|
345
|
+
}
|
|
346
|
+
files.forEach((f) => seenFileIds.add(f.id));
|
|
347
|
+
|
|
348
|
+
// Upload files in smaller batches using new CLI upload endpoint
|
|
349
|
+
for (let i = 0; i < files.length; i += uploadBatchSize) {
|
|
350
|
+
const uploadBatch = files.slice(i, i + uploadBatchSize);
|
|
351
|
+
const batchResults = await this.#uploadBatchViaCli(
|
|
352
|
+
tableName,
|
|
353
|
+
uploadBatch,
|
|
354
|
+
uploadApiConfig,
|
|
355
|
+
options,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Update counters from API response
|
|
359
|
+
batchResults.forEach((result) => {
|
|
360
|
+
results.processed++;
|
|
361
|
+
if (result.uploaded) {
|
|
362
|
+
results.uploaded++;
|
|
363
|
+
} else {
|
|
364
|
+
results.errors++;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Update scan table via scanApi (supports cross-tenant mode)
|
|
369
|
+
// This is called on scanApi, not pushApi, because scan tables live on scanApi
|
|
370
|
+
try {
|
|
371
|
+
await this.scanApiService.batchUpdateUpload(tableName, batchResults);
|
|
372
|
+
} catch (updateError) {
|
|
373
|
+
const msg = `Failed to update scan table for batch: ${updateError.message}`;
|
|
374
|
+
logger.error(msg);
|
|
375
|
+
console.error(`\n⚠ ${msg}`);
|
|
376
|
+
// Don't fail the entire process, just log the error
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Update progress bar
|
|
380
|
+
const elapsed = (Date.now() - results.startTime) / 1000;
|
|
381
|
+
const speed =
|
|
382
|
+
elapsed > 0 ? (results.processed / elapsed).toFixed(0) : 0;
|
|
383
|
+
progressBar.update(results.processed, {
|
|
384
|
+
speed,
|
|
385
|
+
uploaded: results.uploaded,
|
|
386
|
+
errors: results.errors,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check if there are more files
|
|
391
|
+
// If we got fewer files than requested, we've processed all pending files
|
|
392
|
+
if (files.length < batchSize) {
|
|
393
|
+
hasMore = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
progressBar.stop();
|
|
398
|
+
|
|
399
|
+
return results;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Upload a batch of files using the new CLI upload endpoint
|
|
404
|
+
* Note: Scan table is updated separately via scanApi after this batch completes
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
async #uploadBatchViaCli(tableName, files, uploadApiConfig, options = {}) {
|
|
408
|
+
const pushConfig = appConfig.getPushConfig();
|
|
409
|
+
const results = [];
|
|
410
|
+
|
|
411
|
+
// Process files one by one (simpler for now, can optimize to true batch later)
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
const result = await this.#uploadFileViaCli(
|
|
414
|
+
tableName,
|
|
415
|
+
file,
|
|
416
|
+
uploadApiConfig,
|
|
417
|
+
pushConfig,
|
|
418
|
+
options,
|
|
419
|
+
);
|
|
420
|
+
results.push(result);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Upload a single file using the CLI upload endpoint
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
async #uploadFileViaCli(
|
|
431
|
+
tableName,
|
|
432
|
+
file,
|
|
433
|
+
uploadApiConfig,
|
|
434
|
+
pushConfig,
|
|
435
|
+
options = {},
|
|
436
|
+
) {
|
|
437
|
+
const result = {
|
|
438
|
+
id: file.id,
|
|
439
|
+
uploaded: false,
|
|
440
|
+
uploadError: null,
|
|
441
|
+
uploadPath: null,
|
|
442
|
+
uploadedToStorageId: null,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
// Check if file exists
|
|
447
|
+
if (!fs.existsSync(file.absolute_path)) {
|
|
448
|
+
result.uploadError =
|
|
449
|
+
'FILE_NOT_FOUND: File does not exist on filesystem';
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Get file stats
|
|
454
|
+
const stats = fs.statSync(file.absolute_path);
|
|
455
|
+
if (!stats.isFile()) {
|
|
456
|
+
result.uploadError = 'NOT_A_FILE: Path is not a regular file';
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Construct upload path using arela_path
|
|
461
|
+
// arela_path format: RFC/Year/Patente/Aduana/Pedimento/
|
|
462
|
+
const uploadPath = `${file.arela_path}${file.file_name}`;
|
|
463
|
+
result.uploadPath = uploadPath;
|
|
464
|
+
|
|
465
|
+
// Create form data for CLI upload endpoint
|
|
466
|
+
const form = new FormData();
|
|
467
|
+
|
|
468
|
+
// Build folder structure: optionally prefix with PUSH_FOLDER_STRUCTURE env var
|
|
469
|
+
// arela_path already contains the logical path (RFC/Year/Patente/Aduana/Pedimento)
|
|
470
|
+
// folderStructure from config is the bucket prefix (e.g., "documents" or "uploads/2024")
|
|
471
|
+
let arelaPath = file.arela_path.endsWith('/')
|
|
472
|
+
? file.arela_path.slice(0, -1)
|
|
473
|
+
: file.arela_path;
|
|
474
|
+
|
|
475
|
+
// Prepend folder structure prefix if configured
|
|
476
|
+
const folderStructure = pushConfig.folderStructure
|
|
477
|
+
? `${pushConfig.folderStructure}/${arelaPath}`
|
|
478
|
+
: arelaPath;
|
|
479
|
+
|
|
480
|
+
// Create a read stream with the original filename (no encoding needed)
|
|
481
|
+
const fileStream = fs.createReadStream(file.absolute_path);
|
|
482
|
+
form.append('files', fileStream, {
|
|
483
|
+
filename: file.file_name,
|
|
484
|
+
contentType: this.#getMimeType(file.file_extension),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Add required fields for CLI upload
|
|
488
|
+
form.append('tableName', tableName);
|
|
489
|
+
form.append('fileId', file.id);
|
|
490
|
+
form.append('folderStructure', folderStructure);
|
|
491
|
+
form.append('rfc', file.rfc);
|
|
492
|
+
form.append('bucket', pushConfig.bucket);
|
|
493
|
+
form.append('autoDetect', 'true');
|
|
494
|
+
form.append(
|
|
495
|
+
'autoOrganize',
|
|
496
|
+
options.autoOrganize !== false ? 'true' : 'false',
|
|
497
|
+
);
|
|
498
|
+
form.append('batchSize', '1');
|
|
499
|
+
form.append('clientVersion', appConfig.packageVersion);
|
|
500
|
+
|
|
501
|
+
// Upload file using new CLI upload endpoint
|
|
502
|
+
const response = await fetch(
|
|
503
|
+
`${uploadApiConfig.baseUrl}/api/storage/cli-upload`,
|
|
504
|
+
{
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: {
|
|
507
|
+
'x-api-key': uploadApiConfig.token,
|
|
508
|
+
...form.getHeaders(),
|
|
509
|
+
},
|
|
510
|
+
body: form,
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (!response.ok) {
|
|
515
|
+
const errorText = await response.text();
|
|
516
|
+
result.uploadError = `HTTP ${response.status}: ${errorText}`;
|
|
517
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const apiResult = await response.json();
|
|
522
|
+
|
|
523
|
+
// Check response from CLI upload endpoint
|
|
524
|
+
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
525
|
+
const uploadedFile = apiResult.uploaded[0];
|
|
526
|
+
result.uploaded = true;
|
|
527
|
+
// Only assign storageId if it is a valid UUID; ignore placeholder values
|
|
528
|
+
const UUID_RE =
|
|
529
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
530
|
+
result.uploadedToStorageId =
|
|
531
|
+
uploadedFile.storageId && UUID_RE.test(uploadedFile.storageId)
|
|
532
|
+
? uploadedFile.storageId
|
|
533
|
+
: null;
|
|
534
|
+
logger.info(`✓ Uploaded: ${file.file_name} → ${uploadPath}`);
|
|
535
|
+
} else if (apiResult.errors && apiResult.errors.length > 0) {
|
|
536
|
+
const error = apiResult.errors[0];
|
|
537
|
+
result.uploadError = `UPLOAD_FAILED: ${error.error || 'Upload failed'}`;
|
|
538
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
539
|
+
} else {
|
|
540
|
+
result.uploadError = 'Unknown upload error - no files uploaded';
|
|
541
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
result.uploadError = `UPLOAD_ERROR: ${error.message}`;
|
|
545
|
+
logger.error(`✗ Error uploading ${file.file_name}:`, error.message);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Get MIME type from file extension
|
|
553
|
+
* @private
|
|
554
|
+
*/
|
|
555
|
+
#getMimeType(extension) {
|
|
556
|
+
const mimeTypes = {
|
|
557
|
+
pdf: 'application/pdf',
|
|
558
|
+
doc: 'application/msword',
|
|
559
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
560
|
+
xls: 'application/vnd.ms-excel',
|
|
561
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
562
|
+
txt: 'text/plain',
|
|
563
|
+
jpg: 'image/jpeg',
|
|
564
|
+
jpeg: 'image/jpeg',
|
|
565
|
+
png: 'image/png',
|
|
566
|
+
gif: 'image/gif',
|
|
567
|
+
xml: 'application/xml',
|
|
568
|
+
};
|
|
569
|
+
return mimeTypes[extension?.toLowerCase()] || 'application/octet-stream';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Upload a batch of files (legacy - kept for compatibility)
|
|
574
|
+
* @private
|
|
575
|
+
* @deprecated Use #uploadBatchViaCli instead
|
|
576
|
+
*/
|
|
577
|
+
async #uploadBatch(files, uploadApiConfig) {
|
|
578
|
+
const uploadPromises = files.map((file) =>
|
|
579
|
+
this.#uploadFile(file, uploadApiConfig),
|
|
580
|
+
);
|
|
581
|
+
return Promise.all(uploadPromises);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Upload a single file
|
|
586
|
+
* @private
|
|
587
|
+
*/
|
|
588
|
+
async #uploadFile(file, uploadApiConfig) {
|
|
589
|
+
const result = {
|
|
590
|
+
id: file.id,
|
|
591
|
+
uploaded: false,
|
|
592
|
+
uploadError: null,
|
|
593
|
+
uploadPath: null,
|
|
594
|
+
uploadedToStorageId: null,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
// Check if file exists
|
|
599
|
+
if (!fs.existsSync(file.absolute_path)) {
|
|
600
|
+
result.uploadError =
|
|
601
|
+
'FILE_NOT_FOUND: File does not exist on filesystem';
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Get file stats
|
|
606
|
+
const stats = fs.statSync(file.absolute_path);
|
|
607
|
+
if (!stats.isFile()) {
|
|
608
|
+
result.uploadError = 'NOT_A_FILE: Path is not a regular file';
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Construct upload path using arela_path
|
|
613
|
+
// arela_path format: RFC/Year/Patente/Aduana/Pedimento/
|
|
614
|
+
const uploadPath = `${file.arela_path}${file.file_name}`;
|
|
615
|
+
result.uploadPath = uploadPath;
|
|
616
|
+
|
|
617
|
+
// Upload file using storage API
|
|
618
|
+
const response = await this.#uploadToStorageApi(
|
|
619
|
+
file.absolute_path,
|
|
620
|
+
uploadPath,
|
|
621
|
+
uploadApiConfig,
|
|
622
|
+
{
|
|
623
|
+
rfc: file.rfc,
|
|
624
|
+
year: file.detected_pedimento_year,
|
|
625
|
+
originalPath: file.relative_path,
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if (response.success) {
|
|
630
|
+
result.uploaded = true;
|
|
631
|
+
result.uploadedToStorageId = response.fileId;
|
|
632
|
+
logger.info(`✓ Uploaded: ${file.file_name} → ${uploadPath}`);
|
|
633
|
+
} else {
|
|
634
|
+
result.uploadError = `UPLOAD_FAILED: ${response.error || 'Unknown error'}`;
|
|
635
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
636
|
+
}
|
|
637
|
+
} catch (error) {
|
|
638
|
+
result.uploadError = `UPLOAD_ERROR: ${error.message}`;
|
|
639
|
+
logger.error(`✗ Error uploading ${file.file_name}:`, error.message);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Upload file to storage API
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
async #uploadToStorageApi(filePath, uploadPath, apiConfig, metadata = {}) {
|
|
650
|
+
try {
|
|
651
|
+
// Create form data
|
|
652
|
+
const form = new FormData();
|
|
653
|
+
form.append('files', fs.createReadStream(filePath));
|
|
654
|
+
|
|
655
|
+
// uploadPath format: RFC/Year/Patente/Aduana/Pedimento/filename
|
|
656
|
+
// Extract folder structure (arela_path without filename)
|
|
657
|
+
const folderStructure = path.dirname(uploadPath);
|
|
658
|
+
|
|
659
|
+
// Use batch-upload-and-process endpoint (same as legacy upload)
|
|
660
|
+
form.append('bucket', appConfig.getPushConfig().bucket);
|
|
661
|
+
form.append('folderStructure', folderStructure);
|
|
662
|
+
|
|
663
|
+
// Add RFC for multi-database routing
|
|
664
|
+
if (metadata.rfc) {
|
|
665
|
+
form.append('rfc', metadata.rfc);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Enable auto-detection
|
|
669
|
+
form.append('autoDetect', 'true');
|
|
670
|
+
form.append('autoOrganize', 'false');
|
|
671
|
+
form.append('batchSize', '1');
|
|
672
|
+
form.append('clientVersion', appConfig.packageVersion);
|
|
673
|
+
|
|
674
|
+
// Upload file
|
|
675
|
+
const response = await fetch(
|
|
676
|
+
`${apiConfig.baseUrl}/api/storage/batch-upload-and-process`,
|
|
677
|
+
{
|
|
678
|
+
method: 'POST',
|
|
679
|
+
headers: {
|
|
680
|
+
'x-api-key': apiConfig.token,
|
|
681
|
+
...form.getHeaders(),
|
|
682
|
+
},
|
|
683
|
+
body: form,
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
if (!response.ok) {
|
|
688
|
+
const errorText = await response.text();
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const result = await response.json();
|
|
696
|
+
|
|
697
|
+
// batch-upload-and-process returns { uploaded: [], detected: [], errors: [] }
|
|
698
|
+
// Check if upload was successful
|
|
699
|
+
if (result.uploaded && result.uploaded.length > 0) {
|
|
700
|
+
const uploadedFile = result.uploaded[0];
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
fileId: uploadedFile.fileId,
|
|
704
|
+
path: uploadedFile.path,
|
|
705
|
+
};
|
|
706
|
+
} else if (result.errors && result.errors.length > 0) {
|
|
707
|
+
const error = result.errors[0];
|
|
708
|
+
return {
|
|
709
|
+
success: false,
|
|
710
|
+
error: error.error || 'Upload failed',
|
|
711
|
+
};
|
|
712
|
+
} else {
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
error: 'Unknown upload error - no files uploaded',
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
} catch (error) {
|
|
719
|
+
return {
|
|
720
|
+
success: false,
|
|
721
|
+
error: error.message,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Report progress to callback if available (for worker mode)
|
|
728
|
+
* @private
|
|
729
|
+
* @param {number} percent - Progress percentage (0-100)
|
|
730
|
+
* @param {string} message - Progress message
|
|
731
|
+
*/
|
|
732
|
+
#reportProgress(percent, message) {
|
|
733
|
+
if (this.onProgress && typeof this.onProgress === 'function') {
|
|
734
|
+
try {
|
|
735
|
+
this.onProgress(percent, message);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
logger.debug(`Progress callback error: ${error.message}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export default PushCommand;
|