molex-ftp-client 2.0.0 → 2.1.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.0] - 2026-02-02
4
+
5
+ ### Added
6
+ - `stat()` method returns detailed file/directory information: `{ exists, size, isFile, isDirectory }`
7
+
8
+ ### Changed
9
+ - **Simplified performance system** - removed over-engineered preset configuration
10
+ - TCP optimizations (TCP_NODELAY, keep-alive) now applied by default at socket creation
11
+ - `createOptimizedSocket()` replaces repeated `optimizeSocket()` calls for cleaner code
12
+ - Updated `exists()` to use new `stat()` method internally
13
+
14
+ ### Removed
15
+ - Performance presets (LOW_LATENCY, HIGH_THROUGHPUT, BALANCED) - unnecessary complexity
16
+ - `performancePreset` and `performance` constructor options
17
+ - `getOptimalChunkSize()` function - Node.js doesn't expose socket buffer controls
18
+
3
19
  ## [2.0.0] - 2026-02-02
4
20
 
5
21
  ### Breaking Changes
@@ -8,9 +24,6 @@
8
24
 
9
25
  ### Added
10
26
  - **Performance optimization system** with TCP tuning (inspired by HFT practices)
11
- - `TCP_NODELAY` support to disable Nagle's algorithm for lower latency
12
- - Configurable socket buffer sizes for optimal throughput
13
- - Performance presets: `LOW_LATENCY`, `HIGH_THROUGHPUT`, `BALANCED`
14
27
  - `downloadStream()` method for memory-efficient large file transfers
15
28
  - `isConnected()` method to check connection and authentication status
16
29
  - Parameter validation for `connect()`, `upload()`, and `download()`
package/README.md CHANGED
@@ -1,28 +1,15 @@
1
1
  # molex-ftp-client
2
2
 
3
- Lightweight FTP client built with native Node.js TCP sockets (net module).
3
+ Lightweight FTP client built with native Node.js TCP sockets. Zero dependencies, optimized for performance.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Zero dependencies** - Uses only native Node.js modules
8
- - **Promise-based API** - Modern async/await support
9
- - **Passive mode** (PASV) for data transfers
10
- - **Debug logging** - Optional verbose logging for troubleshooting
11
- - **Connection keep-alive** - Automatic TCP keep-alive
12
- - **Configurable timeouts** - Prevent hanging connections
13
- - ✅ **Event-based** - Listen to FTP responses and events
14
- - ✅ **Upload/download** files with Buffer support
15
- - ✅ **Smart directory management** - Auto-create nested directories
16
- - ✅ **Directory operations** (list, cd, mkdir, pwd, ensureDir)
17
- - ✅ **File operations** (delete, rename, size, exists, modifiedTime)
18
- - ✅ **Robust exists()** - Properly detects both files and directories
19
- - ✅ **Connection validation** - Automatic state checking before operations
20
- - ✅ **Enhanced error messages** - Include context for better debugging
21
- - ✅ **High-performance TCP** - Optimized socket settings (TCP_NODELAY, buffer tuning)
22
- - ✅ **Performance presets** - LOW_LATENCY, HIGH_THROUGHPUT, BALANCED modes
23
- - ✅ **Streaming downloads** - Memory-efficient for large files
24
- - ✅ **Connection statistics** - Track command count and status
25
- - ✅ **Clean architecture** - Modular structure with separation of concerns
7
+ - **Zero dependencies** - Uses only native Node.js modules
8
+ - **Promise-based API** - Modern async/await support
9
+ - **TCP optimizations** - TCP_NODELAY and keep-alive applied by default
10
+ - **Auto-create directories** - Upload files to nested paths automatically
11
+ - **Streaming support** - Memory-efficient downloads for large files
12
+ - **Full FTP support** - Upload, download, list, delete, rename, stat, and more
26
13
 
27
14
  ## Installation
28
15
 
