molex-ftp-client 1.0.0 → 1.2.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.
@@ -0,0 +1,226 @@
1
+ const { EventEmitter } = require('events');
2
+ const FTPConnection = require('./connection');
3
+ const FTPCommands = require('./commands');
4
+
5
+ /**
6
+ * Lightweight FTP Client using native Node.js TCP sockets (net module)
7
+ */
8
+ class FTPClient extends EventEmitter {
9
+ constructor(options = {}) {
10
+ super();
11
+
12
+ // Connection state
13
+ this.socket = null;
14
+ this.dataSocket = null;
15
+ this.buffer = '';
16
+ this.connected = false;
17
+ this.authenticated = false;
18
+
19
+ // Configuration
20
+ this.debug = options.debug || false;
21
+ this.timeout = options.timeout || 30000;
22
+ this.keepAlive = options.keepAlive !== false;
23
+ this._log = options.logger || console.log;
24
+
25
+ // Statistics
26
+ this._commandCount = 0;
27
+ this._lastCommand = null;
28
+
29
+ // Initialize subsystems
30
+ this._connection = new FTPConnection(this);
31
+ this._commands = new FTPCommands(this);
32
+ }
33
+
34
+ /**
35
+ * Log message if debug is enabled
36
+ * @private
37
+ */
38
+ _debug(...args) {
39
+ if (this.debug && this._log) {
40
+ this._log('[FTP Debug]', ...args);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Connect to FTP server
46
+ * @param {Object} options - Connection options
47
+ * @param {string} options.host - FTP server host
48
+ * @param {number} [options.port=21] - FTP server port
49
+ * @param {string} [options.user='anonymous'] - Username
50
+ * @param {string} [options.password='anonymous@'] - Password
51
+ * @returns {Promise<void>}
52
+ */
53
+ async connect(options) {
54
+ return this._connection.connect(options);
55
+ }
56
+
57
+ /**
58
+ * Upload file to FTP server
59
+ * @param {string|Buffer} data - File data
60
+ * @param {string} remotePath - Remote file path
61
+ * @returns {Promise<void>}
62
+ */
63
+ async upload(data, remotePath) {
64
+ return this._commands.upload(data, remotePath);
65
+ }
66
+
67
+ /**
68
+ * Download file from FTP server
69
+ * @param {string} remotePath - Remote file path
70
+ * @returns {Promise<Buffer>}
71
+ */
72
+ async download(remotePath) {
73
+ return this._commands.download(remotePath);
74
+ }
75
+
76
+ /**
77
+ * List directory contents
78
+ * @param {string} [path='.'] - Directory path
79
+ * @returns {Promise<string>}
80
+ */
81
+ async list(path = '.') {
82
+ return this._commands.list(path);
83
+ }
84
+
85
+ /**
86
+ * Change working directory
87
+ * @param {string} path - Directory path
88
+ * @returns {Promise<void>}
89
+ */
90
+ async cd(path) {
91
+ return this._commands.cd(path);
92
+ }
93
+
94
+ /**
95
+ * Get current working directory
96
+ * @returns {Promise<string>}
97
+ */
98
+ async pwd() {
99
+ return this._commands.pwd();
100
+ }
101
+
102
+ /**
103
+ * Create directory
104
+ * @param {string} path - Directory path
105
+ * @returns {Promise<void>}
106
+ */
107
+ async mkdir(path) {
108
+ return this._commands.mkdir(path);
109
+ }
110
+
111
+ /**
112
+ * Delete file
113
+ * @param {string} path - File path
114
+ * @returns {Promise<void>}
115
+ */
116
+ async delete(path) {
117
+ return this._commands.delete(path);
118
+ }
119
+
120
+ /**
121
+ * Rename file
122
+ * @param {string} from - Current name
123
+ * @param {string} to - New name
124
+ * @returns {Promise<void>}
125
+ */
126
+ async rename(from, to) {
127
+ return this._commands.rename(from, to);
128
+ }
129
+
130
+ /**
131
+ * Get file size
132
+ * @param {string} path - File path
133
+ * @returns {Promise<number>}
134
+ */
135
+ async size(path) {
136
+ return this._commands.size(path);
137
+ }
138
+
139
+ /**
140
+ * Check if file or directory exists
141
+ * @param {string} path - File or directory path
142
+ * @returns {Promise<boolean>}
143
+ */
144
+ async exists(path) {
145
+ return this._commands.exists(path);
146
+ }
147
+
148
+ /**
149
+ * Ensure directory exists, creating it if necessary
150
+ * @param {string} dirPath - Directory path to ensure exists
151
+ * @param {boolean} recursive - Create parent directories if needed (default: true)
152
+ * @returns {Promise<void>}
153
+ */
154
+ async ensureDir(dirPath, recursive = true) {
155
+ return this._commands.ensureDir(dirPath, recursive);
156
+ }
157
+
158
+ /**
159
+ * Ensure parent directory exists for a file path
160
+ * @param {string} filePath - File path
161
+ * @returns {Promise<void>}
162
+ */
163
+ async ensureParentDir(filePath) {
164
+ return this._commands.ensureParentDir(filePath);
165
+ }
166
+
167
+ /**
168
+ * Upload file and ensure parent directory exists
169
+ * @param {string|Buffer} data - File data
170
+ * @param {string} remotePath - Remote file path
171
+ * @param {boolean} ensureDir - Ensure parent directory exists (default: false)
172
+ * @returns {Promise<void>}
173
+ */
174
+ async uploadFile(data, remotePath, ensureDir = false) {
175
+ return this._commands.uploadFile(data, remotePath, ensureDir);
176
+ }
177
+
178
+ /**
179
+ * Get file modification time
180
+ * @param {string} path - File path
181
+ * @returns {Promise<Date>}
182
+ */
183
+ async modifiedTime(path) {
184
+ return this._commands.modifiedTime(path);
185
+ }
186
+
187
+ /**
188
+ * Get connection statistics
189
+ * @returns {Object}
190
+ */
191
+ getStats() {
192
+ return {
193
+ connected: this.connected,
194
+ authenticated: this.authenticated,
195
+ commandCount: this._commandCount,
196
+ lastCommand: this._lastCommand
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Enable or disable debug mode
202
+ * @param {boolean} enabled - Enable debug mode
203
+ */
204
+ setDebug(enabled) {
205
+ this.debug = enabled;
206
+ this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
207
+ }
208
+
209
+ /**
210
+ * Close connection
211
+ * @returns {Promise<void>}
212
+ */
213
+ async close() {
214
+ return this._connection.close();
215
+ }
216
+
217
+ /**
218
+ * Disconnect (alias for close)
219
+ * @returns {Promise<void>}
220
+ */
221
+ async disconnect() {
222
+ return this.close();
223
+ }
224
+ }
225
+
226
+ module.exports = FTPClient;
@@ -0,0 +1,323 @@
1
+ const net = require('net');
2
+ const { normalizePath, getParentDir, parseMdtmResponse } = require('./utils');
3
+
4
+ /**
5
+ * FTP command implementations
6
+ */
7
+ class FTPCommands {
8
+ constructor(client) {
9
+ this.client = client;
10
+ this.connection = client._connection;
11
+ }
12
+
13
+ /**
14
+ * Upload file to FTP server
15
+ * @param {string|Buffer} data - File data
16
+ * @param {string} remotePath - Remote file path
17
+ * @returns {Promise<void>}
18
+ */
19
+ async upload(data, remotePath) {
20
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
21
+ this.client._debug(`Uploading ${buffer.length} bytes to ${remotePath}`);
22
+ const { host, port } = await this.connection.enterPassiveMode();
23
+
24
+ return new Promise((resolve, reject) => {
25
+ let commandSent = false;
26
+
27
+ this.client.dataSocket = net.createConnection({ host, port }, () => {
28
+ // Send STOR command to start upload (expects 150, then 226)
29
+ if (!commandSent) {
30
+ commandSent = true;
31
+ this.client._debug(`Data connection established for upload`);
32
+ this.connection.sendCommand(`STOR ${remotePath}`, true).catch(reject);
33
+
34
+ // Write data to data socket
35
+ this.client.dataSocket.write(buffer);
36
+ this.client.dataSocket.end();
37
+ }
38
+ });
39
+
40
+ this.client.dataSocket.on('error', reject);
41
+
42
+ this.client.dataSocket.on('close', () => {
43
+ // Wait for final response from control socket
44
+ const finalHandler = (line) => {
45
+ const code = parseInt(line.substring(0, 3));
46
+ if (code === 226 || code === 250) {
47
+ this.client.removeListener('response', finalHandler);
48
+ this.client._debug(`Upload completed successfully`);
49
+ resolve();
50
+ } else if (code >= 400) {
51
+ this.client.removeListener('response', finalHandler);
52
+ reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
53
+ }
54
+ };
55
+ this.client.on('response', finalHandler);
56
+
57
+ // Timeout if no response
58
+ setTimeout(() => {
59
+ this.client.removeListener('response', finalHandler);
60
+ resolve();
61
+ }, 5000);
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Download file from FTP server
68
+ * @param {string} remotePath - Remote file path
69
+ * @returns {Promise<Buffer>}
70
+ */
71
+ async download(remotePath) {
72
+ this.client._debug(`Downloading ${remotePath}`);
73
+ const { host, port } = await this.connection.enterPassiveMode();
74
+
75
+ return new Promise((resolve, reject) => {
76
+ const chunks = [];
77
+ let commandSent = false;
78
+
79
+ this.client.dataSocket = net.createConnection({ host, port }, () => {
80
+ // Send RETR command to start download (expects 150, then 226)
81
+ if (!commandSent) {
82
+ commandSent = true;
83
+ this.client._debug(`Data connection established for download`);
84
+ this.connection.sendCommand(`RETR ${remotePath}`, true).catch(reject);
85
+ }
86
+ });
87
+
88
+ this.client.dataSocket.on('data', (chunk) => {
89
+ chunks.push(chunk);
90
+ this.client._debug(`Received ${chunk.length} bytes`);
91
+ });
92
+
93
+ this.client.dataSocket.on('error', reject);
94
+
95
+ this.client.dataSocket.on('close', () => {
96
+ // Wait for final 226 response
97
+ const finalHandler = (line) => {
98
+ const code = parseInt(line.substring(0, 3));
99
+ if (code === 226 || code === 250) {
100
+ this.client.removeListener('response', finalHandler);
101
+ const result = Buffer.concat(chunks);
102
+ this.client._debug(`Download completed: ${result.length} bytes`);
103
+ resolve(result);
104
+ } else if (code >= 400) {
105
+ this.client.removeListener('response', finalHandler);
106
+ reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
107
+ }
108
+ };
109
+ this.client.on('response', finalHandler);
110
+
111
+ // Timeout if no response
112
+ setTimeout(() => {
113
+ this.client.removeListener('response', finalHandler);
114
+ if (chunks.length > 0) {
115
+ resolve(Buffer.concat(chunks));
116
+ }
117
+ }, 5000);
118
+ });
119
+ });
120
+ }
121
+
122
+ /**
123
+ * List directory contents
124
+ * @param {string} [path='.'] - Directory path
125
+ * @returns {Promise<string>}
126
+ */
127
+ async list(path = '.') {
128
+ this.client._debug(`Listing directory: ${path}`);
129
+ const { host, port } = await this.connection.enterPassiveMode();
130
+
131
+ return new Promise((resolve, reject) => {
132
+ const chunks = [];
133
+ let commandSent = false;
134
+
135
+ this.client.dataSocket = net.createConnection({ host, port }, () => {
136
+ if (!commandSent) {
137
+ commandSent = true;
138
+ this.connection.sendCommand(`LIST ${path}`, true).catch(reject);
139
+ }
140
+ });
141
+
142
+ this.client.dataSocket.on('data', (chunk) => {
143
+ chunks.push(chunk);
144
+ });
145
+
146
+ this.client.dataSocket.on('error', reject);
147
+
148
+ this.client.dataSocket.on('close', () => {
149
+ // Wait for final 226 response
150
+ const finalHandler = (line) => {
151
+ const code = parseInt(line.substring(0, 3));
152
+ if (code === 226 || code === 250) {
153
+ this.client.removeListener('response', finalHandler);
154
+ resolve(Buffer.concat(chunks).toString('utf8'));
155
+ }
156
+ };
157
+ this.client.on('response', finalHandler);
158
+
159
+ // Timeout fallback
160
+ setTimeout(() => {
161
+ this.client.removeListener('response', finalHandler);
162
+ resolve(Buffer.concat(chunks).toString('utf8'));
163
+ }, 3000);
164
+ });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Change working directory
170
+ * @param {string} path - Directory path
171
+ * @returns {Promise<void>}
172
+ */
173
+ async cd(path) {
174
+ await this.connection.sendCommand(`CWD ${path}`);
175
+ }
176
+
177
+ /**
178
+ * Get current working directory
179
+ * @returns {Promise<string>}
180
+ */
181
+ async pwd() {
182
+ const response = await this.connection.sendCommand('PWD');
183
+ const match = response.message.match(/"(.+)"/);
184
+ return match ? match[1] : '/';
185
+ }
186
+
187
+ /**
188
+ * Create directory
189
+ * @param {string} path - Directory path
190
+ * @returns {Promise<void>}
191
+ */
192
+ async mkdir(path) {
193
+ await this.connection.sendCommand(`MKD ${path}`);
194
+ }
195
+
196
+ /**
197
+ * Delete file
198
+ * @param {string} path - File path
199
+ * @returns {Promise<void>}
200
+ */
201
+ async delete(path) {
202
+ await this.connection.sendCommand(`DELE ${path}`);
203
+ }
204
+
205
+ /**
206
+ * Rename file
207
+ * @param {string} from - Current name
208
+ * @param {string} to - New name
209
+ * @returns {Promise<void>}
210
+ */
211
+ async rename(from, to) {
212
+ await this.connection.sendCommand(`RNFR ${from}`);
213
+ await this.connection.sendCommand(`RNTO ${to}`);
214
+ }
215
+
216
+ /**
217
+ * Get file size
218
+ * @param {string} path - File path
219
+ * @returns {Promise<number>}
220
+ */
221
+ async size(path) {
222
+ this.client._debug(`Getting size of ${path}`)
223
+ const response = await this.connection.sendCommand(`SIZE ${path}`);
224
+ return parseInt(response.message);
225
+ }
226
+
227
+ /**
228
+ * Check if file or directory exists
229
+ * @param {string} path - File or directory path
230
+ * @returns {Promise<boolean>}
231
+ */
232
+ async exists(path) {
233
+ try {
234
+ await this.size(path);
235
+ return true;
236
+ } catch (err) {
237
+ return false;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Ensure directory exists, creating it if necessary
243
+ * @param {string} dirPath - Directory path to ensure exists
244
+ * @param {boolean} recursive - Create parent directories if needed (default: true)
245
+ * @returns {Promise<void>}
246
+ */
247
+ async ensureDir(dirPath, recursive = true) {
248
+ this.client._debug(`Ensuring directory exists: ${dirPath}`);
249
+
250
+ // Normalize path
251
+ const normalized = normalizePath(dirPath);
252
+ if (normalized === '/' || normalized === '.') {
253
+ return; // Root or current directory always exists
254
+ }
255
+
256
+ // Try to cd to the directory
257
+ try {
258
+ await this.cd(normalized);
259
+ this.client._debug(`Directory already exists: ${normalized}`);
260
+ return;
261
+ } catch (err) {
262
+ this.client._debug(`Directory doesn't exist: ${normalized}`);
263
+ }
264
+
265
+ // If recursive, ensure parent directory exists first
266
+ if (recursive) {
267
+ const parentDir = normalized.substring(0, normalized.lastIndexOf('/')) || '/';
268
+ if (parentDir !== '/' && parentDir !== '.') {
269
+ await this.ensureDir(parentDir, true);
270
+ }
271
+ }
272
+
273
+ // Create the directory
274
+ try {
275
+ await this.mkdir(normalized);
276
+ this.client._debug(`Created directory: ${normalized}`);
277
+ } catch (err) {
278
+ // Ignore error if directory was created by another process
279
+ if (!err.message.includes('550') && !err.message.includes('exists')) {
280
+ throw err;
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Ensure parent directory exists for a file path
287
+ * @param {string} filePath - File path
288
+ * @returns {Promise<void>}
289
+ */
290
+ async ensureParentDir(filePath) {
291
+ const parentDir = getParentDir(filePath);
292
+ if (parentDir && parentDir !== '.' && parentDir !== '/') {
293
+ await this.ensureDir(parentDir);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Upload file and ensure parent directory exists
299
+ * @param {string|Buffer} data - File data
300
+ * @param {string} remotePath - Remote file path
301
+ * @param {boolean} ensureDir - Ensure parent directory exists (default: false)
302
+ * @returns {Promise<void>}
303
+ */
304
+ async uploadFile(data, remotePath, ensureDir = false) {
305
+ if (ensureDir) {
306
+ await this.ensureParentDir(remotePath);
307
+ }
308
+ return this.upload(data, remotePath);
309
+ }
310
+
311
+ /**
312
+ * Get file modification time
313
+ * @param {string} path - File path
314
+ * @returns {Promise<Date>}
315
+ */
316
+ async modifiedTime(path) {
317
+ this.client._debug(`Getting modification time of ${path}`);
318
+ const response = await this.connection.sendCommand(`MDTM ${path}`);
319
+ return parseMdtmResponse(response.message);
320
+ }
321
+ }
322
+
323
+ module.exports = FTPCommands;