@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.
- package/.env.template +66 -0
- package/README.md +263 -62
- package/docs/API_ENDPOINTS_FOR_DETECTION.md +647 -0
- package/docs/QUICK_REFERENCE_API_DETECTION.md +264 -0
- package/docs/REFACTORING_SUMMARY_DETECT_PEDIMENTOS.md +200 -0
- package/package.json +3 -2
- package/scripts/cleanup-ds-store.js +109 -0
- package/scripts/cleanup-system-files.js +69 -0
- package/scripts/tests/phase-7-features.test.js +415 -0
- package/scripts/tests/signal-handling.test.js +275 -0
- package/scripts/tests/smart-watch-integration.test.js +554 -0
- package/scripts/tests/watch-service-integration.test.js +584 -0
- package/src/commands/UploadCommand.js +31 -4
- package/src/commands/WatchCommand.js +1342 -0
- package/src/config/config.js +270 -2
- package/src/document-type-shared.js +2 -0
- package/src/document-types/support-document.js +200 -0
- package/src/file-detection.js +9 -1
- package/src/index.js +163 -4
- package/src/services/AdvancedFilterService.js +505 -0
- package/src/services/AutoProcessingService.js +749 -0
- package/src/services/BenchmarkingService.js +381 -0
- package/src/services/DatabaseService.js +1019 -539
- package/src/services/ErrorMonitor.js +275 -0
- package/src/services/LoggingService.js +419 -1
- package/src/services/MonitoringService.js +401 -0
- package/src/services/PerformanceOptimizer.js +511 -0
- package/src/services/ReportingService.js +511 -0
- package/src/services/SignalHandler.js +255 -0
- package/src/services/SmartWatchDatabaseService.js +527 -0
- package/src/services/WatchService.js +783 -0
- package/src/services/upload/ApiUploadService.js +447 -3
- package/src/services/upload/MultiApiUploadService.js +233 -0
- package/src/services/upload/SupabaseUploadService.js +12 -5
- package/src/services/upload/UploadServiceFactory.js +24 -0
- package/src/utils/CleanupManager.js +262 -0
- package/src/utils/FileOperations.js +44 -0
- package/src/utils/WatchEventHandler.js +522 -0
- package/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.envbackup +0 -37
- package/SUPABASE_UPLOAD_FIX.md +0 -157
- 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
|
|
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(
|
|
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(
|
|
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;
|