molex-ftp-client 1.0.1 → 1.2.1

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,61 @@
1
+ # Changelog
2
+
3
+ ## [1.2.1] - 2026-02-02
4
+
5
+ ### Changed
6
+ - Improved README documentation
7
+ - Better examples for `ensureDir()`, `ensureParentDir()`, and `uploadFile()`
8
+ - Added architecture section explaining modular structure
9
+ - Enhanced feature list highlighting directory management capabilities
10
+ - Reorganized API documentation for better clarity
11
+
12
+ ## [1.2.0] - 2026-02-02
13
+
14
+ ### Changed
15
+ - **Major refactoring**: Improved separation of concerns
16
+ - `index.js` now serves as simple entry point
17
+ - Implementation moved to organized `lib/` structure:
18
+ - `lib/FTPClient.js` - Main class definition
19
+ - `lib/connection.js` - Connection and authentication logic
20
+ - `lib/commands.js` - All FTP command implementations
21
+ - `lib/utils.js` - Helper functions
22
+ - Better code maintainability and readability
23
+ - No breaking changes - API remains identical
24
+
25
+ ## [1.1.0] - 2026-02-02
26
+
27
+ ### Added
28
+ - `ensureDir(dirPath, recursive)` - Ensure directory exists, creating parent directories if needed
29
+ - `ensureParentDir(filePath)` - Ensure parent directory exists for a file path
30
+ - `uploadFile(data, remotePath, ensureDir)` - Upload with automatic directory creation
31
+ - Utility library (`lib/utils.js`) for better code organization
32
+ - Helper functions for FTP command parsing and path manipulation
33
+
34
+ ### Changed
35
+ - Refactored internal code structure for better maintainability
36
+ - Improved path normalization across all directory operations
37
+ - Better error handling for directory creation
38
+
39
+ ### Improved
40
+ - Cleaner API for common operations
41
+ - Reduced boilerplate code needed for directory handling
42
+ - More consistent error messages
43
+
44
+ ## [1.0.1] - 2026-02-02
45
+
46
+ ### Fixed
47
+ - Updated repository URLs to correct GitHub location
48
+
49
+ ## [1.0.0] - 2026-02-02
50
+
51
+ ### Initial Release
52
+ - Zero dependencies FTP client using native Node.js TCP sockets
53
+ - Promise-based API with async/await support
54
+ - Passive mode (PASV) for data transfers
55
+ - Debug logging with configurable options
56
+ - Connection keep-alive and timeout configuration
57
+ - Upload/download files with Buffer support
58
+ - Directory operations (list, cd, mkdir, pwd)
59
+ - File operations (delete, rename, size, exists, modifiedTime)
60
+ - Connection statistics tracking
61
+ - Event-based architecture
package/README.md CHANGED
@@ -12,9 +12,11 @@ Lightweight FTP client built with native Node.js TCP sockets (net module).
12
12
  - ✅ **Configurable timeouts** - Prevent hanging connections
13
13
  - ✅ **Event-based** - Listen to FTP responses and events
14
14
  - ✅ **Upload/download** files with Buffer support
15
- - ✅ **Directory operations** (list, cd, mkdir, pwd)
15
+ - ✅ **Smart directory management** - Auto-create nested directories
16
+ - ✅ **Directory operations** (list, cd, mkdir, pwd, ensureDir)
16
17
  - ✅ **File operations** (delete, rename, size, exists, modifiedTime)
17
18
  - ✅ **Connection statistics** - Track command count and status
19
+ - ✅ **Clean architecture** - Modular structure with separation of concerns
18
20
 
19
21
  ## Installation
20
22
 
@@ -113,6 +115,27 @@ await client.upload(buffer, '/path/file.bin');
113
115
 
114
116
  Returns: `Promise<void>`
115
117
 
