@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.
Files changed (95) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. 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;