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 +16 -3
- package/README.md +99 -407
- package/lib/FTPClient.js +12 -5
- package/lib/commands.js +28 -23
- package/lib/connection.js +2 -7
- package/lib/performance.js +16 -67
- package/package.json +1 -1
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
|
|
3
|
+
Lightweight FTP client built with native Node.js TCP sockets. Zero dependencies, optimized for performance.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
72
|
-
timeout: 30000,
|
|
73
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
Choose a preset based on your use case:
|
|
53
|
+
## API Reference
|
|
89
54
|
|
|
90
|
-
|
|
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',
|
|
113
|
-
port: 21,
|
|
114
|
-
user: 'username',
|
|
115
|
-
password: 'password'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
139
|
-
await client.upload(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
await client.rename('/old.txt', '/new.txt');
|
|
208
101
|
```
|
|
209
102
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#### `size(path)`
|
|
213
|
-
|
|
214
|
-
Get file size in bytes.
|
|
215
|
-
|
|
103
|
+
#### `exists(path)` → `boolean`
|
|
216
104
|
```javascript
|
|
217
|
-
const
|
|
218
|
-
console.log(`File size: ${bytes} bytes`);
|
|
105
|
+
const exists = await client.exists('/path/file.txt');
|
|
219
106
|
```
|
|
220
107
|
|
|
221
|
-
|
|
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
|
|
229
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
#### `modifiedTime(path)`
|
|
238
|
-
|
|
239
|
-
Get file modification time.
|
|
240
|
-
|
|
115
|
+
#### `size(path)` → `number`
|
|
241
116
|
```javascript
|
|
242
|
-
const
|
|
243
|
-
console.log(`Last modified: ${date.toISOString()}`);
|
|
117
|
+
const bytes = await client.size('/path/file.txt');
|
|
244
118
|
```
|
|
245
119
|
|
|
246
|
-
|
|
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('/
|
|
256
|
-
console.log(listing);
|
|
124
|
+
const listing = await client.list('/path');
|
|
257
125
|
```
|
|
258
126
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
#### `cd(path)`
|
|
262
|
-
|
|
263
|
-
Change working directory.
|
|
264
|
-
|
|
127
|
+
#### `mkdir(path)`
|
|
265
128
|
```javascript
|
|
266
|
-
await client.
|
|
129
|
+
await client.mkdir('/path/newdir');
|
|
267
130
|
```
|
|
268
131
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
#### `pwd()`
|
|
272
|
-
|
|
273
|
-
Get current working directory.
|
|
274
|
-
|
|
132
|
+
#### `cd(path)`
|
|
275
133
|
```javascript
|
|
276
|
-
|
|
277
|
-
console.log(`Current directory: ${dir}`);
|
|
134
|
+
await client.cd('/path/to/directory');
|
|
278
135
|
```
|
|
279
136
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
#### `mkdir(path)`
|
|
283
|
-
|
|
284
|
-
Create directory.
|
|
285
|
-
|
|
137
|
+
#### `pwd()` → `string`
|
|
286
138
|
```javascript
|
|
287
|
-
await client.
|
|
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
|
|
298
|
-
await client.ensureDir('/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
346
|
-
//
|
|
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);
|
|
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('
|
|
401
|
-
|
|
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
|
-
##
|
|
178
|
+
## Debugging
|
|
406
179
|
|
|
407
|
-
Enable debug
|
|
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
|
|
428
|
-
|
|
429
|
-
This library implements TCP-level optimizations:
|
|
430
|
-
|
|
431
|
-
### TCP_NODELAY (Nagle's Algorithm)
|
|
195
|
+
## Performance
|
|
432
196
|
|
|
433
|
-
|
|
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
|
-
|
|
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
|
|
461
|
-
await client.downloadStream('/backup.zip',
|
|
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
|
-
##
|
|
222
|
+
## Example
|
|
493
223
|
|
|
494
224
|
```javascript
|
|
495
225
|
const FTPClient = require('molex-ftp-client');
|
|
496
226
|
|
|
497
|
-
async function
|
|
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.
|
|
507
|
-
port: 21,
|
|
232
|
+
host: 'ftp.example.com',
|
|
508
233
|
user: 'admin',
|
|
509
|
-
password: '
|
|
234
|
+
password: 'secret'
|
|
510
235
|
});
|
|
511
236
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
530
|
-
|
|
531
|
-
|
|
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('
|
|
546
|
-
await client.close();
|
|
250
|
+
console.error('FTP Error:', err.message);
|
|
547
251
|
}
|
|
548
252
|
}
|
|
549
253
|
|
|
550
|
-
|
|
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
|
|
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
|
-
//
|
|
29
|
-
this.
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 -
|
|
338
|
-
|
|
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
|
|
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 =
|
|
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
|
|
package/lib/performance.js
CHANGED
|
@@ -1,80 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TCP Performance optimization utilities
|
|
3
|
-
*
|
|
3
|
+
* Applies sensible defaults for FTP connections
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const net = require('net');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param {Object} options -
|
|
10
|
-
* @param {
|
|
11
|
-
* @
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
if (noDelay) {
|
|
26
|
-
socket.setNoDelay(true);
|
|
27
|
-
}
|
|
28
|
-
|
|
19
|
+
socket.setNoDelay(true);
|
|
20
|
+
|
|
29
21
|
// SO_KEEPALIVE - Detect dead connections
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
79
|
-
PERFORMANCE_PRESETS
|
|
28
|
+
createOptimizedSocket
|
|
80
29
|
};
|