118
+ #### `uploadFile(data, remotePath, ensureDir)`
119
+
120
+ Upload file and optionally ensure parent directory exists. This is the recommended method when uploading to deep paths.
121
+
122
+ ```javascript
123
+ // Upload with automatic directory creation (recommended)
124
+ await client.uploadFile('data', '/deep/nested/path/file.txt', true);
125
+
126
+ // Upload without directory creation
127
+ await client.uploadFile('data', '/file.txt', false);
128
+ ```
129
+
130
+ **Parameters:**
131
+ - `data` (string|Buffer): File content
132
+ - `remotePath` (string): Remote file path
133
+ - `ensureDir` (boolean): Create parent directories if needed (default: false)
134
+
135
+ **Why use this?** Simplifies uploads to nested paths by automatically creating any missing parent directories.
136
+
137
+ Returns: `Promise<void>`
138
+
116
139
  #### `download(remotePath)`
117
140
 
118
141
  Download file from server.
@@ -221,6 +244,46 @@ await client.mkdir('/remote/newdir');
221
244
 
222
245
  Returns: `Promise<void>`
223
246
 
247
+ #### `ensureDir(dirPath, recursive)`
248
+
249
+ Ensure directory exists, creating it (and parent directories) if necessary. Idempotent - safe to call multiple times.
250
+
251
+ ```javascript
252
+ // Create nested directories recursively (default)
253
+ await client.ensureDir('/deep/nested/path');
254
+
255
+ // Create single directory only (will fail if parent doesn't exist)
256
+ await client.ensureDir('/newdir', false);
257
+
258
+ // Idempotent - no error if directory already exists
259
+ await client.ensureDir('/existing/path'); // No error
260
+ ```
261
+
262
+ **Parameters:**
263
+ - `dirPath` (string): Directory path to ensure exists
264
+ - `recursive` (boolean): Create parent directories if needed (default: true)
265
+
266
+ **Use case:** Preparing directory structure before multiple file uploads.
267
+
268
+ Returns: `Promise<void>`
269
+
270
+ #### `ensureParentDir(filePath)`
271
+
272
+ Ensure the parent directory exists for a given file path. Recursively creates all missing parent directories.
273
+
274
+ ```javascript
275
+ // Ensures /path/to exists before uploading
276
+ await client.ensureParentDir('/path/to/file.txt');
277
+ await client.upload('data', '/path/to/file.txt');
278
+
279
+ // Tip: Use uploadFile() instead for convenience
280
+ await client.uploadFile('data', '/path/to/file.txt', true); // Equivalent
281
+ ```
282
+
283
+ **Use case:** Manual directory management before upload. Consider using `uploadFile()` with `ensureDir=true` for a simpler approach.
284
+
285
+ Returns: `Promise<void>`
286
+
224
287
  ### Utilities
225
288
 
226
289
  #### `getStats()`
@@ -368,9 +431,9 @@ async function backupFile() {
368
431
  await client.rename('/backup/data.json', '/backup/data.old.json');
369
432
  }
370
433
 
371
- // Upload new backup
434
+ // Upload new backup (with automatic directory creation)
372
435
  const newData = JSON.stringify({ timestamp: Date.now(), data: [1, 2, 3] });
373
- await client.upload(newData, '/backup/data.json');
436
+ await client.uploadFile(newData, '/backup/data.json', true);
374
437
  console.log('Backup uploaded successfully');
375
438
 
376
439
  // Verify
@@ -392,6 +455,26 @@ async function backupFile() {
392
455
  backupFile();
393
456
  ```
394
457
 
458
+ ## Architecture
459
+
460
+ The library is organized with clean separation of concerns:
461
+
462
+ ```
463
+ molex-ftp-client/
464
+ ├── index.js # Entry point - exports FTPClient
465
+ ├── lib/
466
+ │ ├── FTPClient.js # Main class definition & public API
467
+ │ ├── connection.js # Connection & authentication logic
468
+ │ ├── commands.js # FTP command implementations
469
+ │ └── utils.js # Helper functions (parsing, paths)
470
+ ```
471
+
472
+ **Benefits:**
473
+ - 📁 **Modular structure** - Easy to maintain and extend
474
+ - 🔍 **Clear responsibilities** - Each file has a single purpose
475
+ - 🧪 **Testable** - Isolated components for unit testing
476
+ - 📖 **Readable** - Simple entry point with organized implementation
477
+
395
478
  ## License
396
479
 
397
480
  ISC © Tony Wiedman / MolexWorks
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;