@arela/uploader 0.2.13 → 1.0.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 (43) hide show
  1. package/.env.template +66 -0
  2. package/README.md +263 -62
  3. package/docs/API_ENDPOINTS_FOR_DETECTION.md +647 -0
  4. package/docs/QUICK_REFERENCE_API_DETECTION.md +264 -0
  5. package/docs/REFACTORING_SUMMARY_DETECT_PEDIMENTOS.md +200 -0
  6. package/package.json +3 -2
  7. package/scripts/cleanup-ds-store.js +109 -0
  8. package/scripts/cleanup-system-files.js +69 -0
  9. package/scripts/tests/phase-7-features.test.js +415 -0
  10. package/scripts/tests/signal-handling.test.js +275 -0
  11. package/scripts/tests/smart-watch-integration.test.js +554 -0
  12. package/scripts/tests/watch-service-integration.test.js +584 -0
  13. package/src/commands/UploadCommand.js +31 -4
  14. package/src/commands/WatchCommand.js +1342 -0
  15. package/src/config/config.js +270 -2
  16. package/src/document-type-shared.js +2 -0
  17. package/src/document-types/support-document.js +200 -0
  18. package/src/file-detection.js +9 -1
  19. package/src/index.js +163 -4
  20. package/src/services/AdvancedFilterService.js +505 -0
  21. package/src/services/AutoProcessingService.js +749 -0
  22. package/src/services/BenchmarkingService.js +381 -0
  23. package/src/services/DatabaseService.js +1019 -539
  24. package/src/services/ErrorMonitor.js +275 -0
  25. package/src/services/LoggingService.js +419 -1
  26. package/src/services/MonitoringService.js +401 -0
  27. package/src/services/PerformanceOptimizer.js +511 -0
  28. package/src/services/ReportingService.js +511 -0
  29. package/src/services/SignalHandler.js +255 -0
  30. package/src/services/SmartWatchDatabaseService.js +527 -0
  31. package/src/services/WatchService.js +783 -0
  32. package/src/services/upload/ApiUploadService.js +447 -3
  33. package/src/services/upload/MultiApiUploadService.js +233 -0
  34. package/src/services/upload/SupabaseUploadService.js +12 -5
  35. package/src/services/upload/UploadServiceFactory.js +24 -0
  36. package/src/utils/CleanupManager.js +262 -0
  37. package/src/utils/FileOperations.js +44 -0
  38. package/src/utils/WatchEventHandler.js +522 -0
  39. package/supabase/migrations/001_create_initial_schema.sql +366 -0
  40. package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  41. package/.envbackup +0 -37
  42. package/SUPABASE_UPLOAD_FIX.md +0 -157
  43. package/commands.md +0 -14