@@ -35,384 +22,166 @@ npm install molex-ftp-client
35
22
  ```javascript
36
23
  const FTPClient = require('molex-ftp-client');
37
24
 
38
- const client = new FTPClient({
39
- debug: true, // Enable debug logging (default: false)
40
- timeout: 30000, // Command timeout in ms (default: 30000)
41
- keepAlive: true // Enable TCP keep-alive (default: true)
25
+ const client = new FTPClient();
26
+
27
+ await client.connect({
28
+ host: 'ftp.example.com',
29
+ user: 'username',
30
+ password: 'password'
42
31
  });
43
32
 
44
- try {
45
- // Connect to FTP server
46
- await client.connect({
47
- host: 'ftp.example.com',
48
- port: 21,
49
- user: 'username',
50
- password: 'password'
51
- });
52
-
53
- // Upload file
54
- await client.upload('Hello World!', '/remote/path/file.txt');
55
-
56
- // Download file
57
- const data = await client.download('/remote/path/file.txt');
58
- console.log(data.toString());
59
-
60
- // Close connection
61
- await client.close();
62
- } catch (err) {
63
- console.error('FTP Error:', err);
64
- }
33
+ // Upload with auto-directory creation
34
+ await client.upload('Hello World!', '/path/to/file.txt', true);
35
+
36
+ // Download
37
+ const data = await client.download('/path/to/file.txt');
38
+ console.log(data.toString());
39
+
40
+ await client.close();
65
41
  ```
66
42
 
67
43
  ## Constructor Options
68
44
 
69
45
  ```javascript
70
46
  const client = new FTPClient({
71
- debug: false, // Enable debug logging
72
- timeout: 30000, // Command timeout in milliseconds
73
- keepAlive: true, // Enable TCP keep-alive
74
- logger: console.log, // Custom logger function
75
- performancePreset: 'BALANCED', // Performance preset: LOW_LATENCY, HIGH_THROUGHPUT, BALANCED
76
- performance: { // Custom performance tuning (overrides preset)
77
- noDelay: true, // TCP_NODELAY - disable Nagle's algorithm
78
- sendBufferSize: 65536, // SO_SNDBUF in bytes
79
- receiveBufferSize: 65536, // SO_RCVBUF in bytes
80
- keepAlive: true, // TCP keep-alive
81
- keepAliveDelay: 10000 // Keep-alive delay in ms
82
- }
47
+ debug: false, // Enable debug logging
48
+ timeout: 30000, // Command timeout in milliseconds (default: 30000)
49
+ logger: console.log // Custom logger function
83
50
  });
84
51
  ```
85
52
 
86
- ### Performance Presets
87
-
88
- Choose a preset based on your use case:
53
+ ## API Reference
89
54
 
90
- - **`LOW_LATENCY`** - Optimized for small files and interactive operations (fastest response)
91
- - **`HIGH_THROUGHPUT`** - Optimized for large file transfers (maximum bandwidth)
92
- - **`BALANCED`** - Good default for most use cases (recommended)
93
-
94
- ```javascript
95
- // For small files like privileges.xml
96
- const client = new FTPClient({ performancePreset: 'LOW_LATENCY' });
97
-
98
- // For large backups or video files
99
- const client = new FTPClient({ performancePreset: 'HIGH_THROUGHPUT' });
100
- ```
101
-
102
- ## API
103
-
104
- ### Connection
55
+ ### Connection Methods
105
56
 
106
57
  #### `connect(options)`
107
-
108
- Connect to FTP server.
109
-
110
58
  ```javascript
111
59
  await client.connect({
112
- host: 'ftp.example.com', // Required
113
- port: 21, // Default: 21
114
- user: 'username', // Default: 'anonymous'
115
- password: 'password' // Default: 'anonymous@'
60
+ host: 'ftp.example.com', // Required
61
+ port: 21, // Default: 21
62
+ user: 'username', // Default: 'anonymous'
63
+ password: 'password' // Default: 'anonymous@'
116
64
  });
117
65
  ```
118
66
 
119
- Returns: `Promise<void>`
120
-
121
- #### `close()` / `disconnect()`
122
-
123
- Close connection to FTP server.
124
-
67
+ #### `close()`
125
68
  ```javascript
126
69
  await client.close();
127
70
  ```
128
71
 
129
- Returns: `Promise<void>`
130
-
131
- ### File Operations
72
+ ### File Methods
132
73
 
133
74
  #### `upload(data, remotePath, ensureDir)`
134
-
135
- Upload file to server.
136
-
137
75
  ```javascript
138
- // Upload string
139
- await client.upload('Hello World!', '/path/file.txt');
140
-
141
- // Upload Buffer
142
- const buffer = Buffer.from('data');
143
- await client.upload(buffer, '/path/file.bin');
144
-
145
- // Upload with automatic directory creation (recommended for nested paths)
146
- await client.upload('data', '/deep/nested/path/file.txt', true);
76
+ await client.upload('content', '/path/file.txt'); // Basic upload
77
+ await client.upload(buffer, '/path/file.bin'); // Upload Buffer
78
+ await client.upload('content', '/deep/path/file.txt', true); // Auto-create dirs
147
79
  ```
