@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,262 @@
|
|
|
1
|
+
import logger from '../services/LoggingService.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cleanup Manager
|
|
5
|
+
* Centralizes and coordinates cleanup of resources during graceful shutdown
|
|
6
|
+
* Ensures resources are cleaned up in reverse order of registration (LIFO)
|
|
7
|
+
*/
|
|
8
|
+
export class CleanupManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
/**
|
|
11
|
+
* Array of registered resources
|
|
12
|
+
* @type {Array<{name: string, resource: any, cleanupFn: Function}>}
|
|
13
|
+
*/
|
|
14
|
+
this.resources = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Flag to prevent concurrent cleanup
|
|
18
|
+
* @type {boolean}
|
|
19
|
+
*/
|
|
20
|
+
this.cleanupInProgress = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Timestamp when cleanup started
|
|
24
|
+
* @type {number|null}
|
|
25
|
+
*/
|
|
26
|
+
this.cleanupStartTime = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cleanup timeout duration (30 seconds)
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
this.cleanupTimeout = 30000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Results of cleanup operations
|
|
36
|
+
* @type {Array<{name: string, success: boolean, duration: number, error: string|null}>}
|
|
37
|
+
*/
|
|
38
|
+
this.cleanupResults = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a resource to be cleaned up during shutdown
|
|
43
|
+
* Resources are cleaned up in reverse order (LIFO)
|
|
44
|
+
* @param {string} name - Name of the resource
|
|
45
|
+
* @param {any} resource - The resource object
|
|
46
|
+
* @param {Function} cleanupFn - Async function to clean up the resource
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*/
|
|
49
|
+
registerResource(name, resource, cleanupFn) {
|
|
50
|
+
if (!name || typeof name !== 'string') {
|
|
51
|
+
logger.warn('CleanupManager: Resource name must be a non-empty string');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof cleanupFn !== 'function') {
|
|
56
|
+
logger.warn(
|
|
57
|
+
`CleanupManager: Cleanup function for "${name}" is not a function`,
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.resources.push({
|
|
63
|
+
name,
|
|
64
|
+
resource,
|
|
65
|
+
cleanupFn,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
logger.debug(`CleanupManager: Registered resource "${name}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Perform cleanup of all registered resources in reverse order
|
|
73
|
+
* @param {string} reason - Reason for cleanup (e.g., 'SIGINT', 'SIGTERM')
|
|
74
|
+
* @returns {Promise<Object>} Cleanup results
|
|
75
|
+
*/
|
|
76
|
+
async performCleanup(reason = 'unknown') {
|
|
77
|
+
// Prevent concurrent cleanup operations
|
|
78
|
+
if (this.cleanupInProgress) {
|
|
79
|
+
logger.warn(
|
|
80
|
+
`CleanupManager: Cleanup already in progress, skipping new cleanup request`,
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
reason: 'Cleanup already in progress',
|
|
85
|
+
results: this.cleanupResults,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.cleanupInProgress = true;
|
|
90
|
+
this.cleanupStartTime = Date.now();
|
|
91
|
+
this.cleanupResults = [];
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
`CleanupManager: Starting cleanup sequence (reason: ${reason})`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Set timeout for cleanup
|
|
98
|
+
const cleanupTimeoutId = setTimeout(() => {
|
|
99
|
+
logger.error('CleanupManager: Cleanup timeout reached, forcing exit');
|
|
100
|
+
}, this.cleanupTimeout);
|
|
101
|
+
|
|
102
|
+
// Clean up resources in reverse order (LIFO)
|
|
103
|
+
for (let i = this.resources.length - 1; i >= 0; i--) {
|
|
104
|
+
const { name, cleanupFn } = this.resources[i];
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
logger.debug(`CleanupManager: Cleaning up resource "${name}"...`);
|
|
109
|
+
|
|
110
|
+
// Execute cleanup function
|
|
111
|
+
const result = cleanupFn();
|
|
112
|
+
|
|
113
|
+
// Handle both sync and async cleanup functions
|
|
114
|
+
if (result && typeof result.catch === 'function') {
|
|
115
|
+
await result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const duration = Date.now() - startTime;
|
|
119
|
+
this.cleanupResults.push({
|
|
120
|
+
name,
|
|
121
|
+
success: true,
|
|
122
|
+
duration,
|
|
123
|
+
error: null,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
logger.info(
|
|
127
|
+
`CleanupManager: Successfully cleaned up "${name}" (${duration}ms)`,
|
|
128
|
+
);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const duration = Date.now() - startTime;
|
|
131
|
+
this.cleanupResults.push({
|
|
132
|
+
name,
|
|
133
|
+
success: false,
|
|
134
|
+
duration,
|
|
135
|
+
error: error.message,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
logger.error(
|
|
139
|
+
`CleanupManager: Error cleaning up "${name}": ${error.message}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Clear timeout
|
|
145
|
+
clearTimeout(cleanupTimeoutId);
|
|
146
|
+
|
|
147
|
+
const totalDuration = Date.now() - this.cleanupStartTime;
|
|
148
|
+
const successCount = this.cleanupResults.filter((r) => r.success).length;
|
|
149
|
+
const failureCount = this.cleanupResults.filter((r) => !r.success).length;
|
|
150
|
+
|
|
151
|
+
logger.info(
|
|
152
|
+
`CleanupManager: Cleanup complete (${totalDuration}ms, ${successCount} successful, ${failureCount} failed)`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: failureCount === 0,
|
|
157
|
+
reason,
|
|
158
|
+
totalDuration,
|
|
159
|
+
successCount,
|
|
160
|
+
failureCount,
|
|
161
|
+
results: this.cleanupResults,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clear all registered resources without cleaning them up
|
|
167
|
+
* Use this for resetting the cleanup manager
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
clearResources() {
|
|
171
|
+
const count = this.resources.length;
|
|
172
|
+
this.resources = [];
|
|
173
|
+
logger.debug(`CleanupManager: Cleared ${count} registered resources`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the number of registered resources
|
|
178
|
+
* @returns {number} Number of registered resources
|
|
179
|
+
*/
|
|
180
|
+
getResourceCount() {
|
|
181
|
+
return this.resources.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get details of all registered resources
|
|
186
|
+
* @returns {Array<string>} Array of resource names
|
|
187
|
+
*/
|
|
188
|
+
getResourceNames() {
|
|
189
|
+
return this.resources.map((r) => r.name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if cleanup is in progress
|
|
194
|
+
* @returns {boolean} True if cleanup is currently running
|
|
195
|
+
*/
|
|
196
|
+
isCleanupInProgress() {
|
|
197
|
+
return this.cleanupInProgress;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get cleanup results from last operation
|
|
202
|
+
* @returns {Array<Object>} Array of cleanup results
|
|
203
|
+
*/
|
|
204
|
+
getCleanupResults() {
|
|
205
|
+
return [...this.cleanupResults];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate a formatted report of the cleanup operation
|
|
210
|
+
* @returns {string} Formatted cleanup report
|
|
211
|
+
*/
|
|
212
|
+
generateCleanupReport() {
|
|
213
|
+
if (this.cleanupResults.length === 0) {
|
|
214
|
+
return 'No cleanup operations recorded';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const totalDuration = this.cleanupStartTime
|
|
218
|
+
? Date.now() - this.cleanupStartTime
|
|
219
|
+
: 0;
|
|
220
|
+
|
|
221
|
+
let report =
|
|
222
|
+
'\n═══════════════════════════════════════════════════════════\n';
|
|
223
|
+
report += 'CLEANUP OPERATION REPORT\n';
|
|
224
|
+
report += '═══════════════════════════════════════════════════════════\n\n';
|
|
225
|
+
|
|
226
|
+
for (const result of this.cleanupResults) {
|
|
227
|
+
const status = result.success ? '✅' : '❌';
|
|
228
|
+
report += `${status} ${result.name}\n`;
|
|
229
|
+
report += ` Duration: ${result.duration}ms\n`;
|
|
230
|
+
if (result.error) {
|
|
231
|
+
report += ` Error: ${result.error}\n`;
|
|
232
|
+
}
|
|
233
|
+
report += '\n';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const successCount = this.cleanupResults.filter((r) => r.success).length;
|
|
237
|
+
const failureCount = this.cleanupResults.filter((r) => !r.success).length;
|
|
238
|
+
|
|
239
|
+
report += '─────────────────────────────────────────────────────────────\n';
|
|
240
|
+
report += `Total Duration: ${totalDuration}ms\n`;
|
|
241
|
+
report += `Successful: ${successCount}\n`;
|
|
242
|
+
report += `Failed: ${failureCount}\n`;
|
|
243
|
+
report += '═══════════════════════════════════════════════════════════\n';
|
|
244
|
+
|
|
245
|
+
return report;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reset cleanup state (useful for testing)
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
reset() {
|
|
253
|
+
this.cleanupInProgress = false;
|
|
254
|
+
this.cleanupStartTime = null;
|
|
255
|
+
this.cleanupResults = [];
|
|
256
|
+
logger.debug('CleanupManager: Reset to initial state');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Export singleton instance
|
|
261
|
+
export const cleanupManager = new CleanupManager();
|
|
262
|
+
export default cleanupManager;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File Operations Utility
|
|
6
|
+
* Provides optimized file system operations with batching
|
|
7
|
+
*/
|
|
8
|
+
export class FileOperations {
|
|
9
|
+
/**
|
|
10
|
+
* Batch read file stats for multiple files
|
|
11
|
+
* @param {string[]} filePaths - Array of file paths
|
|
12
|
+
* @returns {Array} Array of results with path, stats, and error
|
|
13
|
+
*/
|
|
14
|
+
static batchReadFileStats(filePaths) {
|
|
15
|
+
const results = [];
|
|
16
|
+
|
|
17
|
+
for (const filePath of filePaths) {
|
|
18
|
+
try {
|
|
19
|
+
const stats = fs.statSync(filePath);
|
|
20
|
+
results.push({ path: filePath, stats, error: null });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
results.push({ path: filePath, stats: null, error: error.message });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if file exists synchronously
|
|
31
|
+
* @param {string} filePath - Path to file
|
|
32
|
+
* @returns {boolean} True if file exists
|
|
33
|
+
*/
|
|
34
|
+
static fileExists(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.existsSync(filePath);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get file size safely
|
|
44
|
+
* @param {string} filePath - Path to file
|
|
45
|
+
* @returns {number|null} File size in bytes or null if error
|
|
46
|
+
*/
|
|
47
|
+
static getFileSize(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
const stats = fs.statSync(filePath);
|
|
50
|
+
return stats.size;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get file modification time
|
|
58
|
+
* @param {string} filePath - Path to file
|
|
59
|
+
* @returns {Date|null} Modification time or null if error
|
|
60
|
+
*/
|
|
61
|
+
static getFileModTime(filePath) {
|
|
62
|
+
try {
|
|
63
|
+
const stats = fs.statSync(filePath);
|
|
64
|
+
return stats.mtime;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get file stats
|
|
72
|
+
* @param {string} filePath - Path to file
|
|
73
|
+
* @returns {Object|null} File stats or null if error
|
|
74
|
+
*/
|
|
75
|
+
static getFileStats(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
return fs.statSync(filePath);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create directory recursively if it doesn't exist
|
|
85
|
+
* @param {string} dirPath - Directory path
|
|
86
|
+
* @returns {boolean} True if successful
|
|
87
|
+
*/
|
|
88
|
+
static ensureDirectory(dirPath) {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(dirPath)) {
|
|
91
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read file with error handling
|
|
101
|
+
* @param {string} filePath - Path to file
|
|
102
|
+
* @param {string} encoding - File encoding (default: 'utf-8')
|
|
103
|
+
* @returns {string|null} File content or null if error
|
|
104
|
+
*/
|
|
105
|
+
static readFileContent(filePath, encoding = 'utf-8') {
|
|
106
|
+
try {
|
|
107
|
+
return fs.readFileSync(filePath, encoding);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Write file with error handling and directory creation
|
|
115
|
+
* @param {string} filePath - Path to file
|
|
116
|
+
* @param {string} content - Content to write
|
|
117
|
+
* @param {string} encoding - File encoding (default: 'utf-8')
|
|
118
|
+
* @returns {boolean} True if successful
|
|
119
|
+
*/
|
|
120
|
+
static writeFileContent(filePath, content, encoding = 'utf-8') {
|
|
121
|
+
try {
|
|
122
|
+
const dir = path.dirname(filePath);
|
|
123
|
+
this.ensureDirectory(dir);
|
|
124
|
+
fs.writeFileSync(filePath, content, encoding);
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Append to file with error handling
|
|
133
|
+
* @param {string} filePath - Path to file
|
|
134
|
+
* @param {string} content - Content to append
|
|
135
|
+
* @param {string} encoding - File encoding (default: 'utf-8')
|
|
136
|
+
* @returns {boolean} True if successful
|
|
137
|
+
*/
|
|
138
|
+
static appendToFile(filePath, content, encoding = 'utf-8') {
|
|
139
|
+
try {
|
|
140
|
+
fs.appendFileSync(filePath, content, encoding);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List all files in a directory (non-recursive)
|
|
149
|
+
* @param {string} dirPath - Directory path
|
|
150
|
+
* @param {Object} options - Options { excludePattern, onlyPdf }
|
|
151
|
+
* @returns {Array} Array of file paths
|
|
152
|
+
*/
|
|
153
|
+
static listFilesInDirectory(dirPath, options = {}) {
|
|
154
|
+
try {
|
|
155
|
+
const {
|
|
156
|
+
excludePattern = /(^|[\/\\])\.|node_modules|\.git/,
|
|
157
|
+
onlyPdf = false,
|
|
158
|
+
} = options;
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(dirPath)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
165
|
+
const files = [];
|
|
166
|
+
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
169
|
+
|
|
170
|
+
// Skip excluded patterns
|
|
171
|
+
if (excludePattern && excludePattern.test(fullPath)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Only include files, not directories
|
|
176
|
+
if (entry.isFile()) {
|
|
177
|
+
// If onlyPdf is true, filter for PDF files
|
|
178
|
+
if (onlyPdf && !entry.name.toLowerCase().endsWith('.pdf')) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
files.push(fullPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return files;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default FileOperations;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* File Sanitization Utility
|
|
5
|
+
* Handles filename sanitization with caching for performance
|
|
6
|
+
*/
|
|
7
|
+
export class FileSanitizer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.cache = new Map();
|
|
10
|
+
this.patterns = [
|
|
11
|
+
[/[áàâäãåāăą]/gi, 'a'],
|
|
12
|
+
[/[éèêëēĕėę]/gi, 'e'],
|
|
13
|
+
[/[íìîïīĭį]/gi, 'i'],
|
|
14
|
+
[/[óòôöõōŏő]/gi, 'o'],
|
|
15
|
+
[/[úùûüūŭů]/gi, 'u'],
|
|
16
|
+
[/[ñň]/gi, 'n'],
|
|
17
|
+
[/[ç]/gi, 'c'],
|
|
18
|
+
[/[ý]/gi, 'y'],
|
|
19
|
+
[/[멕]/g, 'meok'],
|
|
20
|
+
[/[시]/g, 'si'],
|
|
21
|
+
[/[코]/g, 'ko'],
|
|
22
|
+
[/[용]/g, 'yong'],
|
|
23
|
+
[/[가-힣]/g, 'kr'],
|
|
24
|
+
[/[\u0300-\u036f]/g, ''],
|
|
25
|
+
[/[\\?%*:|"<>[\]~`^]/g, '-'],
|
|
26
|
+
[/[{}]/g, '-'],
|
|
27
|
+
[/[&]/g, 'and'],
|
|
28
|
+
[/[()]/g, ''],
|
|
29
|
+
[/\s+/g, '-'],
|
|
30
|
+
[/-+/g, '-'],
|
|
31
|
+
[/^-+|-+$/g, ''],
|
|
32
|
+
[/^\.+/, ''],
|
|
33
|
+
[/[^\w.-]/g, ''],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sanitize a filename for safe storage
|
|
39
|
+
* @param {string} fileName - Original filename
|
|
40
|
+
* @returns {string} Sanitized filename
|
|
41
|
+
*/
|
|
42
|
+
sanitizeFileName(fileName) {
|
|
43
|
+
if (this.cache.has(fileName)) {
|
|
44
|
+
return this.cache.get(fileName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ext = path.extname(fileName);
|
|
48
|
+
const nameWithoutExt = path.basename(fileName, ext);
|
|
49
|
+
|
|
50
|
+
// If already clean, return as-is
|
|
51
|
+
if (/^[a-zA-Z0-9._-]+$/.test(nameWithoutExt)) {
|
|
52
|
+
const result = fileName;
|
|
53
|
+
this.cache.set(fileName, result);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let sanitized = nameWithoutExt.normalize('NFD');
|
|
58
|
+
|
|
59
|
+
// Apply all sanitization patterns
|
|
60
|
+
for (const [pattern, replacement] of this.patterns) {
|
|
61
|
+
sanitized = sanitized.replace(pattern, replacement);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Additional cleanup
|
|
65
|
+
sanitized = sanitized
|
|
66
|
+
.replace(/~/g, '-')
|
|
67
|
+
.replace(/\s+/g, '-')
|
|
68
|
+
.replace(/\.+/g, '-')
|
|
69
|
+
.replace(/-+/g, '-')
|
|
70
|
+
.replace(/^-+|-+$/g, '');
|
|
71
|
+
|
|
72
|
+
if (!sanitized) {
|
|
73
|
+
sanitized = 'unnamed_file';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = sanitized + ext;
|
|
77
|
+
this.cache.set(fileName, result);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear the sanitization cache
|
|
83
|
+
*/
|
|
84
|
+
clearCache() {
|
|
85
|
+
this.cache.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get cache size for monitoring
|
|
90
|
+
* @returns {number} Number of cached entries
|
|
91
|
+
*/
|
|
92
|
+
getCacheSize() {
|
|
93
|
+
return this.cache.size;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Export singleton instance
|
|
98
|
+
export const fileSanitizer = new FileSanitizer();
|
|
99
|
+
export default fileSanitizer;
|