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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## [1.2.0] - 2026-02-02
4
+
5
+ ### Changed
6
+ - **Major refactoring**: Improved separation of concerns
7
+ - `index.js` now serves as simple entry point
8
+ - Implementation moved to organized `lib/` structure:
9
+ - `lib/FTPClient.js` - Main class definition
10
+ - `lib/connection.js` - Connection and authentication logic
11
+ - `lib/commands.js` - All FTP command implementations
12
+ - `lib/utils.js` - Helper functions
13
+ - Better code maintainability and readability
14
+ - No breaking changes - API remains identical
15
+
16
+ ## [1.1.0] - 2026-02-02
17
+
18
+ ### Added
19
+ - `ensureDir(dirPath, recursive)` - Ensure directory exists, creating parent directories if needed
20
+ - `ensureParentDir(filePath)` - Ensure parent directory exists for a file path
21
+ - `uploadFile(data, remotePath, ensureDir)` - Upload with automatic directory creation
22
+ - Utility library (`lib/utils.js`) for better code organization
23
+ - Helper functions for FTP command parsing and path manipulation
24
+
25
+ ### Changed
26
+ - Refactored internal code structure for better maintainability
27
+ - Improved path normalization across all directory operations
28
+ - Better error handling for directory creation
29
+
30
+ ### Improved
31
+ - Cleaner API for common operations
32
+ - Reduced boilerplate code needed for directory handling
33
+ - More consistent error messages
34
+
35
+ ## [1.0.1] - 2026-02-02
36
+
37
+ ### Fixed
38
+ - Updated repository URLs to correct GitHub location
39
+
40
+ ## [1.0.0] - 2026-02-02
41
+
42
+ ### Initial Release
43
+ - Zero dependencies FTP client using native Node.js TCP sockets
44
+ - Promise-based API with async/await support
45
+ - Passive mode (PASV) for data transfers
46
+ - Debug logging with configurable options
47
+ - Connection keep-alive and timeout configuration
48
+ - Upload/download files with Buffer support
49
+ - Directory operations (list, cd, mkdir, pwd)
50
+ - File operations (delete, rename, size, exists, modifiedTime)
51
+ - Connection statistics tracking
52
+ - Event-based architecture
package/README.md CHANGED
@@ -177,6 +177,24 @@ console.log(`Last modified: ${date.toISOString()}`);
177
177
 
178
178
  Returns: `Promise<Date>`
179
179
 
180
+ #### `uploadFile(data, remotePath, ensureDir)`
181
+
182
+ Upload file and optionally ensure parent directory exists.
183
+
184
+ ```javascript
185
+ // Upload with automatic directory creation
186
+ await client.uploadFile('data', '/deep/nested/path/file.txt', true);
187
+
188
+ // Upload without directory creation (default behavior)
189
+ await client.uploadFile('data', '/file.txt');
190
+ ```
191
+
192
+ - `data` (string|Buffer): File content
193
+ - `remotePath` (string): Remote file path
194
+ - `ensureDir` (boolean): Create parent directories if needed (default: false)
195
+
196
+ Returns: `Promise<void>`
197
+
180
198
  ### Directory Operations
181
199
 
182
200
  #### `list(path)`
@@ -221,6 +239,35 @@ await client.mkdir('/remote/newdir');
221
239
 
222
240
  Returns: `Promise<void>`
223
241
 
242
+ #### `ensureDir(dirPath, recursive)`
243
+
244
+ Ensure directory exists, creating it (and parent directories) if necessary.
245
+
246
+ ```javascript
247
+ // Create nested directories recursively
248
+ await client.ensureDir('/deep/nested/path');
249
+
250
+ // Create single directory (no parent creation)
251
+ await client.ensureDir('/newdir', false);
252
+ ```
253
+
254
+ - `dirPath` (string): Directory path to ensure exists
255
+ - `recursive` (boolean): Create parent directories if needed (default: true)
256
+
257
+ Returns: `Promise<void>`
258
+
259
+ #### `ensureParentDir(filePath)`
260
+
261
+ Ensure the parent directory exists for a given file path.
262
+
263
+ ```javascript
264
+ // Ensures /path/to exists before uploading
265
+ await client.ensureParentDir('/path/to/file.txt');
266
+ await client.upload('data', '/path/to/file.txt');
267
+ ```
268
+
269
+ Returns: `Promise<void>`
270
+
224
271
  ### Utilities
