@arela/uploader 0.2.12 ā 0.3.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/.vscode/settings.json +1 -0
- package/README.md +134 -58
- package/SUPABASE_UPLOAD_FIX.md +157 -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 +36 -2
- package/src/commands/WatchCommand.js +1305 -0
- package/src/config/config.js +113 -0
- package/src/document-type-shared.js +2 -0
- package/src/document-types/support-document.js +201 -0
- package/src/file-detection.js +2 -1
- package/src/index.js +44 -0
- package/src/services/AdvancedFilterService.js +505 -0
- package/src/services/AutoProcessingService.js +639 -0
- package/src/services/BenchmarkingService.js +381 -0
- package/src/services/DatabaseService.js +723 -170
- 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 +30 -4
- package/src/services/upload/SupabaseUploadService.js +28 -6
- package/src/utils/CleanupManager.js +262 -0
- package/src/utils/FileOperations.js +41 -0
- package/src/utils/WatchEventHandler.js +517 -0
- package/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/commands.md +0 -6
|
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
|
|
9
9
|
import appConfig from '../../config/config.js';
|
|
10
|
+
import logger from '../LoggingService.js';
|
|
10
11
|
import { BaseUploadService } from './BaseUploadService.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -57,22 +58,47 @@ export class ApiUploadService extends BaseUploadService {
|
|
|
57
58
|
* @returns {Promise<Object>} API response
|
|
58
59
|
*/
|
|
59
60
|
async upload(files, options) {
|
|
61
|
+
// Validate files parameter
|
|
62
|
+
if (!files || !Array.isArray(files)) {
|
|
63
|
+
logger.warn(`Invalid files parameter: ${typeof files}`);
|
|
64
|
+
throw new Error('Files must be an array');
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
const formData = new FormData();
|
|
61
68
|
|
|
69
|
+
// Filter out system files (macOS, Windows, etc.)
|
|
70
|
+
const systemFilePattern = /^\.|__pycache__|\.pyc|\.swp|\.swo|Thumbs\.db|desktop\.ini|DS_Store|\$RECYCLE\.BIN|System Volume Information|~\$|\.tmp/i;
|
|
71
|
+
const filteredFiles = files.filter(file => {
|
|
72
|
+
const fileName = file.name || path.basename(file.path);
|
|
73
|
+
if (systemFilePattern.test(fileName)) {
|
|
74
|
+
logger.warn(`Skipping system file from upload: ${fileName}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (filteredFiles.length === 0) {
|
|
81
|
+
throw new Error('No valid files to upload after filtering system files');
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
// Add files to form data asynchronously
|
|
63
|
-
for (const file of
|
|
85
|
+
for (const file of filteredFiles) {
|
|
64
86
|
try {
|
|
65
87
|
// Check file size for streaming vs buffer approach
|
|
66
|
-
|
|
88
|
+
let size = file.size;
|
|
89
|
+
if (size === undefined || size === null) {
|
|
90
|
+
const stats = await fs.promises.stat(file.path);
|
|
91
|
+
size = stats.size;
|
|
92
|
+
}
|
|
67
93
|
const fileSizeThreshold = 10 * 1024 * 1024; // 10MB threshold
|
|
68
94
|
|
|
69
|
-
if (
|
|
95
|
+
if (size > fileSizeThreshold) {
|
|
70
96
|
// Use streaming for large files
|
|
71
97
|
const fileStream = fs.createReadStream(file.path);
|
|
72
98
|
formData.append('files', fileStream, {
|
|
73
99
|
filename: file.name,
|
|
74
100
|
contentType: file.contentType,
|
|
75
|
-
knownLength:
|
|
101
|
+
knownLength: size,
|
|
76
102
|
});
|
|
77
103
|
} else {
|
|
78
104
|
// Use buffer for smaller files
|
|
@@ -26,9 +26,16 @@ export class SupabaseUploadService extends BaseUploadService {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (!appConfig.supabase.url || !appConfig.supabase.key || !this.bucket) {
|
|
29
|
-
|
|
29
|
+
const missing = [];
|
|
30
|
+
if (!appConfig.supabase.url) missing.push('SUPABASE_URL');
|
|
31
|
+
if (!appConfig.supabase.key) missing.push('SUPABASE_KEY');
|
|
32
|
+
if (!this.bucket) missing.push('SUPABASE_BUCKET');
|
|
33
|
+
throw new Error(`Missing Supabase configuration: ${missing.join(', ')}`);
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
console.log(`š Connecting to Supabase: ${appConfig.supabase.url}`);
|
|
37
|
+
console.log(`š¦ Using bucket: ${this.bucket}`);
|
|
38
|
+
|
|
32
39
|
this.client = createClient(appConfig.supabase.url, appConfig.supabase.key, {
|
|
33
40
|
db: {
|
|
34
41
|
schema: 'public',
|
|
@@ -48,11 +55,19 @@ export class SupabaseUploadService extends BaseUploadService {
|
|
|
48
55
|
},
|
|
49
56
|
});
|
|
50
57
|
|
|
51
|
-
// Test connection
|
|
52
|
-
|
|
58
|
+
// Test connection and bucket access
|
|
59
|
+
console.log(`š§Ŗ Testing bucket access...`);
|
|
60
|
+
const { error } = await this.client.storage
|
|
61
|
+
.from(this.bucket)
|
|
62
|
+
.list('', { limit: 1 });
|
|
53
63
|
if (error) {
|
|
54
|
-
|
|
64
|
+
console.error(`ā Bucket access test failed:`, error.message);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Error connecting to Supabase bucket '${this.bucket}': ${error.message}`,
|
|
67
|
+
);
|
|
55
68
|
}
|
|
69
|
+
|
|
70
|
+
console.log(`ā
Successfully connected to Supabase bucket: ${this.bucket}`);
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
/**
|
|
@@ -80,18 +95,25 @@ export class SupabaseUploadService extends BaseUploadService {
|
|
|
80
95
|
const content = fs.readFileSync(file.path);
|
|
81
96
|
const contentType = mime.lookup(file.path) || 'application/octet-stream';
|
|
82
97
|
|
|
98
|
+
const normalizedPath = uploadPath.replace(/\\/g, '/');
|
|
99
|
+
|
|
83
100
|
const { data, error } = await this.client.storage
|
|
84
101
|
.from(this.bucket)
|
|
85
|
-
.upload(
|
|
102
|
+
.upload(normalizedPath, content, {
|
|
86
103
|
upsert: true,
|
|
87
104
|
contentType,
|
|
88
105
|
});
|
|
89
106
|
|
|
90
107
|
if (error) {
|
|
108
|
+
console.error(
|
|
109
|
+
`ā Supabase upload failed for ${normalizedPath}:`,
|
|
110
|
+
error.message,
|
|
111
|
+
);
|
|
91
112
|
return { success: false, error: error.message };
|
|
92
113
|
}
|
|
93
114
|
|
|
94
|
-
|
|
115
|
+
console.log(`ā
Supabase upload successful: ${normalizedPath}`);
|
|
116
|
+
return { success: true, data, uploadPath: normalizedPath };
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
/**
|
|
@@ -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,47 @@ 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 { excludePattern = /(^|[\/\\])\.|node_modules|\.git/, onlyPdf = false } = options;
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(dirPath)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
162
|
+
const files = [];
|
|
163
|
+
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
166
|
+
|
|
167
|
+
// Skip excluded patterns
|
|
168
|
+
if (excludePattern && excludePattern.test(fullPath)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Only include files, not directories
|
|
173
|
+
if (entry.isFile()) {
|
|
174
|
+
// If onlyPdf is true, filter for PDF files
|
|
175
|
+
if (onlyPdf && !entry.name.toLowerCase().endsWith('.pdf')) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
files.push(fullPath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return files;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
146
187
|
}
|
|
147
188
|
|
|
148
189
|
export default FileOperations;
|