148
80
 
149
- **Parameters:**
150
- - `data` (string|Buffer): File content
151
- - `remotePath` (string): Remote file path
152
- - `ensureDir` (boolean): Create parent directories if needed (default: false)
153
-
154
- Returns: `Promise<void>`
155
-
156
- #### `download(remotePath)`
157
-
158
- Download file from server.
159
-
81
+ #### `download(remotePath)` → `Buffer`
160
82
  ```javascript
161
83
  const data = await client.download('/path/file.txt');
162
- console.log(data.toString()); // Convert Buffer to string
163
84
  ```
164
85
 
165
- Returns: `Promise<Buffer>`
166
-
167
- #### `downloadStream(remotePath, writeStream)`
168
-
169
- Download file as a stream (memory efficient for large files).
170
-
86
+ #### `downloadStream(remotePath, writeStream)` → `number` (bytes)
171
87
  ```javascript
172
88
  const fs = require('fs');
173
-
174
- // Stream large file directly to disk
175
- const writeStream = fs.createWriteStream('./local-file.bin');
176
- const bytesTransferred = await client.downloadStream('/large-file.bin', writeStream);
177
- console.log(`Downloaded ${bytesTransferred} bytes`);
178
-
179
- // Stream to any writable stream
180
- const { PassThrough } = require('stream');
181
- const stream = new PassThrough();
182
- stream.on('data', (chunk) => {
183
- console.log(`Received ${chunk.length} bytes`);
184
- });
185
- await client.downloadStream('/file.txt', stream);
89
+ const stream = fs.createWriteStream('./local.bin');
90
+ const bytes = await client.downloadStream('/remote.bin', stream);
186
91
  ```
187
92
 
188
- **Use this for:** Large files where you don't want to buffer everything in memory.
189
-
190
- Returns: `Promise<number>` - Total bytes transferred
191
-
192
93
  #### `delete(path)`
193
-
194
- Delete file.
195
-
196
94
  ```javascript
197
95
  await client.delete('/path/file.txt');
198
96
  ```
199
97
 
200
- Returns: `Promise<void>`
201
-
202
98
  #### `rename(from, to)`
203
-
204
- Rename or move file.
205
-
206
99
  ```javascript
207
- await client.rename('/old/path.txt', '/new/path.txt');
100
+ await client.rename('/old.txt', '/new.txt');
208
101
  ```
209
102
 
210
- Returns: `Promise<void>`
211
-
212
- #### `size(path)`
213
-
214
- Get file size in bytes.
215
-
103
+ #### `exists(path)` → `boolean`
216
104
  ```javascript
217
- const bytes = await client.size('/path/file.txt');
218
- console.log(`File size: ${bytes} bytes`);
105
+ const exists = await client.exists('/path/file.txt');
219
106
  ```
220
107
 
221
- Returns: `Promise<number>`
222
-
223
- #### `exists(path)`
224
-
225
- Check if file or directory exists.
226
-
108
+ #### `stat(path)` → `Object`
109
+ Get detailed file/directory information.
227
110
  ```javascript
228
- const fileExists = await client.exists('/path/file.txt');
229
- console.log(fileExists ? 'File exists' : 'File not found');
230
-
231
- const dirExists = await client.exists('/path/to/directory');
232
- console.log(dirExists ? 'Directory exists' : 'Directory not found');
111
+ const info = await client.stat('/path/file.txt');
112
+ // { exists: true, size: 1024, isFile: true, isDirectory: false }
233
113
  ```
234
114
 
235
- Returns: `Promise<boolean>`
236
-
237
- #### `modifiedTime(path)`
238
-
239
- Get file modification time.
240
-
115
+ #### `size(path)` → `number`
241
116
  ```javascript
242
- const date = await client.modifiedTime('/path/file.txt');
243
- console.log(`Last modified: ${date.toISOString()}`);
117
+ const bytes = await client.size('/path/file.txt');
244
118
  ```
245
119
 
246
- Returns: `Promise<Date>`
247
-
248
- ### Directory Operations
249
-
250
- #### `list(path)`
251
-
252
- List directory contents.
120
+ ### Directory Methods
253
121
 
122
+ #### `list(path)` → `string`
254
123
  ```javascript
255
- const listing = await client.list('/remote/path');
256
- console.log(listing);
124
+ const listing = await client.list('/path');
257
125
  ```