225
272
 
226
273
  #### `getStats()`
package/index.js CHANGED
@@ -1,475 +1,17 @@
1
- const net = require('net');
2
- const { EventEmitter } = require('events');
3
-
4
- /**
5
- * Lightweight FTP Client using native Node.js TCP sockets (net module)
1
+ /*
2
+ * File: c:\Users\tonyw\Desktop\PRIVS\molex-ftp-client\index.js
3
+ * Project: c:\Users\tonyw\Desktop\PRIVS\molex-ftp-client
4
+ * Created Date: Monday February 2nd 2026
5
+ * Author: Tony Wiedman
6
+ * -----
7
+ * Last Modified: Mon February 2nd 2026 1:23:44
8
+ * Modified By: Tony Wiedman
9
+ * -----
10
+ * Copyright (c) 2026 MolexWorks
6
11
  */
7
- class FTPClient extends EventEmitter {
8
- constructor(options = {}) {
9
- super();
10
- this.socket = null;
11
- this.dataSocket = null;
12
- this.buffer = '';
13
- this.connected = false;
14
- this.authenticated = false;
15
- this.debug = options.debug || false;
16
- this.timeout = options.timeout || 30000;
17
- this.keepAlive = options.keepAlive !== false;
18
- this._log = options.logger || console.log;
19
- this._commandCount = 0;
20
- this._lastCommand = null;
21
- }
22
-
23
- /**
24
- * Log message if debug is enabled
25
- * @private
26
- */
27
- _debug(...args) {
28
- if (this.debug && this._log) {
29
- this._log('[FTP Debug]', ...args);
30
- }
31
- }
32
-
33
- /**
34
- * Connect to FTP server
35
- * @param {Object} options - Connection options
36
- * @param {string} options.host - FTP server host
37
- * @param {number} [options.port=21] - FTP server port
38
- * @param {string} [options.user='anonymous'] - Username
39
- * @param {string} [options.password='anonymous@'] - Password
40
- * @returns {Promise<void>}
41
- */
42
- async connect({ host, port = 21, user = 'anonymous', password = 'anonymous@' }) {
43
- this._debug(`Connecting to ${host}:${port} as ${user}`);
44
- return new Promise((resolve, reject) => {
45
- this.socket = net.createConnection({ host, port }, () => {
46
- this.connected = true;
47
- this._debug('TCP connection established');
48
- if (this.keepAlive) {
49
- this.socket.setKeepAlive(true, 10000);
50
- }
51
- this.emit('connected');
52
- });
53
-
54
- this.socket.setEncoding('utf8');
55
- this.socket.on('data', async (data) => {
56
- this.buffer += data;
57
- const lines = this.buffer.split('\r\n');
58
- this.buffer = lines.pop();
59
-
60
- for (const line of lines) {
61
- if (line) {
62
- this._debug('<<<', line);
63
- this.emit('response', line);
64
- const code = parseInt(line.substring(0, 3));
65
-
66
- // Handle initial connection
67
- if (code === 220 && !this.authenticated) {
68
- try {
69
- this._debug('Authenticating...');
70
- await this._sendCommand(`USER ${user}`);
71
- await this._sendCommand(`PASS ${password}`);
72
- this.authenticated = true;
73
- this._debug('Authentication successful');
74
- resolve();
75
- } catch (err) {
76
- reject(err);
77
- }
78
- }
79
- }
80
- }
81
- });
82
-
83
- this.socket.on('error', (err) => {
84
- this.emit('error', err);
85
- reject(err);
86
- });
87
-
88
- this.socket.on('close', () => {
89
- this.connected = false;
90
- this.authenticated = false;
91
- this.emit('close');
92
- });
93
-
94
- setTimeout(() => reject(new Error('Connection timeout')), 10000);
95
- });
96
- }
97
-
98
- /**
99
- * Send FTP command and wait for response
100
- * @param {string} command - FTP command
101
- * @param {boolean} allowPreliminary - Allow 1xx preliminary responses
102
- * @returns {Promise<Object>}
103
- */
104
- _sendCommand(command, allowPreliminary = false) {
105
- return new Promise((resolve, reject) => {
106
- if (!this.connected) {
107
- return reject(new Error('Not connected'));
108
- }
109
-
110
- this._commandCount++;
111
- this._lastCommand = command;
112
- const cmdToLog = command.startsWith('PASS ') ? 'PASS ********' : command;
113
- this._debug('>>>', cmdToLog);
114
-
115
- const timeoutId = setTimeout(() => {
116
- this.removeListener('response', responseHandler);
117
- reject(new Error(`Command timeout: ${cmdToLog}`));
118
- }, this.timeout);
119
-
120
- const responseHandler = (line) => {
121
- clearTimeout(timeoutId);
122
- const code = parseInt(line.substring(0, 3));
123
- const message = line.substring(4);
124
-
125
- // Check if this is a complete response (not a multi-line response in progress)
126
- if (line.charAt(3) === ' ') {
127
- // 1xx = Preliminary positive reply (command okay, another command expected)
128
- // 2xx = Positive completion reply
129
- // 3xx = Positive intermediate reply (command okay, awaiting more info)
130
- // 4xx/5xx = Negative replies (errors)
131
-
132
- if (code >= 100 && code < 200 && allowPreliminary) {
133
- // Don't remove listener, wait for final response
134
- this._debug('Preliminary response, waiting for completion...');
135
- return;
136
- }
137
-
138
- clearTimeout(timeoutId);
139
- this.removeListener('response', responseHandler);
140
-
141
- if (code >= 200 && code < 400) {
142
- resolve({ code, message, raw: line });
143
- } else {
144
- this._debug(`Error response: ${code}`);
145
- reject(new Error(`FTP Error ${code}: ${message}`));
146
- }
147
- }
148
- };
149
-
150
- this.on('response', responseHandler);
151
- this.socket.write(command + '\r\n');
152
- });
153
- }
154
-
155
- /**
156
- * Enter passive mode and get data connection info
157
- * @returns {Promise<Object>}
158
- */
159
- async _enterPassiveMode() {
160
- const response = await this._sendCommand('PASV');
161
- const match = response.message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
162
-
163
- if (!match) {
164
- throw new Error('Failed to parse PASV response');
165
- }
166
-
167
- const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
168
- const port = parseInt(match[5]) * 256 + parseInt(match[6]);
169
-
170
- return { host, port };
171
- }
172
-
173
- /**
174
- * Upload file to FTP server
175
- * @param {string|Buffer} data - File data
176
- * @param {string} remotePath - Remote file path
177
- * @returns {Promise<void>}
178
- */
179
- async upload(data, remotePath) {
180
- const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
181
- this._debug(`Uploading ${buffer.length} bytes to ${remotePath}`);
182
- const { host, port } = await this._enterPassiveMode();
183
-
184
- return new Promise((resolve, reject) => {
185
- let commandSent = false;
186
-
187
- this.dataSocket = net.createConnection({ host, port }, () => {
188
- // Send STOR command to start upload (expects 150, then 226)
189
- if (!commandSent) {
190
- commandSent = true;
191
- this._debug(`Data connection established for upload`);
192
- this._sendCommand(`STOR ${remotePath}`, true).catch(reject);
193
-
194
- // Write data to data socket
195
- this.dataSocket.write(buffer);
196
- this.dataSocket.end();
197
- }
198
- });
199
-
200
- this.dataSocket.on('error', reject);
201
-
202
- this.dataSocket.on('close', () => {
203
- // Wait for final response from control socket
204
- const finalHandler = (line) => {
205
- const code = parseInt(line.substring(0, 3));
206
- if (code === 226 || code === 250) {
207
- this.removeListener('response', finalHandler);
208
- this._debug(`Upload completed successfully`);
209
- resolve();
210
- } else if (code >= 400) {
211
- this.removeListener('response', finalHandler);
212
- reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
213
- }
214
- };
215
- this.on('response', finalHandler);
216
-
217
- // Timeout if no response
218
- setTimeout(() => {
219
- this.removeListener('response', finalHandler);
220
- resolve();
221
- }, 5000);
222
- });
223
- });
224
- }
225
-
226
- /**
227
- * Download file from FTP server
228
- * @param {string} remotePath - Remote file path
229
- * @returns {Promise<Buffer>}
230
- */
231
- async download(remotePath) {
232
- this._debug(`Downloading ${remotePath}`);
233
- const { host, port } = await this._enterPassiveMode();
234
-
235
- return new Promise((resolve, reject) => {
236
- const chunks = [];
237
- let commandSent = false;
238
-
239
- this.dataSocket = net.createConnection({ host, port }, () => {
240
- // Send RETR command to start download (expects 150, then 226)
241
- if (!commandSent) {
242
- commandSent = true;
243
- this._debug(`Data connection established for download`);
244
- this._sendCommand(`RETR ${remotePath}`, true).catch(reject);
245
- }
246
- });
247
-
248
- this.dataSocket.on('data', (chunk) => {
249
- chunks.push(chunk);
250
- this._debug(`Received ${chunk.length} bytes`);
251
- });
252
-
253
- this.dataSocket.on('error', reject);
254
-
255
- this.dataSocket.on('close', () => {
256
- // Wait for final 226 response
257
- const finalHandler = (line) => {
258
- const code = parseInt(line.substring(0, 3));
259
- if (code === 226 || code === 250) {
260
- this.removeListener('response', finalHandler);
261
- const result = Buffer.concat(chunks);
262
- this._debug(`Download completed: ${result.length} bytes`);
263
- resolve(result);
264
- } else if (code >= 400) {
265
- this.removeListener('response', finalHandler);
266
- reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
267
- }
268
- };
269
- this.on('response', finalHandler);
270
-
271
- // Timeout if no response
272
- setTimeout(() => {
273
- this.removeListener('response', finalHandler);
274
- if (chunks.length > 0) {
275
- resolve(Buffer.concat(chunks));
276
- }
277
- }, 5000);
278
- });
279
- });
280
- }
281
-
282
- /**
283
- * List directory contents
284
- * @param {string} [path='.'] - Directory path
285
- * @returns {Promise<string>}
286
- */
287
- async list(path = '.') {
288
- this._debug(`Listing directory: ${path}`);
289
- const { host, port } = await this._enterPassiveMode();
290
-
291
- return new Promise((resolve, reject) => {
292
- const chunks = [];
293
- let commandSent = false;
294
-
295
- this.dataSocket = net.createConnection({ host, port }, () => {
296
- if (!commandSent) {
297
- commandSent = true;
298
- this._sendCommand(`LIST ${path}`, true).catch(reject);
299
- }
300
- });
301
-
302
- this.dataSocket.on('data', (chunk) => {
303
- chunks.push(chunk);
304
- });
305
-
306
- this.dataSocket.on('error', reject);
307
-
308
- this.dataSocket.on('close', () => {
309
- // Wait for final 226 response
310
- const finalHandler = (line) => {
311
- const code = parseInt(line.substring(0, 3));
312
- if (code === 226 || code === 250) {
313
- this.removeListener('response', finalHandler);
314
- resolve(Buffer.concat(chunks).toString('utf8'));
315
- }
316
- };
317
- this.on('response', finalHandler);
318
-
319
- // Timeout fallback
320
- setTimeout(() => {
321
- this.removeListener('response', finalHandler);
322
- resolve(Buffer.concat(chunks).toString('utf8'));
323
- }, 3000);
324
- });
325
- });
326
- }
327
-
328
- /**
329
- * Change working directory
330
- * @param {string} path - Directory path
331
- * @returns {Promise<void>}
332
- */
333
- async cd(path) {
334
- await this._sendCommand(`CWD ${path}`);
335
- }
336
-
337
- /**
338
- * Get current working directory
339
- * @returns {Promise<string>}
340
- */
341
- async pwd() {
342
- const response = await this._sendCommand('PWD');
343
- const match = response.message.match(/"(.+)"/);
344
- return match ? match[1] : '/';
345
- }
346
-
347
- /**
348
- * Create directory
349
- * @param {string} path - Directory path
350
- * @returns {Promise<void>}
351
- */
352
- async mkdir(path) {
353
- await this._sendCommand(`MKD ${path}`);
354
- }
355
-
356
- /**
357
- * Delete file
358
- * @param {string} path - File path
359
- * @returns {Promise<void>}
360
- */
361
- async delete(path) {
362
- await this._sendCommand(`DELE ${path}`);
363
- }
364
-
365
- /**
366
- * Rename file
367
- * @param {string} from - Current name
368
- * @param {string} to - New name
369
- * @returns {Promise<void>}
370
- */
371
- async rename(from, to) {
372
- await this._sendCommand(`RNFR ${from}`);
373
- await this._sendCommand(`RNTO ${to}`);
374
- }
375
-
376
- /**
377
- * Get file size
378
- * @param {string} path - File path
379
- * @returns {Promise<number>}
380
- */
381
- async size(path) {
382
- this._debug(`Getting size of ${path}`)
383
- const response = await this._sendCommand(`SIZE ${path}`);
384
- return parseInt(response.message);
385
- }
386
-
387
- /**
388
- * Check if file or directory exists
389
- * @param {string} path - File or directory path
390
- * @returns {Promise<boolean>}
391
- */
392
- async exists(path) {
393
- try {
394
- await this.size(path);
395
- return true;
396
- } catch (err) {
397
- return false;
398
- }
399
- }
400
-
401
- /**
402
- * Get file modification time
403
- * @param {string} path - File path
404
- * @returns {Promise<Date>}
405
- */
406
- async modifiedTime(path) {
407
- this._debug(`Getting modification time of ${path}`);
408
- const response = await this._sendCommand(`MDTM ${path}`);
409
- // Parse MDTM response: YYYYMMDDhhmmss
410
- const match = response.message.match(/(\d{14})/);
411
- if (match) {
412
- const str = match[1];
413
- const year = parseInt(str.substring(0, 4));
414
- const month = parseInt(str.substring(4, 6)) - 1;
415
- const day = parseInt(str.substring(6, 8));
416
- const hour = parseInt(str.substring(8, 10));
417
- const minute = parseInt(str.substring(10, 12));
418
- const second = parseInt(str.substring(12, 14));
419
- return new Date(Date.UTC(year, month, day, hour, minute, second));
420
- }
421
- throw new Error('Failed to parse MDTM response');
422
- }
423
-
424
- /**
425
- * Get connection statistics
426
- * @returns {Object}
427
- */
428
- getStats() {
429
- return {
430
- connected: this.connected,
431
- authenticated: this.authenticated,
432
- commandCount: this._commandCount,
433
- lastCommand: this._lastCommand
434
- };
435
- }
436
-
437
- /**
438
- * Enable or disable debug mode
439
- * @param {boolean} enabled - Enable debug mode
440
- */
441
- setDebug(enabled) {
442
- this.debug = enabled;
443
- this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
444
- }
445
12
 
446
- /**
447
- * Close connection
448
- * @returns {Promise<void>}
449
- */
450
- async close() {
451
- if (this.connected) {
452
- this._debug('Closing connection...');
453
- try {
454
- await this._sendCommand('QUIT');
455
- } catch (err) {
456
- this._debug('Error during QUIT:', err.message);
457
- }
458
- this.socket.end();
459
- this.connected = false;
460
- this.authenticated = false;
461
- this._debug('Connection closed');
462
- }
463
- }
464
13
 
465
- /**
466
- * Disconnect (alias for close)
467
- * @returns {Promise<void>}
468
- */
469
- async disconnect() {
470
- return this.close();
471
- }
472
- }
14
+ const FTPClient = require('./lib/FTPClient');
473
15
 
474
16
  module.exports = FTPClient;
475
17
  module.exports.FTPClient = FTPClient;