@arela/uploader 1.1.3 ā 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/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- 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,587 @@
|
|
|
1
|
+
import cliProgress from 'cli-progress';
|
|
2
|
+
import { globby } from 'globby';
|
|
3
|
+
import mime from 'mime-types';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import databaseService from '../services/DatabaseService.js';
|
|
7
|
+
import logger from '../services/LoggingService.js';
|
|
8
|
+
import watchService from '../services/WatchService.js';
|
|
9
|
+
import uploadServiceFactory from '../services/upload/UploadServiceFactory.js';
|
|
10
|
+
|
|
11
|
+
import appConfig from '../config/config.js';
|
|
12
|
+
import ErrorHandler from '../errors/ErrorHandler.js';
|
|
13
|
+
import {
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
FileOperationError,
|
|
16
|
+
} from '../errors/ErrorTypes.js';
|
|
17
|
+
import FileOperations from '../utils/FileOperations.js';
|
|
18
|
+
import fileSanitizer from '../utils/FileSanitizer.js';
|
|
19
|
+
import pathDetector from '../utils/PathDetector.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Upload Command Handler
|
|
23
|
+
* Handles the main upload functionality
|
|
24
|
+
*/
|
|
25
|
+
export class UploadCommand {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.errorHandler = new ErrorHandler(logger);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute the upload command
|
|
32
|
+
* @param {Object} options - Command options
|
|
33
|
+
*/
|
|
34
|
+
async execute(options) {
|
|
35
|
+
try {
|
|
36
|
+
// Prevent direct uploads while in watch mode
|
|
37
|
+
if (watchService.isWatchActive()) {
|
|
38
|
+
logger.error('ā Cannot upload directly while in watch mode');
|
|
39
|
+
logger.info('š” Files in watch mode are processed automatically');
|
|
40
|
+
logger.info(
|
|
41
|
+
'š” Stop watch mode first (Ctrl+C) before using upload command',
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
reason: 'Watch mode is active - cannot upload directly',
|
|
46
|
+
source: null,
|
|
47
|
+
stats: {
|
|
48
|
+
successCount: 0,
|
|
49
|
+
detectedCount: 0,
|
|
50
|
+
organizedCount: 0,
|
|
51
|
+
failureCount: 0,
|
|
52
|
+
skippedCount: 0,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate configuration
|
|
58
|
+
this.#validateOptions(options);
|
|
59
|
+
|
|
60
|
+
// Initialize services
|
|
61
|
+
const uploadService = await uploadServiceFactory.getUploadService(
|
|
62
|
+
options.forceSupabase,
|
|
63
|
+
);
|
|
64
|
+
const sources = appConfig.getUploadSources();
|
|
65
|
+
const basePath = appConfig.getBasePath();
|
|
66
|
+
|
|
67
|
+
// Log command start
|
|
68
|
+
logger.info(`Starting upload with ${uploadService.getServiceName()}`);
|
|
69
|
+
|
|
70
|
+
if (options.clearLog) {
|
|
71
|
+
logger.clearLogFile();
|
|
72
|
+
logger.info('Log file cleared');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Process each source with configurable concurrency
|
|
76
|
+
let globalResults = {
|
|
77
|
+
successCount: 0,
|
|
78
|
+
detectedCount: 0,
|
|
79
|
+
organizedCount: 0,
|
|
80
|
+
failureCount: 0,
|
|
81
|
+
skippedCount: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Determine processing strategy based on configuration
|
|
85
|
+
const maxConcurrentSources =
|
|
86
|
+
appConfig.performance?.maxConcurrentSources || 1;
|
|
87
|
+
|
|
88
|
+
if (maxConcurrentSources > 1 && sources.length > 1) {
|
|
89
|
+
// Parallel source processing
|
|
90
|
+
logger.info(
|
|
91
|
+
`Processing ${sources.length} sources with concurrency: ${maxConcurrentSources}`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Process sources in batches to control concurrency
|
|
95
|
+
for (let i = 0; i < sources.length; i += maxConcurrentSources) {
|
|
96
|
+
const sourceBatch = sources.slice(i, i + maxConcurrentSources);
|
|
97
|
+
|
|
98
|
+
const sourcePromises = sourceBatch.map(async (source) => {
|
|
99
|
+
const sourcePath = path
|
|
100
|
+
.resolve(basePath, source)
|
|
101
|
+
.replace(/\\/g, '/');
|
|
102
|
+
logger.info(`Processing folder: ${sourcePath}`);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const files = await this.#discoverFiles(sourcePath);
|
|
106
|
+
logger.info(`Found ${files.length} files in ${source}`);
|
|
107
|
+
|
|
108
|
+
const result = await this.#processFilesInBatches(
|
|
109
|
+
files,
|
|
110
|
+
options,
|
|
111
|
+
uploadService,
|
|
112
|
+
basePath,
|
|
113
|
+
source,
|
|
114
|
+
sourcePath,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
this.#logSourceSummary(source, result, options);
|
|
118
|
+
return { success: true, source, result };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.errorHandler.handleError(error, { source, sourcePath });
|
|
121
|
+
return { success: false, source, error: error.message };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Wait for this batch of sources to complete
|
|
126
|
+
const results = await Promise.allSettled(sourcePromises);
|
|
127
|
+
|
|
128
|
+
results.forEach((result) => {
|
|
129
|
+
if (result.status === 'fulfilled') {
|
|
130
|
+
const sourceResult = result.value;
|
|
131
|
+
if (sourceResult.success) {
|
|
132
|
+
this.#updateGlobalResults(globalResults, sourceResult.result);
|
|
133
|
+
} else {
|
|
134
|
+
globalResults.failureCount++;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
globalResults.failureCount++;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Sequential source processing (original behavior)
|
|
143
|
+
for (const source of sources) {
|
|
144
|
+
const sourcePath = path.resolve(basePath, source).replace(/\\/g, '/');
|
|
145
|
+
logger.info(`Processing folder: ${sourcePath}`);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const files = await this.#discoverFiles(sourcePath);
|
|
149
|
+
logger.info(`Found ${files.length} files to process`);
|
|
150
|
+
|
|
151
|
+
const result = await this.#processFilesInBatches(
|
|
152
|
+
files,
|
|
153
|
+
options,
|
|
154
|
+
uploadService,
|
|
155
|
+
basePath,
|
|
156
|
+
source,
|
|
157
|
+
sourcePath,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
this.#updateGlobalResults(globalResults, result);
|
|
161
|
+
this.#logSourceSummary(source, result, options);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.errorHandler.handleError(error, { source, sourcePath });
|
|
164
|
+
globalResults.failureCount++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.#logFinalSummary(globalResults, options, uploadService);
|
|
170
|
+
|
|
171
|
+
// Handle additional phases if requested
|
|
172
|
+
if (options.runAllPhases && options.statsOnly) {
|
|
173
|
+
await this.#runAdditionalPhases(options);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.errorHandler.handleFatalError(error, { command: 'upload', options });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate command options
|
|
182
|
+
* @private
|
|
183
|
+
* @param {Object} options - Options to validate
|
|
184
|
+
*/
|
|
185
|
+
#validateOptions(options) {
|
|
186
|
+
try {
|
|
187
|
+
appConfig.validateConfiguration(options.forceSupabase);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new ConfigurationError(error.message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
options.batchSize &&
|
|
194
|
+
(options.batchSize < 1 || options.batchSize > 100)
|
|
195
|
+
) {
|
|
196
|
+
throw new ConfigurationError('Batch size must be between 1 and 100');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Discover files in a source path
|
|
202
|
+
* @private
|
|
203
|
+
* @param {string} sourcePath - Path to discover files in
|
|
204
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
205
|
+
*/
|
|
206
|
+
async #discoverFiles(sourcePath) {
|
|
207
|
+
try {
|
|
208
|
+
if (!FileOperations.fileExists(sourcePath)) {
|
|
209
|
+
throw new FileOperationError(
|
|
210
|
+
`Source path does not exist: ${sourcePath}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const stats = FileOperations.getFileStats(sourcePath);
|
|
215
|
+
|
|
216
|
+
if (stats?.isDirectory()) {
|
|
217
|
+
return await globby([`${sourcePath}/**/*`], { onlyFiles: true });
|
|
218
|
+
} else {
|
|
219
|
+
return [sourcePath];
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new FileOperationError(
|
|
223
|
+
`Failed to discover files in ${sourcePath}`,
|
|
224
|
+
sourcePath,
|
|
225
|
+
{ originalError: error.message },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Process files in batches
|
|
232
|
+
* @private
|
|
233
|
+
* @param {string[]} files - Files to process
|
|
234
|
+
* @param {Object} options - Processing options
|
|
235
|
+
* @param {Object} uploadService - Upload service instance
|
|
236
|
+
* @param {string} basePath - Base path
|
|
237
|
+
* @param {string} source - Source name
|
|
238
|
+
* @param {string} sourcePath - Source path
|
|
239
|
+
* @returns {Promise<Object>} Processing results
|
|
240
|
+
*/
|
|
241
|
+
async #processFilesInBatches(
|
|
242
|
+
files,
|
|
243
|
+
options,
|
|
244
|
+
uploadService,
|
|
245
|
+
basePath,
|
|
246
|
+
source,
|
|
247
|
+
sourcePath,
|
|
248
|
+
) {
|
|
249
|
+
const batchSize =
|
|
250
|
+
parseInt(options.batchSize) || appConfig.performance.batchSize || 50;
|
|
251
|
+
const results = {
|
|
252
|
+
successCount: 0,
|
|
253
|
+
detectedCount: 0,
|
|
254
|
+
organizedCount: 0,
|
|
255
|
+
failureCount: 0,
|
|
256
|
+
skippedCount: 0,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Get processed paths if available
|
|
260
|
+
const processedPaths = options.skipProcessed
|
|
261
|
+
? databaseService.getProcessedPaths()
|
|
262
|
+
: new Set();
|
|
263
|
+
|
|
264
|
+
// Create progress bar
|
|
265
|
+
const progressBar = new cliProgress.SingleBar({
|
|
266
|
+
format: `š¤ ${source} |{bar}| {percentage}% | {value}/{total} | Success: {success} | Errors: {errors}`,
|
|
267
|
+
barCompleteChar: 'ā',
|
|
268
|
+
barIncompleteChar: 'ā',
|
|
269
|
+
hideCursor: true,
|
|
270
|
+
clearOnComplete: false,
|
|
271
|
+
stopOnComplete: true,
|
|
272
|
+
stream: process.stderr, // Use stderr to separate from stdout logging
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
progressBar.start(files.length, 0, { success: 0, errors: 0 });
|
|
276
|
+
|
|
277
|
+
// Process files in batches
|
|
278
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
279
|
+
const batch = files.slice(i, i + batchSize);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const batchResult = await this.#processBatch(
|
|
283
|
+
batch,
|
|
284
|
+
options,
|
|
285
|
+
uploadService,
|
|
286
|
+
basePath,
|
|
287
|
+
processedPaths,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
this.#updateResults(results, batchResult);
|
|
291
|
+
|
|
292
|
+
progressBar.update(Math.min(i + batchSize, files.length), {
|
|
293
|
+
success: results.successCount,
|
|
294
|
+
errors: results.failureCount,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Delay between batches if configured
|
|
298
|
+
if (appConfig.performance.batchDelay > 0) {
|
|
299
|
+
await new Promise((resolve) =>
|
|
300
|
+
setTimeout(resolve, appConfig.performance.batchDelay),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
this.errorHandler.handleError(error, {
|
|
305
|
+
batch: i / batchSize + 1,
|
|
306
|
+
batchSize,
|
|
307
|
+
});
|
|
308
|
+
results.failureCount += batch.length;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
progressBar.stop();
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Process a batch of files
|
|
318
|
+
* @private
|
|
319
|
+
* @param {string[]} batch - Files in this batch
|
|
320
|
+
* @param {Object} options - Processing options
|
|
321
|
+
* @param {Object} uploadService - Upload service
|
|
322
|
+
* @param {string} basePath - Base path
|
|
323
|
+
* @param {Set} processedPaths - Already processed paths
|
|
324
|
+
* @returns {Promise<Object>} Batch results
|
|
325
|
+
*/
|
|
326
|
+
async #processBatch(batch, options, uploadService, basePath, processedPaths) {
|
|
327
|
+
const batchResults = {
|
|
328
|
+
successCount: 0,
|
|
329
|
+
detectedCount: 0,
|
|
330
|
+
organizedCount: 0,
|
|
331
|
+
failureCount: 0,
|
|
332
|
+
skippedCount: 0,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
if (options.statsOnly) {
|
|
336
|
+
// Stats-only mode: just record file information
|
|
337
|
+
const fileObjects = batch.map((filePath) => ({
|
|
338
|
+
path: filePath,
|
|
339
|
+
originalName: path.basename(filePath),
|
|
340
|
+
stats: FileOperations.getFileStats(filePath),
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const result = await databaseService.insertStatsOnlyToUploaderTable(
|
|
345
|
+
fileObjects,
|
|
346
|
+
{
|
|
347
|
+
...options,
|
|
348
|
+
quietMode: options.quietMode || false, // Pass through quiet mode flag
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
batchResults.successCount = result.totalInserted;
|
|
352
|
+
batchResults.skippedCount = result.totalSkipped;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw new Error(`Failed to insert stats: ${error.message}`);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// Upload mode: process files with controlled concurrency to match API replicas
|
|
358
|
+
const maxConcurrentApiCalls =
|
|
359
|
+
appConfig.performance?.maxApiConnections || 10;
|
|
360
|
+
|
|
361
|
+
// Process batch in chunks to respect API replica limits
|
|
362
|
+
const allResults = [];
|
|
363
|
+
for (let i = 0; i < batch.length; i += maxConcurrentApiCalls) {
|
|
364
|
+
const chunk = batch.slice(i, i + maxConcurrentApiCalls);
|
|
365
|
+
|
|
366
|
+
// Process this chunk concurrently (up to API replica count)
|
|
367
|
+
const chunkPromises = chunk.map(async (filePath) => {
|
|
368
|
+
try {
|
|
369
|
+
const result = await this.#processFile(
|
|
370
|
+
filePath,
|
|
371
|
+
options,
|
|
372
|
+
uploadService,
|
|
373
|
+
basePath,
|
|
374
|
+
processedPaths,
|
|
375
|
+
);
|
|
376
|
+
return { success: true, filePath, result };
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this.errorHandler.handleError(error, { filePath });
|
|
379
|
+
return { success: false, filePath, error: error.message };
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Wait for this chunk to complete before starting the next
|
|
384
|
+
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
385
|
+
allResults.push(...chunkResults);
|
|
386
|
+
|
|
387
|
+
// Small delay between chunks to prevent overwhelming API
|
|
388
|
+
if (i + maxConcurrentApiCalls < batch.length) {
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Process all results and update batch results
|
|
394
|
+
allResults.forEach((result) => {
|
|
395
|
+
if (result.status === 'fulfilled') {
|
|
396
|
+
const fileResult = result.value;
|
|
397
|
+
if (fileResult.success) {
|
|
398
|
+
if (fileResult.result && fileResult.result.skipped) {
|
|
399
|
+
batchResults.skippedCount++;
|
|
400
|
+
} else {
|
|
401
|
+
batchResults.successCount++;
|
|
402
|
+
if (fileResult.result && fileResult.result.detectedCount) {
|
|
403
|
+
batchResults.detectedCount += fileResult.result.detectedCount;
|
|
404
|
+
}
|
|
405
|
+
if (fileResult.result && fileResult.result.organizedCount) {
|
|
406
|
+
batchResults.organizedCount += fileResult.result.organizedCount;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
batchResults.failureCount++;
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
batchResults.failureCount++;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return batchResults;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Process a single file
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
async #processFile(
|
|
426
|
+
filePath,
|
|
427
|
+
options,
|
|
428
|
+
uploadService,
|
|
429
|
+
basePath,
|
|
430
|
+
processedPaths,
|
|
431
|
+
) {
|
|
432
|
+
// Skip if already processed
|
|
433
|
+
if (processedPaths.has(filePath)) {
|
|
434
|
+
return { skipped: true };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Prepare file for upload
|
|
438
|
+
const sanitizedName = fileSanitizer.sanitizeFileName(
|
|
439
|
+
path.basename(filePath),
|
|
440
|
+
);
|
|
441
|
+
const pathInfo = pathDetector.extractYearAndPedimentoFromPath(
|
|
442
|
+
filePath,
|
|
443
|
+
basePath,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
let uploadPath = sanitizedName;
|
|
447
|
+
if (pathInfo.detected && options.autoDetectStructure) {
|
|
448
|
+
uploadPath = `${pathInfo.year}/${pathInfo.pedimento}/${sanitizedName}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const fileObject = {
|
|
452
|
+
path: filePath,
|
|
453
|
+
name: sanitizedName,
|
|
454
|
+
contentType: this.#getMimeType(filePath),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Upload based on service type
|
|
458
|
+
let result = { successCount: 1 };
|
|
459
|
+
|
|
460
|
+
if (uploadService.getServiceName() === 'Arela API') {
|
|
461
|
+
result = await uploadService.upload([fileObject], {
|
|
462
|
+
...options,
|
|
463
|
+
uploadPath,
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
// Supabase direct upload
|
|
467
|
+
const uploadResult = await uploadService.upload([fileObject], {
|
|
468
|
+
uploadPath,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Check if upload was successful
|
|
472
|
+
if (!uploadResult.success) {
|
|
473
|
+
throw new Error(`Supabase upload failed: ${uploadResult.error}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
result = { successCount: 1 };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
logger.info(`SUCCESS: ${path.basename(filePath)} -> ${uploadPath}`);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
skipped: false,
|
|
483
|
+
detectedCount: result.detectedCount || 0,
|
|
484
|
+
organizedCount: result.organizedCount || 0,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Get MIME type for file
|
|
490
|
+
* @private
|
|
491
|
+
*/
|
|
492
|
+
#getMimeType(filePath) {
|
|
493
|
+
return mime.lookup(filePath) || 'application/octet-stream';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Update results object
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
#updateResults(target, source) {
|
|
501
|
+
target.successCount += source.successCount;
|
|
502
|
+
target.detectedCount += source.detectedCount;
|
|
503
|
+
target.organizedCount += source.organizedCount;
|
|
504
|
+
target.failureCount += source.failureCount;
|
|
505
|
+
target.skippedCount += source.skippedCount;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Update global results
|
|
510
|
+
* @private
|
|
511
|
+
*/
|
|
512
|
+
#updateGlobalResults(global, source) {
|
|
513
|
+
this.#updateResults(global, source);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Log source summary
|
|
518
|
+
* @private
|
|
519
|
+
*/
|
|
520
|
+
#logSourceSummary(source, result, options) {
|
|
521
|
+
console.log(`\nš¦ Summary for ${source}:`);
|
|
522
|
+
if (options.statsOnly) {
|
|
523
|
+
console.log(` š Stats recorded: ${result.successCount}`);
|
|
524
|
+
console.log(` āļø Duplicates: ${result.skippedCount}`);
|
|
525
|
+
} else {
|
|
526
|
+
console.log(` ā
Uploaded: ${result.successCount}`);
|
|
527
|
+
if (result.detectedCount)
|
|
528
|
+
console.log(` š Detected: ${result.detectedCount}`);
|
|
529
|
+
if (result.organizedCount)
|
|
530
|
+
console.log(` š Organized: ${result.organizedCount}`);
|
|
531
|
+
console.log(` āļø Skipped: ${result.skippedCount}`);
|
|
532
|
+
}
|
|
533
|
+
console.log(` ā Errors: ${result.failureCount}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Log final summary
|
|
538
|
+
* @private
|
|
539
|
+
*/
|
|
540
|
+
#logFinalSummary(results, options, uploadService) {
|
|
541
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
542
|
+
if (options.statsOnly) {
|
|
543
|
+
console.log(`š STATS COLLECTION COMPLETED`);
|
|
544
|
+
console.log(` š Total stats recorded: ${results.successCount}`);
|
|
545
|
+
console.log(` āļø Total duplicates: ${results.skippedCount}`);
|
|
546
|
+
} else {
|
|
547
|
+
console.log(
|
|
548
|
+
`šÆ ${uploadService.getServiceName().toUpperCase()} UPLOAD COMPLETED`,
|
|
549
|
+
);
|
|
550
|
+
console.log(` ā
Total uploaded: ${results.successCount}`);
|
|
551
|
+
if (results.detectedCount)
|
|
552
|
+
console.log(` š Total detected: ${results.detectedCount}`);
|
|
553
|
+
if (results.organizedCount)
|
|
554
|
+
console.log(` š Total organized: ${results.organizedCount}`);
|
|
555
|
+
console.log(` āļø Total skipped: ${results.skippedCount}`);
|
|
556
|
+
}
|
|
557
|
+
console.log(` ā Total errors: ${results.failureCount}`);
|
|
558
|
+
console.log(` š Log file: ${logger.getLogFilePath()}`);
|
|
559
|
+
console.log(`${'='.repeat(60)}\n`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Run additional phases
|
|
564
|
+
* @private
|
|
565
|
+
*/
|
|
566
|
+
async #runAdditionalPhases(options) {
|
|
567
|
+
try {
|
|
568
|
+
// Phase 2: PDF Detection
|
|
569
|
+
console.log('\nš === PHASE 2: PDF Detection ===');
|
|
570
|
+
const detectionResult = await databaseService.detectPedimentosInDatabase({
|
|
571
|
+
batchSize:
|
|
572
|
+
parseInt(options.batchSize) || appConfig.performance.batchSize || 50,
|
|
573
|
+
});
|
|
574
|
+
console.log(
|
|
575
|
+
`ā
Phase 2 Complete: ${detectionResult.detectedCount} detected, ${detectionResult.errorCount} errors`,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// Additional phases would be implemented here
|
|
579
|
+
console.log('\nš All phases completed successfully!');
|
|
580
|
+
} catch (error) {
|
|
581
|
+
this.errorHandler.handleError(error, { phase: 'additional-phases' });
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export default UploadCommand;
|