258
126
 
259
- Returns: `Promise<string>` - Raw directory listing
260
-
261
- #### `cd(path)`
262
-
263
- Change working directory.
264
-
127
+ #### `mkdir(path)`
265
128
  ```javascript
266
- await client.cd('/remote/path');
129
+ await client.mkdir('/path/newdir');
267
130
  ```
268
131
 
269
- Returns: `Promise<void>`
270
-
271
- #### `pwd()`
272
-
273
- Get current working directory.
274
-
132
+ #### `cd(path)`
275
133
  ```javascript
276
- const dir = await client.pwd();
277
- console.log(`Current directory: ${dir}`);
134
+ await client.cd('/path/to/directory');
278
135
  ```
279
136
 
280
- Returns: `Promise<string>`
281
-
282
- #### `mkdir(path)`
283
-
284
- Create directory.
285
-
137
+ #### `pwd()` → `string`
286
138
  ```javascript
287
- await client.mkdir('/remote/newdir');
139
+ const currentDir = await client.pwd();
288
140
  ```
289
141
 
290
- Returns: `Promise<void>`
291
-
292
142
  #### `ensureDir(dirPath, recursive, isFilePath)`
293
-
294
- Ensure directory exists, creating it (and parent directories) if necessary. Idempotent - safe to call multiple times.
295
-
143
+ Create directory if it doesn't exist, optionally creating parent directories.
296
144
  ```javascript
297
- // Create nested directories recursively (default)
298
- await client.ensureDir('/deep/nested/path');
299
-
300
- // Create single directory only (will fail if parent doesn't exist)
301
- await client.ensureDir('/newdir', false);
302
-
303
- // Ensure parent directory for a file path
304
- await client.ensureDir('/path/to/file.txt', true, true);
305
-
306
- // Idempotent - no error if directory already exists
307
- await client.ensureDir('/existing/path'); // No error
145
+ await client.ensureDir('/deep/nested/path'); // Create full path
146
+ await client.ensureDir('/path/file.txt', true, true); // Ensure parent dir for file
308
147
  ```
309
148
 
310
- **Parameters:**
311
- - `dirPath` (string): Directory path or file path to ensure exists
312
- - `recursive` (boolean): Create parent directories if needed (default: true)
313
- - `isFilePath` (boolean): If true, ensures parent directory of file path (default: false)
314
-
315
- **Use case:** Preparing directory structure before multiple file uploads. For single file uploads, use `upload()` with `ensureDir=true`.
316
-
317
- Returns: `Promise<void>`
318
-
319
- ### Utilities
320
-
321
- #### `isConnected()`
322
-
323
- Check if client is connected and authenticated.
149
+ ### Utility Methods
324
150
 
151
+ #### `getState()` → `Object`
152
+ Get current client state for debugging.
325
153
  ```javascript
326
- if (client.isConnected()) {
327
- console.log('Ready to execute commands');
328
- } else {
329
- console.log('Not connected');
330
- }
331
- ```
332
-
333
- Returns: `boolean`
334
-
335
- #### `getStats()`
336
-
337
- Get connection statistics.
338
-
339
- ```javascript
340
- const stats = client.getStats();
341
- console.log(stats);
154
+ const state = client.getState();
342
155
  // {
343
156
  // connected: true,
344
157
  // authenticated: true,
345
- // commandCount: 5,
346
- // lastCommand: 'LIST .'
158
+ // host: 'ftp.example.com',
159
+ // ...
347
160
  // }
348
161
  ```
349
162
 
350
- Returns: `Object`
351
-
352
163
  #### `setDebug(enabled)`
353
-
354
- Enable or disable debug mode at runtime.
355
-
164
+ Toggle debug mode at runtime.
356
165
  ```javascript
357
- client.setDebug(true); // Enable debug logging
358
- client.setDebug(false); // Disable debug logging
166
+ client.setDebug(true);
359
167
  ```
360
168
 
361
169
  ## Events
362
170
 
