@arela/uploader 0.2.4 → 0.2.6

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.
@@ -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,153 @@
1
+ import { Blob } from 'buffer';
2
+ import { FormData } from 'formdata-node';
3
+ import fs from 'fs';
4
+ import fetch from 'node-fetch';
5
+ import path from 'path';
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 (
133
+ options.folderStructure &&
134
+ options.autoDetectStructure &&
135
+ files.length > 0
136
+ ) {
137
+ // This would require the path detection utility - we'll implement this after creating that module
138
+ console.log(`📁 Custom folder structure: ${options.folderStructure}`);
139
+ return options.folderStructure;
140
+ } else if (options.folderStructure) {
141
+ console.log(
142
+ `📁 Using custom folder structure: ${options.folderStructure}`,
143
+ );
144
+ return options.folderStructure;
145
+ } else if (options.autoDetectStructure && files.length > 0) {
146
+ // Auto-detection logic would go here
147
+ console.log('📁 Auto-detection requested but not yet implemented');
148
+ return null;
149
+ }
150
+
151
+ return null;
152
+ }
153
+ }
@@ -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,126 @@
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
+ db: {
34
+ schema: 'public',
35
+ },
36
+ auth: {
37
+ autoRefreshToken: false,
38
+ persistSession: false,
39
+ },
40
+ global: {
41
+ headers: {
42
+ 'x-client-info': 'arela-uploader',
43
+ },
44
+ },
45
+ // Increase timeout settings to handle large queries
46
+ realtime: {
47
+ timeout: 60000, // 60 seconds
48
+ },
49
+ });
50
+
51
+ // Test connection
52
+ const { error } = await this.client.storage.from(this.bucket).list('');
53
+ if (error) {
54
+ throw new Error(`Error connecting to Supabase: ${error.message}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Upload file directly to Supabase
60
+ * @param {Array} files - Array with single file object
61
+ * @param {Object} options - Upload options with uploadPath
62
+ * @returns {Promise<Object>} Upload result
63
+ */
64
+ async upload(files, options) {
65
+ await this.#initializeClient();
66
+
67
+ if (files.length !== 1) {
68
+ throw new Error(
69
+ 'SupabaseUploadService only supports single file uploads',
70
+ );
71
+ }
72
+
73
+ const file = files[0];
74
+ const uploadPath = options.uploadPath;
75
+
76
+ if (!uploadPath) {
77
+ throw new Error('uploadPath is required for Supabase uploads');
78
+ }
79
+
80
+ const content = fs.readFileSync(file.path);
81
+ const contentType = mime.lookup(file.path) || 'application/octet-stream';
82
+
83
+ const { data, error } = await this.client.storage
84
+ .from(this.bucket)
85
+ .upload(uploadPath.replace(/\\/g, '/'), content, {
86
+ upsert: true,
87
+ contentType,
88
+ });
89
+
90
+ if (error) {
91
+ throw new Error(error.message);
92
+ }
93
+
94
+ return data;
95
+ }
96
+
97
+ /**
98
+ * Check if Supabase service is available
99
+ * @returns {Promise<boolean>} True if available
100
+ */
101
+ async isAvailable() {
102
+ try {
103
+ await this.#initializeClient();
104
+ return true;
105
+ } catch (error) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get service name
112
+ * @returns {string} Service name
113
+ */
114
+ getServiceName() {
115
+ return 'Supabase';
116
+ }
117
+
118
+ /**
119
+ * Get Supabase client for database operations
120
+ * @returns {Object} Supabase client
121
+ */
122
+ async getClient() {
123
+ await this.#initializeClient();
124
+ return this.client;
125
+ }
126
+ }
@@ -0,0 +1,76 @@
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(
26
+ `✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`,
27
+ );
28
+ return this.supabaseService;
29
+ } else {
30
+ throw new Error('Supabase service is not available');
31
+ }
32
+ }
33
+
34
+ // Try API service first
35
+ if (await this.apiService.isAvailable()) {
36
+ console.log(
37
+ '🌐 API mode enabled - files will be uploaded to Arela API with automatic processing',
38
+ );
39
+ console.log(`✅ Connected to ${this.apiService.getServiceName()}`);
40
+ return this.apiService;
41
+ }
42
+
43
+ // Fallback to Supabase
44
+ if (await this.supabaseService.isAvailable()) {
45
+ console.warn(
46
+ '⚠️ API connection failed, falling back to direct Supabase upload',
47
+ );
48
+ console.log(
49
+ `✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`,
50
+ );
51
+ return this.supabaseService;
52
+ }
53
+
54
+ throw new Error('No upload service is available');
55
+ }
56
+
57
+ /**
58
+ * Check if API mode is currently active
59
+ * @returns {Promise<boolean>} True if API service is available
60
+ */
61
+ async isApiModeAvailable() {
62
+ return await this.apiService.isAvailable();
63
+ }
64
+
65
+ /**
66
+ * Get Supabase service for database operations
67
+ * @returns {SupabaseUploadService} Supabase service instance
68
+ */
69
+ getSupabaseService() {
70
+ return this.supabaseService;
71
+ }
72
+ }
73
+
74
+ // Export singleton instance
75
+ export const uploadServiceFactory = new UploadServiceFactory();
76
+ export default uploadServiceFactory;