@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.
@@ -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;