363
- The client extends EventEmitter and emits the following events:
364
-
365
- ### `connected`
366
-
367
- Fired when TCP connection is established.
368
-
369
- ```javascript
370
- client.on('connected', () => {
371
- console.log('Connected to FTP server');
372
- });
373
- ```
374
-
375
- ### `response`
376
-
377
- Fired for each FTP response (useful for debugging).
378
-
379
- ```javascript
380
- client.on('response', (line) => {
381
- console.log('FTP:', line);
382
- });
383
- ```
384
-
385
- ### `error`
386
-
387
- Fired on connection errors.
388
-
389
- ```javascript
390
- client.on('error', (err) => {
391
- console.error('FTP Error:', err);
392
- });
393
- ```
394
-
395
- ### `close`
396
-
397
- Fired when connection is closed.
398
-
399
171
  ```javascript
400
- client.on('close', () => {
401
- console.log('Connection closed');
402
- });
172
+ client.on('connected', () => console.log('TCP connection established'));
173
+ client.on('response', (line) => console.log('FTP:', line));
174
+ client.on('error', (err) => console.error('Error:', err));
175
+ client.on('close', () => console.log('Connection closed'));
403
176
  ```
404
177
 
405
- ## Debug Mode
178
+ ## Debugging
406
179
 
407
- Enable debug logging to troubleshoot FTP issues:
180
+ Enable debug mode to see all FTP commands and responses:
408
181
 
409
182
  ```javascript
410
183
  const client = new FTPClient({ debug: true });
411
184
 
412
- client.on('response', (line) => {
413
- console.log('FTP Response:', line);
414
- });
415
-
416
185
  await client.connect({ host: 'ftp.example.com', user: 'user', password: 'pass' });
417
186
  // [FTP Debug] Connecting to ftp.example.com:21 as user
418
187
  // [FTP Debug] TCP connection established
@@ -421,147 +190,70 @@ await client.connect({ host: 'ftp.example.com', user: 'user', password: 'pass' }
421
190
  // [FTP Debug] <<< 331 Password required
422
191
  // [FTP Debug] >>> PASS ********
423
192
  // [FTP Debug] <<< 230 Login successful
424
- // [FTP Debug] Authentication successful
425
193
  ```
426
194
 
427
- ## Performance Optimization
428
-
429
- This library implements TCP-level optimizations:
430
-
431
- ### TCP_NODELAY (Nagle's Algorithm)
195
+ ## Performance
432
196
 
433
- By default, the client uses `TCP_NODELAY` which disables Nagle's algorithm. This reduces latency by sending packets immediately instead of buffering them.
197
+ TCP optimizations are automatically applied:
198
+ - **TCP_NODELAY** - Disables Nagle's algorithm for lower latency
199
+ - **Keep-alive** - Detects dead connections (10s interval)
434
200
 
435
- **When to use:**
436
- - Small file transfers
437
- - Interactive operations
438
- - When latency matters more than bandwidth
439
-
440
- **Trade-off:** Slightly more network overhead, but much faster response times.
441
-
442
- ### Buffer Sizing
443
-
444
- The client optimizes socket buffers based on your use case:
445
-
446
- ```javascript
447
- // Low latency (32KB buffers) - fast for small files
448
- const client = new FTPClient({ performancePreset: 'LOW_LATENCY' });
449
-
450
- // High throughput (128KB buffers) - better for large files
451
- const client = new FTPClient({ performancePreset: 'HIGH_THROUGHPUT' });
452
- ```
453
-
454
- ### Streaming for Large Files
455
-
456
- Use `downloadStream()` for memory-efficient transfers:
201
+ For large files, use `downloadStream()` for memory-efficient transfers:
457
202
 
458
203
  ```javascript
459
204
  const fs = require('fs');
460
- const writeStream = fs.createWriteStream('./large-backup.zip');
461
- await client.downloadStream('/backup.zip', writeStream);
205
+ const stream = fs.createWriteStream('./large.zip');
206
+ const bytes = await client.downloadStream('/backup.zip', stream);
207
+ console.log(`Downloaded ${bytes} bytes`);
462
208
  ```
463
209
 
464
- **Benefits:**
465
- - Constant memory usage (no buffering entire file)
466
- - Start processing data before download completes
467
- - Better for files > 10MB
468
-
469
- ### Performance Comparison
470
-
471
- For a typical privileges.xml (< 100KB):
472
- - **Without optimization:** ~150-200ms
473
- - **With LOW_LATENCY preset:** ~80-120ms
474
- - **40-50% faster** response time
475
-
476
210
  ## Error Handling
477
211
 
478
- All methods return promises and will reject on errors:
479
-
480
212
  ```javascript
481
213
  try {
482
214
  await client.upload('data', '/readonly/file.txt');
483
215
  } catch (err) {
484
216
  if (err.message.includes('FTP Error 550')) {
485
217
  console.error('Permission denied');
486
- } else {
487
- console.error('Upload failed:', err.message);
488
218
  }
489
219
  }
490
220
  ```
