@arela/uploader 0.2.4 → 0.2.5
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/package.json +2 -1
- package/src/commands/UploadCommand.js +388 -0
- package/src/config/config.js +173 -0
- package/src/errors/ErrorHandler.js +271 -0
- package/src/errors/ErrorTypes.js +104 -0
- package/src/index-old.js +2650 -0
- package/src/index.js +248 -2594
- package/src/services/DatabaseService.js +953 -0
- package/src/services/LoggingService.js +194 -0
- package/src/services/upload/ApiUploadService.js +147 -0
- package/src/services/upload/BaseUploadService.js +36 -0
- package/src/services/upload/SupabaseUploadService.js +107 -0
- package/src/services/upload/UploadServiceFactory.js +68 -0
- package/src/utils/FileOperations.js +148 -0
- package/src/utils/FileSanitizer.js +99 -0
- package/src/utils/PathDetector.js +196 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import appConfig from '../config/config.js';
|
|
5
|
+
import FileOperations from '../utils/FileOperations.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logging Service
|
|
9
|
+
* Handles application logging with buffering, file output, and console output
|
|
10
|
+
*/
|
|
11
|
+
export class LoggingService {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.logFilePath = appConfig.logging.logFilePath;
|
|
14
|
+
this.isVerbose = appConfig.logging.verbose;
|
|
15
|
+
this.buffer = [];
|
|
16
|
+
this.bufferSize = appConfig.performance.logBufferSize;
|
|
17
|
+
this.flushInterval = appConfig.performance.logFlushInterval;
|
|
18
|
+
this.lastFlushTime = Date.now();
|
|
19
|
+
|
|
20
|
+
this.#setupProcessHandlers();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Setup process handlers for graceful shutdown
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
#setupProcessHandlers() {
|
|
28
|
+
const flushAndExit = (exitCode = 0) => {
|
|
29
|
+
this.flush();
|
|
30
|
+
process.exit(exitCode);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
process.on('exit', () => this.flush());
|
|
34
|
+
process.on('SIGINT', () => flushAndExit(0));
|
|
35
|
+
process.on('SIGTERM', () => flushAndExit(0));
|
|
36
|
+
process.on('uncaughtException', (error) => {
|
|
37
|
+
this.error(`Uncaught Exception: ${error.message}`);
|
|
38
|
+
flushAndExit(1);
|
|
39
|
+
});
|
|
40
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
41
|
+
this.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
|
|
42
|
+
flushAndExit(1);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write a log message
|
|
48
|
+
* @param {string} message - Log message
|
|
49
|
+
* @param {string} level - Log level (info, warn, error, debug)
|
|
50
|
+
*/
|
|
51
|
+
writeLog(message, level = 'info') {
|
|
52
|
+
try {
|
|
53
|
+
const timestamp = new Date().toISOString();
|
|
54
|
+
const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
55
|
+
|
|
56
|
+
this.buffer.push(logEntry);
|
|
57
|
+
|
|
58
|
+
// Flush if buffer is full or enough time has passed
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (
|
|
61
|
+
this.buffer.length >= this.bufferSize ||
|
|
62
|
+
now - this.lastFlushTime >= this.flushInterval
|
|
63
|
+
) {
|
|
64
|
+
this.flush();
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`❌ Error buffering log message: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Log info message
|
|
73
|
+
* @param {string} message - Message to log
|
|
74
|
+
*/
|
|
75
|
+
info(message) {
|
|
76
|
+
this.writeLog(message, 'info');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log warning message
|
|
81
|
+
* @param {string} message - Message to log
|
|
82
|
+
*/
|
|
83
|
+
warn(message) {
|
|
84
|
+
this.writeLog(message, 'warn');
|
|
85
|
+
if (this.isVerbose) {
|
|
86
|
+
console.warn(`⚠️ ${message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log error message
|
|
92
|
+
* @param {string} message - Message to log
|
|
93
|
+
*/
|
|
94
|
+
error(message) {
|
|
95
|
+
this.writeLog(message, 'error');
|
|
96
|
+
console.error(`❌ ${message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Log debug message (only if verbose mode is enabled)
|
|
101
|
+
* @param {string} message - Message to log
|
|
102
|
+
*/
|
|
103
|
+
debug(message) {
|
|
104
|
+
if (this.isVerbose) {
|
|
105
|
+
this.writeLog(message, 'debug');
|
|
106
|
+
console.log(`🔍 ${message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Log verbose message (only if verbose mode is enabled)
|
|
112
|
+
* @param {string} message - Message to log
|
|
113
|
+
*/
|
|
114
|
+
verbose(message) {
|
|
115
|
+
if (this.isVerbose) {
|
|
116
|
+
this.writeLog(message, 'verbose');
|
|
117
|
+
console.log(message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Log success message
|
|
123
|
+
* @param {string} message - Message to log
|
|
124
|
+
*/
|
|
125
|
+
success(message) {
|
|
126
|
+
this.writeLog(message, 'success');
|
|
127
|
+
console.log(`✅ ${message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Flush log buffer to file
|
|
132
|
+
*/
|
|
133
|
+
flush() {
|
|
134
|
+
if (this.buffer.length === 0) return;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const logContent = this.buffer.join('\n') + '\n';
|
|
138
|
+
FileOperations.appendToFile(this.logFilePath, logContent);
|
|
139
|
+
this.buffer = [];
|
|
140
|
+
this.lastFlushTime = Date.now();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`❌ Error writing to log file: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear the log file
|
|
148
|
+
*/
|
|
149
|
+
clearLogFile() {
|
|
150
|
+
try {
|
|
151
|
+
if (FileOperations.fileExists(this.logFilePath)) {
|
|
152
|
+
fs.unlinkSync(this.logFilePath);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`❌ Error clearing log file: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get log file path
|
|
161
|
+
* @returns {string} Path to log file
|
|
162
|
+
*/
|
|
163
|
+
getLogFilePath() {
|
|
164
|
+
return this.logFilePath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if verbose logging is enabled
|
|
169
|
+
* @returns {boolean} True if verbose logging is enabled
|
|
170
|
+
*/
|
|
171
|
+
isVerboseEnabled() {
|
|
172
|
+
return this.isVerbose;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set verbose mode
|
|
177
|
+
* @param {boolean} verbose - Enable/disable verbose mode
|
|
178
|
+
*/
|
|
179
|
+
setVerbose(verbose) {
|
|
180
|
+
this.isVerbose = verbose;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get current buffer size
|
|
185
|
+
* @returns {number} Number of buffered log entries
|
|
186
|
+
*/
|
|
187
|
+
getBufferSize() {
|
|
188
|
+
return this.buffer.length;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Export singleton instance
|
|
193
|
+
export const logger = new LoggingService();
|
|
194
|
+
export default logger;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { FormData } from 'formdata-node';
|
|
5
|
+
import { Blob } from 'buffer';
|
|
6
|
+
|
|
7
|
+
import appConfig from '../../config/config.js';
|
|
8
|
+
import { BaseUploadService } from './BaseUploadService.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* API Upload Service
|
|
12
|
+
* Handles uploads to the Arela API with automatic processing
|
|
13
|
+
*/
|
|
14
|
+
export class ApiUploadService extends BaseUploadService {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.baseUrl = appConfig.api.baseUrl;
|
|
18
|
+
this.token = appConfig.api.token;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Upload files to Arela API with automatic detection and organization
|
|
23
|
+
* @param {Array} files - Array of file objects
|
|
24
|
+
* @param {Object} options - Upload options
|
|
25
|
+
* @returns {Promise<Object>} API response
|
|
26
|
+
*/
|
|
27
|
+
async upload(files, options) {
|
|
28
|
+
const formData = new FormData();
|
|
29
|
+
|
|
30
|
+
// Add files to form data
|
|
31
|
+
files.forEach((file) => {
|
|
32
|
+
const fileBuffer = fs.readFileSync(file.path);
|
|
33
|
+
const blob = new Blob([fileBuffer], { type: file.contentType });
|
|
34
|
+
formData.append('files', blob, file.name);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Add configuration parameters
|
|
38
|
+
if (appConfig.supabase.bucket) {
|
|
39
|
+
formData.append('bucket', appConfig.supabase.bucket);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.prefix) {
|
|
43
|
+
formData.append('prefix', options.prefix);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle folder structure configuration
|
|
47
|
+
const folderStructure = this.#buildFolderStructure(files, options);
|
|
48
|
+
if (folderStructure) {
|
|
49
|
+
formData.append('folderStructure', folderStructure);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add client path if specified
|
|
53
|
+
if (options.clientPath) {
|
|
54
|
+
formData.append('clientPath', options.clientPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add processing options
|
|
58
|
+
formData.append('autoDetect', String(options.autoDetect ?? true));
|
|
59
|
+
formData.append('autoOrganize', String(options.autoOrganize ?? false));
|
|
60
|
+
formData.append('batchSize', String(options.batchSize || 10));
|
|
61
|
+
formData.append('clientVersion', appConfig.packageVersion);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(
|
|
65
|
+
`${this.baseUrl}/api/storage/batch-upload-and-process`,
|
|
66
|
+
{
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'x-api-key': this.token,
|
|
70
|
+
},
|
|
71
|
+
body: formData,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorText = await response.text();
|
|
77
|
+
throw new Error(
|
|
78
|
+
`API request failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await response.json();
|
|
83
|
+
|
|
84
|
+
// Normalize response format to match DatabaseService expectations
|
|
85
|
+
return { success: true, data: result };
|
|
86
|
+
} catch (fetchError) {
|
|
87
|
+
// Return normalized error format
|
|
88
|
+
return { success: false, error: fetchError.message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if API service is available
|
|
94
|
+
* @returns {Promise<boolean>} True if available
|
|
95
|
+
*/
|
|
96
|
+
async isAvailable() {
|
|
97
|
+
if (!this.baseUrl || !this.token) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`${this.baseUrl}/api/health`, {
|
|
103
|
+
headers: {
|
|
104
|
+
'x-api-key': this.token,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return response.ok;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get service name
|
|
116
|
+
* @returns {string} Service name
|
|
117
|
+
*/
|
|
118
|
+
getServiceName() {
|
|
119
|
+
return 'Arela API';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build folder structure from options and auto-detection
|
|
124
|
+
* @private
|
|
125
|
+
* @param {Array} files - Files being uploaded
|
|
126
|
+
* @param {Object} options - Upload options
|
|
127
|
+
* @returns {string|null} Folder structure or null
|
|
128
|
+
*/
|
|
129
|
+
#buildFolderStructure(files, options) {
|
|
130
|
+
let combinedStructure = null;
|
|
131
|
+
|
|
132
|
+
if (options.folderStructure && options.autoDetectStructure && files.length > 0) {
|
|
133
|
+
// This would require the path detection utility - we'll implement this after creating that module
|
|
134
|
+
console.log(`📁 Custom folder structure: ${options.folderStructure}`);
|
|
135
|
+
return options.folderStructure;
|
|
136
|
+
} else if (options.folderStructure) {
|
|
137
|
+
console.log(`📁 Using custom folder structure: ${options.folderStructure}`);
|
|
138
|
+
return options.folderStructure;
|
|
139
|
+
} else if (options.autoDetectStructure && files.length > 0) {
|
|
140
|
+
// Auto-detection logic would go here
|
|
141
|
+
console.log('📁 Auto-detection requested but not yet implemented');
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base upload service interface
|
|
7
|
+
* Defines the contract for upload implementations
|
|
8
|
+
*/
|
|
9
|
+
export class BaseUploadService {
|
|
10
|
+
/**
|
|
11
|
+
* Upload files using the specific implementation
|
|
12
|
+
* @param {Array} files - Array of file objects with path, name, and contentType
|
|
13
|
+
* @param {Object} options - Upload options
|
|
14
|
+
* @returns {Promise<Object>} Upload result
|
|
15
|
+
* @throws {Error} If upload fails
|
|
16
|
+
*/
|
|
17
|
+
async upload(files, options) {
|
|
18
|
+
throw new Error('upload() method must be implemented by subclass');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if the service is available and properly configured
|
|
23
|
+
* @returns {Promise<boolean>} True if service is available
|
|
24
|
+
*/
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
throw new Error('isAvailable() method must be implemented by subclass');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the service name for logging purposes
|
|
31
|
+
* @returns {string} Service name
|
|
32
|
+
*/
|
|
33
|
+
getServiceName() {
|
|
34
|
+
throw new Error('getServiceName() method must be implemented by subclass');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import mime from 'mime-types';
|
|
4
|
+
|
|
5
|
+
import appConfig from '../../config/config.js';
|
|
6
|
+
import { BaseUploadService } from './BaseUploadService.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supabase Upload Service
|
|
10
|
+
* Handles direct uploads to Supabase Storage
|
|
11
|
+
*/
|
|
12
|
+
export class SupabaseUploadService extends BaseUploadService {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.client = null;
|
|
16
|
+
this.bucket = appConfig.supabase.bucket;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize Supabase client if not already initialized
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
async #initializeClient() {
|
|
24
|
+
if (this.client) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!appConfig.supabase.url || !appConfig.supabase.key || !this.bucket) {
|
|
29
|
+
throw new Error('Missing Supabase configuration');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.client = createClient(appConfig.supabase.url, appConfig.supabase.key);
|
|
33
|
+
|
|
34
|
+
// Test connection
|
|
35
|
+
const { error } = await this.client.storage.from(this.bucket).list('');
|
|
36
|
+
if (error) {
|
|
37
|
+
throw new Error(`Error connecting to Supabase: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Upload file directly to Supabase
|
|
43
|
+
* @param {Array} files - Array with single file object
|
|
44
|
+
* @param {Object} options - Upload options with uploadPath
|
|
45
|
+
* @returns {Promise<Object>} Upload result
|
|
46
|
+
*/
|
|
47
|
+
async upload(files, options) {
|
|
48
|
+
await this.#initializeClient();
|
|
49
|
+
|
|
50
|
+
if (files.length !== 1) {
|
|
51
|
+
throw new Error('SupabaseUploadService only supports single file uploads');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const file = files[0];
|
|
55
|
+
const uploadPath = options.uploadPath;
|
|
56
|
+
|
|
57
|
+
if (!uploadPath) {
|
|
58
|
+
throw new Error('uploadPath is required for Supabase uploads');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = fs.readFileSync(file.path);
|
|
62
|
+
const contentType = mime.lookup(file.path) || 'application/octet-stream';
|
|
63
|
+
|
|
64
|
+
const { data, error } = await this.client.storage
|
|
65
|
+
.from(this.bucket)
|
|
66
|
+
.upload(uploadPath.replace(/\\/g, '/'), content, {
|
|
67
|
+
upsert: true,
|
|
68
|
+
contentType,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
throw new Error(error.message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if Supabase service is available
|
|
80
|
+
* @returns {Promise<boolean>} True if available
|
|
81
|
+
*/
|
|
82
|
+
async isAvailable() {
|
|
83
|
+
try {
|
|
84
|
+
await this.#initializeClient();
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get service name
|
|
93
|
+
* @returns {string} Service name
|
|
94
|
+
*/
|
|
95
|
+
getServiceName() {
|
|
96
|
+
return 'Supabase';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get Supabase client for database operations
|
|
101
|
+
* @returns {Object} Supabase client
|
|
102
|
+
*/
|
|
103
|
+
async getClient() {
|
|
104
|
+
await this.#initializeClient();
|
|
105
|
+
return this.client;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ApiUploadService } from './ApiUploadService.js';
|
|
2
|
+
import { SupabaseUploadService } from './SupabaseUploadService.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Upload Service Factory
|
|
6
|
+
* Manages upload service selection and initialization
|
|
7
|
+
*/
|
|
8
|
+
export class UploadServiceFactory {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.apiService = new ApiUploadService();
|
|
11
|
+
this.supabaseService = new SupabaseUploadService();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the appropriate upload service based on availability and preferences
|
|
16
|
+
* @param {boolean} forceSupabase - Force Supabase mode
|
|
17
|
+
* @returns {Promise<BaseUploadService>} Upload service instance
|
|
18
|
+
* @throws {Error} If no service is available
|
|
19
|
+
*/
|
|
20
|
+
async getUploadService(forceSupabase = false) {
|
|
21
|
+
if (forceSupabase) {
|
|
22
|
+
console.log('🔧 Force Supabase mode enabled - skipping API');
|
|
23
|
+
|
|
24
|
+
if (await this.supabaseService.isAvailable()) {
|
|
25
|
+
console.log(`✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`);
|
|
26
|
+
return this.supabaseService;
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error('Supabase service is not available');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try API service first
|
|
33
|
+
if (await this.apiService.isAvailable()) {
|
|
34
|
+
console.log('🌐 API mode enabled - files will be uploaded to Arela API with automatic processing');
|
|
35
|
+
console.log(`✅ Connected to ${this.apiService.getServiceName()}`);
|
|
36
|
+
return this.apiService;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fallback to Supabase
|
|
40
|
+
if (await this.supabaseService.isAvailable()) {
|
|
41
|
+
console.warn('⚠️ API connection failed, falling back to direct Supabase upload');
|
|
42
|
+
console.log(`✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`);
|
|
43
|
+
return this.supabaseService;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error('No upload service is available');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if API mode is currently active
|
|
51
|
+
* @returns {Promise<boolean>} True if API service is available
|
|
52
|
+
*/
|
|
53
|
+
async isApiModeAvailable() {
|
|
54
|
+
return await this.apiService.isAvailable();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Supabase service for database operations
|
|
59
|
+
* @returns {SupabaseUploadService} Supabase service instance
|
|
60
|
+
*/
|
|
61
|
+
getSupabaseService() {
|
|
62
|
+
return this.supabaseService;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Export singleton instance
|
|
67
|
+
export const uploadServiceFactory = new UploadServiceFactory();
|
|
68
|
+
export default uploadServiceFactory;
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
export default FileOperations;
|