@@ -0,0 +1,233 @@
1
+ import { Agent } from 'http';
2
+ import { Agent as HttpsAgent } from 'https';
3
+ import fetch from 'node-fetch';
4
+
5
+ import appConfig from '../../config/config.js';
6
+ import logger from '../LoggingService.js';
7
+ import { ApiUploadService } from './ApiUploadService.js';
8
+
9
+ /**
10
+ * Multi-API Upload Service
11
+ *
12
+ * Extends ApiUploadService to support uploading to multiple API instances
13
+ * based on RFC/client routing. Uses service discovery from Global API.
14
+ */
15
+ export class MultiApiUploadService extends ApiUploadService {
16
+ constructor() {
17
+ super();
18
+
19
+ // Service discovery configuration
20
+ this.globalApiUrl = process.env.API_GLOBAL_URL || appConfig.api.baseUrl;
21
+ this.serviceCache = new Map(); // RFC -> { url, cachedAt }
22
+ this.cacheTtlMs = parseInt(process.env.SERVICE_CACHE_TTL) || 300000; // 5 minutes
23
+
24
+ logger.info('🌐 MultiApiUploadService initialized');
25
+ logger.info(` Global API: ${this.globalApiUrl}`);
26
+ }
27
+
28
+ /**
29
+ * Discover service URL for a specific RFC
30
+ * @param {string} rfc - The client RFC
31
+ * @returns {Promise<string|null>} Service URL or null
32
+ */
33
+ async discoverServiceByRfc(rfc) {
34
+ const rfcUpper = rfc.toUpperCase();
35
+
36
+ // Check cache first
37
+ const cached = this.serviceCache.get(rfcUpper);
38
+ if (cached && Date.now() - cached.cachedAt < this.cacheTtlMs) {
39
+ logger.debug(
40
+ `šŸ“‹ Using cached service URL for ${rfcUpper}: ${cached.url}`,
41
+ );
42
+ return cached.url;
43
+ }
44
+
45
+ // Fetch from Global API
46
+ try {
47
+ const response = await fetch(
48
+ `${this.globalApiUrl}/api/service-registry/by-rfc/${rfcUpper}`,
49
+ {
50
+ method: 'GET',
51
+ headers: {
52
+ Authorization: `Bearer ${this.token}`,
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ agent: this.globalApiUrl.startsWith('https')
56
+ ? this.httpsAgent
57
+ : this.httpAgent,
58
+ },
59
+ );
60
+
61
+ if (!response.ok) {
62
+ logger.warn(
63
+ `āš ļø Service discovery failed for ${rfcUpper}: ${response.status}`,
64
+ );
65
+ return null;
66
+ }
67
+
68
+ const data = await response.json();
69
+
70
+ if (data.url) {
71
+ // Cache the result
72
+ this.serviceCache.set(rfcUpper, {
73
+ url: data.url,
74
+ cachedAt: Date.now(),
75
+ });
76
+
77
+ logger.info(`šŸ” Discovered service for ${rfcUpper}: ${data.url}`);
78
+ return data.url;
79
+ }
80
+
81
+ return null;
82
+ } catch (error) {
83
+ logger.error(
84
+ `āŒ Service discovery error for ${rfcUpper}: ${error.message}`,
85
+ );
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Clear service cache
92
+ */
93
+ clearCache() {
94
+ this.serviceCache.clear();
95
+ logger.info('🧹 Service cache cleared');
96
+ }
97
+
98
+ /**
99
+ * Clear cache for specific RFC
100
+ * @param {string} rfc - The RFC to clear
101
+ */
102
+ clearRfcCache(rfc) {
103
+ this.serviceCache.delete(rfc.toUpperCase());
104
+ }
105
+
106
+ /**
107
+ * Upload files to the appropriate API based on RFC
108
+ * @param {Array} files - Array of file objects
109
+ * @param {Object} options - Upload options including rfc
110
+ * @returns {Promise<Object>} API response
111
+ */
112
+ async upload(files, options) {
113
+ const { rfc } = options;
114
+
115
+ if (!rfc) {
116
+ // No RFC specified, use default API
117
+ return super.upload(files, options);
118
+ }
119
+
120
+ // Discover the service URL for this RFC
121
+ const serviceUrl = await this.discoverServiceByRfc(rfc);
122
+
123
+ if (serviceUrl) {
124
+ // Override baseUrl for this upload
125
+ const originalBaseUrl = this.baseUrl;
126
+ this.baseUrl = serviceUrl;
127
+
128
+ try {
129
+ logger.info(
130
+ `šŸ“¤ Uploading ${files.length} files to ${serviceUrl} (RFC: ${rfc})`,
131
+ );
132
+ const result = await super.upload(files, options);
133
+ return result;
134
+ } finally {
135
+ // Restore original baseUrl
136
+ this.baseUrl = originalBaseUrl;
137
+ }
138
+ }
139
+
140
+ // Fallback to default API if service discovery fails
141
+ logger.warn(`āš ļø No specific service found for ${rfc}, using default API`);
142
+ return super.upload(files, options);
143
+ }
144
+
145
+ /**
146
+ * Upload files to multiple RFCs in parallel
147
+ * @param {Object} filesByRfc - Map of RFC -> files array
148
+ * @param {Object} options - Upload options
149
+ * @returns {Promise<Map<string, Object>>} Results by RFC
150
+ */
151
+ async uploadToMultipleRfcs(filesByRfc, options = {}) {
152
+ const results = new Map();
153
+ const rfcs = Object.keys(filesByRfc);
154
+
155
+ logger.info(`šŸ“¤ Starting multi-RFC upload for ${rfcs.length} RFCs`);
156
+
157
+ // Process RFCs in parallel (with concurrency limit)
158
+ const concurrencyLimit = parseInt(process.env.MULTI_RFC_CONCURRENCY) || 3;
159
+ const chunks = [];
160
+
161
+ for (let i = 0; i < rfcs.length; i += concurrencyLimit) {
162
+ chunks.push(rfcs.slice(i, i + concurrencyLimit));
163
+ }
164
+
165
+ for (const chunk of chunks) {
166
+ await Promise.all(
167
+ chunk.map(async (rfc) => {
168
+ try {
169
+ const files = filesByRfc[rfc];
170
+ const result = await this.upload(files, { ...options, rfc });
171
+ results.set(rfc, { success: true, result });
172
+ } catch (error) {
173
+ logger.error(`āŒ Upload failed for RFC ${rfc}: ${error.message}`);
174
+ results.set(rfc, { success: false, error: error.message });
175
+ }
176
+ }),
177
+ );
178
+ }
179
+
180
+ // Log summary
181
+ const successful = [...results.values()].filter((r) => r.success).length;
182
+ const failed = results.size - successful;
183
+ logger.info(
184
+ `āœ… Multi-RFC upload complete: ${successful} successful, ${failed} failed`,
185
+ );
186
+
187
+ return results;
188
+ }
189
+
190
+ /**
191
+ * Get all available services from Global API
192
+ * @returns {Promise<Array>} List of services
193
+ */
194
+ async getAllServices() {
195
+ try {
196
+ const response = await fetch(
197
+ `${this.globalApiUrl}/api/service-registry/services`,
198
+ {
199
+ method: 'GET',
200
+ headers: {
201
+ Authorization: `Bearer ${this.token}`,
202
+ 'Content-Type': 'application/json',
203
+ },
204
+ agent: this.globalApiUrl.startsWith('https')
205
+ ? this.httpsAgent
206
+ : this.httpAgent,
207
+ },
208
+ );
209
+
210
+ if (!response.ok) {
211
+ throw new Error(`Failed to fetch services: ${response.status}`);
212
+ }
213
+
214
+ const data = await response.json();
215
+ return data.services || [];
216
+ } catch (error) {
217
+ logger.error(`āŒ Failed to get services: ${error.message}`);
218
+ return [];
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Pre-warm cache for a list of RFCs
224
+ * @param {Array<string>} rfcs - List of RFCs to cache
225
+ */
226
+ async prewarmCache(rfcs) {
227
+ logger.info(`šŸ”„ Pre-warming cache for ${rfcs.length} RFCs`);
228
+
229
+ await Promise.all(rfcs.map((rfc) => this.discoverServiceByRfc(rfc)));
230
+
231
+ logger.info('āœ… Cache pre-warm complete');
232
+ }
233
+ }
@@ -57,12 +57,16 @@ export class SupabaseUploadService extends BaseUploadService {
57
57
 
58
58
  // Test connection and bucket access
59
59
  console.log(`🧪 Testing bucket access...`);
60
- const { error } = await this.client.storage.from(this.bucket).list('', { limit: 1 });
60
+ const { error } = await this.client.storage
61
+ .from(this.bucket)
62
+ .list('', { limit: 1 });
61
63
  if (error) {
62
64
  console.error(`āŒ Bucket access test failed:`, error.message);
63
- throw new Error(`Error connecting to Supabase bucket '${this.bucket}': ${error.message}`);
65
+ throw new Error(
66
+ `Error connecting to Supabase bucket '${this.bucket}': ${error.message}`,
67
+ );
64
68
  }
65
-
69
+
66
70
  console.log(`āœ… Successfully connected to Supabase bucket: ${this.bucket}`);
67
71
  }
68
72
 
@@ -92,7 +96,7 @@ export class SupabaseUploadService extends BaseUploadService {
92
96
  const contentType = mime.lookup(file.path) || 'application/octet-stream';
93
97
 
94
98
  const normalizedPath = uploadPath.replace(/\\/g, '/');
95
-
99
+
96
100
  const { data, error } = await this.client.storage
97
101
  .from(this.bucket)
98
102
  .upload(normalizedPath, content, {
@@ -101,7 +105,10 @@ export class SupabaseUploadService extends BaseUploadService {
101
105
  });
102
106
 
103
107
  if (error) {
104
- console.error(`āŒ Supabase upload failed for ${normalizedPath}:`, error.message);
108
+ console.error(
109
+ `āŒ Supabase upload failed for ${normalizedPath}:`,
110
+ error.message,
111
+ );
105
112
  return { success: false, error: error.message };
106
113
  }
107
114
 
@@ -9,6 +9,8 @@ export class UploadServiceFactory {
9
9
  constructor() {
10
10
  this.apiService = new ApiUploadService();
11
11
  this.supabaseService = new SupabaseUploadService();
12
+ // Cache for target-specific API services
13
+ this._targetServices = new Map();
12
14
  }
13
15
 
14
16
  /**
@@ -54,6 +56,28 @@ export class UploadServiceFactory {
54
56
  throw new Error('No upload service is available');
55
57
  }
56
58
 
59
+ /**
60
+ * Get an API service configured for a specific target
61
+ * @param {string} target - API target: 'agencia', 'cliente', 'ktj931117p55', or 'default'
62
+ * @returns {Promise<ApiUploadService>} API service for the specified target
63
+ */
64
+ async getApiServiceForTarget(target) {
65
+ // Import config dynamically to avoid circular dependency
66
+ const { appConfig } = await import('../../config/config.js');
67
+
68
+ const targetConfig = appConfig.getApiConfig(target);
69
+ if (!targetConfig.baseUrl) {
70
+ throw new Error(`No API URL configured for target '${target}'`);
71
+ }
72
+
73
+ // Create a new service instance with specific config
74
+ // Use setExternalConfig to prevent automatic config refresh from overwriting
75
+ const service = new ApiUploadService();
76
+ service.setExternalConfig(targetConfig.baseUrl, targetConfig.token);
77
+
78
+ return service;
79
+ }
80
+
57
81
  /**
58
82
  * Check if API mode is currently active
59
83
  * @returns {Promise<boolean>} True if API service is available
@@ -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;
@@ -143,6 +143,50 @@ export class FileOperations {
143
143
  return false;
144
144
  }
145
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
+ }
146
190
  }
147
191
 
148
192
  export default FileOperations;