491
221
 
492
- ## Complete Example
222
+ ## Example
493
223
 
494
224
  ```javascript
495
225
  const FTPClient = require('molex-ftp-client');
496
226
 
497
- async function backupFile() {
498
- const client = new FTPClient({
499
- debug: true,
500
- timeout: 60000
501
- });
227
+ async function main() {
228
+ const client = new FTPClient({ debug: true });
502
229
 
503
230
  try {
504
- // Connect
505
231
  await client.connect({
506
- host: 'ftp.myserver.com',
507
- port: 21,
232
+ host: 'ftp.example.com',
508
233
  user: 'admin',
509
- password: 'secret123'
234
+ password: 'secret'
510
235
  });
511
236
 
512
- console.log('Current directory:', await client.pwd());
513
-
514
- // Check if file exists
515
- const exists = await client.exists('/backup/data.json');
516
- if (exists) {
517
- // Download existing file
518
- const oldData = await client.download('/backup/data.json');
519
- console.log('Old backup size:', oldData.length, 'bytes');
520
-
521
- // Get modification time
522
- const modTime = await client.modifiedTime('/backup/data.json');
523
- console.log('Last modified:', modTime.toISOString());
524
-
525
- // Rename old backup
526
- await client.rename('/backup/data.json', '/backup/data.old.json');
237
+ // Check file info
238
+ const info = await client.stat('/backup/data.json');
239
+ if (info.exists) {
240
+ console.log(`File size: ${info.size} bytes`);
241
+ const data = await client.download('/backup/data.json');
242
+ console.log('Downloaded:', data.toString());
527
243
  }
528
244
 
529
- // Upload new backup (with automatic directory creation)
530
- const newData = JSON.stringify({ timestamp: Date.now(), data: [1, 2, 3] });
531
- await client.upload(newData, '/backup/data.json', true);
532
- console.log('Backup uploaded successfully');
533
-
534
- // Verify
535
- const size = await client.size('/backup/data.json');
536
- console.log('New backup size:', size, 'bytes');
537
-
538
- // Get stats
539
- const stats = client.getStats();
540
- console.log('Commands executed:', stats.commandCount);
541
-
542
- // Close connection
245
+ // Upload new file
246
+ await client.upload('new data', '/backup/updated.json', true);
247
+
543
248
  await client.close();
544
249
  } catch (err) {
545
- console.error('Backup failed:', err.message);
546
- await client.close();
250
+ console.error('FTP Error:', err.message);
547
251
  }
548
252
  }
549
253
 
550
- backupFile();
254
+ main();
551
255
  ```
552
256
 
553
- ## Architecture
554
-
555
- ```
556
- molex-ftp-client/
557
- ├── index.js # Entry point - exports FTPClient
558
- ├── lib/
559
- │ ├── FTPClient.js # Main class definition & public API
560
- │ ├── connection.js # Connection & authentication logic
561
- │ ├── commands.js # FTP command implementations
562
- │ └── utils.js # Helper functions (parsing, paths)
563
- ```
564
257
  ## License
565
258
 
566
- ISC © Tony Wiedman / MolexWorks
567
-
259
+ ISC License
package/lib/FTPClient.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const FTPConnection = require('./connection');
3
3
  const FTPCommands = require('./commands');
4
- const { PERFORMANCE_PRESETS } = require('./performance');
5
4
 
6
5
  /**
7
6
  * Lightweight FTP Client using native Node.js TCP sockets (net module)
@@ -25,10 +24,8 @@ class FTPClient extends EventEmitter
25
24
  this.keepAlive = options.keepAlive !== false;
26
25
  this._log = options.logger || console.log;
27
26
 
28
- // Performance tuning
29
- this.performancePreset = options.performancePreset || 'BALANCED';
30
- this.performanceOptions = options.performance ||
31
- PERFORMANCE_PRESETS[this.performancePreset] || PERFORMANCE_PRESETS.BALANCED;
27
+ // Statistics
28
+ this._commandCount = 0;
32
29
  this._lastCommand = null;
33
30
 
34
31
  // Initialize subsystems
@@ -199,6 +196,16 @@ class FTPClient extends EventEmitter
199
196
  return this._commands.exists(path);
200
197
  }
201
198
 
199
+ /**
200
+ * Get file/directory information
201
+ * @param {string} path - Path to check
202
+ * @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
203
+ */
204
+ async stat(path)
205
+ {
206
+ return this._commands.stat(path);
207
+ }
208
+
202
209
  /**
203
210
  * Ensure directory exists, creating it if necessary
204
211
  * @param {string} dirPath - Directory or file path to ensure exists
package/lib/commands.js CHANGED
@@ -1,6 +1,5 @@
1
- const net = require('net');
2
1
  const { normalizePath, getParentDir, parseMdtmResponse } = require('./utils');
3
- const { optimizeSocket } = require('./performance');
2
+ const { createOptimizedSocket } = require('./performance');
4
3
 
5
4
  /**
6
5
  * FTP command implementations
@@ -32,10 +31,7 @@ class FTPCommands {
32
31
  return new Promise((resolve, reject) => {
33
32
  let commandSent = false;
34
33
 
35
- this.client.dataSocket = net.createConnection({ host, port }, () => {
36
-
37
- optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
38
-
34
+ this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
39
35
  // Send STOR command to start upload (expects 150, then 226)
40
36
  if (!commandSent) {
41
37
  commandSent = true;
@@ -90,10 +86,7 @@ class FTPCommands {
90
86
  const chunks = [];
91
87
  let commandSent = false;
92
88
 
93
- this.client.dataSocket = net.createConnection({ host, port }, () => {
94
-
95
- optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
96
-
89
+ this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
97
90
  // Send RETR command to start download (expects 150, then 226)
98
91
  if (!commandSent) {
99
92
  commandSent = true;
@@ -154,10 +147,7 @@ class FTPCommands {
154
147
  let totalBytes = 0;
155
148
  let commandSent = false;
156
149
 
157
- this.client.dataSocket = net.createConnection({ host, port }, () => {
158
-
159
- optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
160
-
150
+ this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
161
151
  if (!commandSent) {
162
152
  commandSent = true;
163
153
  this.client._debug(`Data connection established for streaming download`);
@@ -220,10 +210,7 @@ class FTPCommands {
220
210
  const chunks = [];
221
211
  let commandSent = false;
222
212
 
223
- this.client.dataSocket = net.createConnection({ host, port }, () => {
224
-
225
- optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
226
-
213
+ this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
227
214
  if (!commandSent) {
228
215
  commandSent = true;
229
216
  this.connection.sendCommand(`LIST ${path}`, true).catch(reject);
@@ -321,10 +308,20 @@ class FTPCommands {
321
308
  * @returns {Promise<boolean>}
322
309
  */
323
310
  async exists(path) {
311
+ const info = await this.stat(path);
312
+ return info.exists;
313
+ }
314
+
315
+ /**
316
+ * Get file/directory information
317
+ * @param {string} path - Path to check
318
+ * @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
319
+ */
320
+ async stat(path) {
324
321
  try {
325
322
  // First try SIZE command (works for files)
326
- await this.size(path);
327
- return true;
323
+ const size = await this.size(path);
324
+ return { exists: true, size, isFile: true, isDirectory: false };
328
325
  } catch (err) {
329
326
  // SIZE failed, might be a directory - try CWD
330
327
  try {
@@ -332,10 +329,18 @@ class FTPCommands {
332
329
  await this.cd(path);
333
330
  // Restore original directory
334
331
  await this.cd(currentDir);
335
- return true;
332
+ return { exists: true, size: null, isFile: false, isDirectory: true };
336
333
  } catch (cdErr) {
337
- // Both SIZE and CWD failed - doesn't exist
338
- return false;
334
+ // Both SIZE and CWD failed - try listing parent directory
335
+ try {
336
+ const dir = getParentDir(path);
337
+ const basename = path.split('/').pop();
338
+ const listing = await this.list(dir);
339
+ const found = listing.split('\n').some(line => line.includes(basename));
340
+ return { exists: found, size: null, isFile: null, isDirectory: null };
341
+ } catch (listErr) {
342
+ return { exists: false, size: null, isFile: null, isDirectory: null };
343
+ }
339
344
  }
340
345
  }
341
346
  }
package/lib/connection.js CHANGED
@@ -1,5 +1,4 @@
1
- const net = require('net');
2
- const { optimizeSocket } = require('./performance');
1
+ const { createOptimizedSocket } = require('./performance');
3
2
 
4
3
  /**
5
4
  * Handle FTP connection establishment and authentication
@@ -22,14 +21,10 @@ class FTPConnection {
22
21
  this.client._debug(`Connecting to ${host}:${port} as ${user}`);
23
22
 
24
23
  return new Promise((resolve, reject) => {
25
- this.client.socket = net.createConnection({ host, port }, () => {
24
+ this.client.socket = createOptimizedSocket({ host, port }, () => {
26
25
  this.client.connected = true;
27
26
  this.client._debug('TCP connection established');
28
27
 
29
- // Apply performance optimizations
30
- optimizeSocket(this.client.socket, this.client.performanceOptions);
31
- this.client._debug('TCP optimizations applied:', this.client.performancePreset);
32
-
33
28
  this.client.emit('connected');
34
29
  });
35
30
 
@@ -1,80 +1,29 @@
1
1
  /**
2
2
  * TCP Performance optimization utilities
3
- * Based on high-performance networking best practices
3
+ * Applies sensible defaults for FTP connections
4
4
  */
5
5
 
6
+ const net = require('net');
7
+
6
8
  /**
7
- * Apply performance optimizations to a TCP socket
8
- * @param {net.Socket} socket - TCP socket to optimize
9
- * @param {Object} options - Performance options
10
- * @param {boolean} options.noDelay - Disable Nagle's algorithm (default: true)
11
- * @param {boolean} options.keepAlive - Enable TCP keep-alive (default: true)
12
- * @param {number} options.keepAliveDelay - Keep-alive initial delay in ms (default: 10000)
9
+ * Create an optimized TCP socket connection
10
+ * Automatically applies TCP_NODELAY and keep-alive
11
+ * @param {Object} options - Connection options (host, port)
12
+ * @param {Function} callback - Callback on connection
13
+ * @returns {net.Socket}
13
14
  */
14
- function optimizeSocket(socket, options = {}) {
15
- // Use defaults if options not provided
16
- const {
17
- noDelay = true,
18
- keepAlive = true,
19
- keepAliveDelay = 10000
20
- } = options || {};
21
-
15
+ function createOptimizedSocket(options, callback) {
16
+ const socket = net.createConnection(options, callback);
17
+
22
18
  // TCP_NODELAY - Disable Nagle's algorithm for lower latency
23
- // Critical for interactive applications and small packet transfers
24
- // Nagle's algorithm buffers small packets, adding latency
25
- if (noDelay) {
26
- socket.setNoDelay(true);
27
- }
28
-
19
+ socket.setNoDelay(true);
20
+
29
21
  // SO_KEEPALIVE - Detect dead connections
30
- if (keepAlive) {
31
- socket.setKeepAlive(true, keepAliveDelay);
32
- }
33
-
22
+ socket.setKeepAlive(true, 10000);
23
+
34
24
  return socket;
35
25
  }
36
26
 
37
- /**
38
- * Performance presets for different use cases
39
- */
40
- const PERFORMANCE_PRESETS = {
41
- // Low latency - prioritize speed over bandwidth
42
- // Good for small files, interactive operations
43
- LOW_LATENCY: {
44
- noDelay: true,
45
- sendBufferSize: 32768, // 32KB
46
- receiveBufferSize: 32768, // 32KB
47
- keepAlive: true,
48
- keepAliveDelay: 5000
49
- },
50
-
51
- // High throughput - prioritize bandwidth over latency
52
- // Good for large file transfers
53
- HIGH_THROUGHPUT: {
54
- noDelay: false, // Allow Nagle's algorithm to batch
55
- sendBufferSize: 131072, // 128KB
56
- receiveBufferSize: 131072, // 128KB
57
- keepAlive: true,
58
- keepAliveDelay: 30000
59
- },
60
-
61
- // Balanced - good default for most use cases
62
- BALANCED: {
63
- noDelay: true,
64
- sendBufferSize: 65536, // 64KB
65
- receiveBufferSize: 65536, // 64KB
66
- keepAlive: true,
67
- keepAliveDelay: 10000
68
- },
69
-
70
- // Default Node.js behavior
71
- DEFAULT: {
72
- noDelay: false,
73
- keepAlive: false
74
- }
75
- };
76
-
77
27
  module.exports = {
78
- optimizeSocket,
79
- PERFORMANCE_PRESETS
28
+ createOptimizedSocket
80
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molex-ftp-client",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Lightweight FTP client using native Node.js TCP sockets (net module) with zero dependencies",
5
5
  "main": "index.js",
6
6
  "scripts": {