molex-ftp-client 2.1.0 → 2.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0] - 2026-02-02
4
+
5
+ ### Fixed
6
+ - **CRITICAL: Fixed 30-second timeout on all data transfers** - upload, download, list, downloadStream now complete instantly
7
+ - Commands with data connections no longer wait for 226 completion response, dramatically improving speed
8
+ - Download speed improved from 0.03 MB/s to 2.47 MB/s (80x faster!)
9
+ - Recursive directory deletion now blazing fast (sub-second instead of minutes)
10
+
11
+ ### Added
12
+ - `removeDir(path, recursive)` - Remove directories with optional recursive deletion
13
+ - `chmod(path, mode)` - Change file permissions (Unix/Linux servers)
14
+ - `listDetailed(path)` - Get parsed directory listings with permissions, owner, size, etc.
15
+ - `site(command)` - Execute server-specific SITE commands
16
+
17
+ ### Improved
18
+ - `listDetailed()` now filters out `.` and `..` entries automatically
19
+ - Better handling of unknown file types in recursive operations
20
+
3
21
  ## [2.1.0] - 2026-02-02
4
22
 
5
23
  ### Added
package/README.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # molex-ftp-client
2
2
 
3
- Lightweight FTP client built with native Node.js TCP sockets. Zero dependencies, optimized for performance.
3
+ [![npm version](https://img.shields.io/npm/v/molex-ftp-client.svg)](https://www.npmjs.com/package/molex-ftp-client)
4
+ [![npm downloads](https://img.shields.io/npm/dm/molex-ftp-client.svg)](https://www.npmjs.com/package/molex-ftp-client)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D14-brightgreen.svg)](https://nodejs.org)
7
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](package.json)
8
+
9
+ > **Lightweight FTP client built with native Node.js TCP sockets. Zero dependencies, optimized for performance.**
4
10
 
5
11
  ## Features
6
12
 
7
13
  - **Zero dependencies** - Uses only native Node.js modules
8
14
  - **Promise-based API** - Modern async/await support
9
- - **TCP optimizations** - TCP_NODELAY and keep-alive applied by default
15
+ - **TCP optimizations** - TCP_NODELAY and keep-alive applied by default (~2.5 MB/s transfer speeds)
10
16
  - **Auto-create directories** - Upload files to nested paths automatically
11
17
  - **Streaming support** - Memory-efficient downloads for large files
12
- - **Full FTP support** - Upload, download, list, delete, rename, stat, and more
18
+ - **Full FTP support** - Upload, download, list, delete, rename, chmod, stat, and more
19
+ - **Debug mode** - See all FTP commands and responses in real-time
13
20
 
14
21
  ## Installation
15
22
 
@@ -83,11 +90,13 @@ await client.upload('content', '/deep/path/file.txt', true); // Auto-create dirs
83
90
  const data = await client.download('/path/file.txt');
84
91
  ```
85
92
 
86
- #### `downloadStream(remotePath, writeStream)` → `number` (bytes)
93
+ #### `downloadStream(remotePath, writeStream)` → `number`
94
+ Stream download directly to a writable stream (for saving to disk or processing chunks).
87
95
  ```javascript
88
96
  const fs = require('fs');
89
- const stream = fs.createWriteStream('./local.bin');
90
- const bytes = await client.downloadStream('/remote.bin', stream);
97
+ const fileStream = fs.createWriteStream('./local-file.bin');
98
+ const bytes = await client.downloadStream('/remote.bin', fileStream);
99
+ console.log(`Saved ${bytes} bytes to disk`);
91
100
  ```
92
101
 
93
102
  #### `delete(path)`
@@ -95,6 +104,13 @@ const bytes = await client.downloadStream('/remote.bin', stream);
95
104
  await client.delete('/path/file.txt');
96
105
  ```
97
106
 
107
+ #### `removeDir(path, recursive)`
108
+ Remove directory, optionally with all contents.
109
+ ```javascript
110
+ await client.removeDir('/path/emptydir'); // Remove empty directory
111
+ await client.removeDir('/path/dir', true); // Delete recursively with contents
112
+ ```
113
+
98
114
  #### `rename(from, to)`
99
115
  ```javascript
100
116
  await client.rename('/old.txt', '/new.txt');
@@ -117,13 +133,37 @@ const info = await client.stat('/path/file.txt');
117
133
  const bytes = await client.size('/path/file.txt');
118
134
  ```
119
135
 
136
+ #### `modifiedTime(path)` → `Date`
137
+ ```javascript
138
+ const date = await client.modifiedTime('/path/file.txt');
139
+ ```
140
+
141
+ #### `chmod(path, mode)`
142
+ Change file permissions (Unix/Linux servers only).
143
+ ```javascript
144
+ await client.chmod('/path/file.txt', '755'); // String format
145
+ await client.chmod('/path/script.sh', 0755); // Octal format
146
+ ```
147
+
120
148
  ### Directory Methods
121
149
 
122
150
  #### `list(path)` → `string`
151
+ Raw directory listing.
123
152
  ```javascript
124
153
  const listing = await client.list('/path');
125
154
  ```
126
155
 
156
+ #### `listDetailed(path)` → `Array`
157
+ Parsed directory listing with permissions, owner, size, etc.
158
+ ```javascript
159
+ const files = await client.listDetailed('/path');
160
+ // [
161
+ // { name: 'file.txt', type: 'file', permissions: '-rw-r--r--',
162
+ // owner: 'user', group: 'group', size: 1024, date: 'Jan 15 10:30' },
163
+ // { name: 'subdir', type: 'directory', permissions: 'drwxr-xr-x', ... }
164
+ // ]
165
+ ```
166
+
127
167
  #### `mkdir(path)`
128
168
  ```javascript
129
169
  await client.mkdir('/path/newdir');
@@ -166,6 +206,13 @@ Toggle debug mode at runtime.
166
206
  client.setDebug(true);
167
207
  ```
168
208
 
209
+ #### `site(command)` → `Object`
210
+ Execute server-specific SITE commands.
211
+ ```javascript
212
+ await client.site('CHMOD 755 /path/file.txt'); // Alternative chmod
213
+ const response = await client.site('HELP'); // Get server help
214
+ ```
215
+
169
216
  ## Events
170
217
 
171
218
  ```javascript
@@ -198,13 +245,15 @@ TCP optimizations are automatically applied:
198
245
  - **TCP_NODELAY** - Disables Nagle's algorithm for lower latency
199
246
  - **Keep-alive** - Detects dead connections (10s interval)
200
247
 
201
- For large files, use `downloadStream()` for memory-efficient transfers:
248
+ **Typical transfer speeds:** ~2.5 MB/s for 1MB files over standard internet connections.
249
+
250
+ For large files, use `downloadStream()` to save directly to disk without buffering in memory:
202
251
 
203
252
  ```javascript
204
253
  const fs = require('fs');
205
- const stream = fs.createWriteStream('./large.zip');
206
- const bytes = await client.downloadStream('/backup.zip', stream);
207
- console.log(`Downloaded ${bytes} bytes`);
254
+ const fileStream = fs.createWriteStream('./large-backup.zip');
255
+ const bytes = await client.downloadStream('/backup.zip', fileStream);
256
+ console.log(`Saved ${bytes} bytes to disk`);
208
257
  ```
209
258
 
210
259
  ## Error Handling
package/lib/FTPClient.js CHANGED
@@ -165,6 +165,17 @@ class FTPClient extends EventEmitter
165
165
  return this._commands.delete(path);
166
166
  }
167
167
 
168
+ /**
169
+ * Remove directory
170
+ * @param {string} path - Directory path
171
+ * @param {boolean} recursive - Delete all contents recursively (default: false)
172
+ * @returns {Promise<void>}
173
+ */
174
+ async removeDir(path, recursive = false)
175
+ {
176
+ return this._commands.removeDir(path, recursive);
177
+ }
178
+
168
179
  /**
169
180
  * Rename file
170
181
  * @param {string} from - Current name
@@ -261,6 +272,37 @@ class FTPClient extends EventEmitter
261
272
  this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
262
273
  }
263
274
 
275
+ /**
276
+ * Change file permissions (Unix/Linux servers only)
277
+ * @param {string} path - File or directory path
278
+ * @param {string|number} mode - Permissions (e.g., '755', 0755)
279
+ * @returns {Promise<void>}
280
+ */
281
+ async chmod(path, mode)
282
+ {
283
+ return this._commands.chmod(path, mode);
284
+ }
285
+
286
+ /**
287
+ * Execute a SITE command (server-specific commands)
288
+ * @param {string} command - SITE command to execute
289
+ * @returns {Promise<Object>}
290
+ */
291
+ async site(command)
292
+ {
293
+ return this._commands.site(command);
294
+ }
295
+
296
+ /**
297
+ * Get detailed directory listing with permissions, owner, size, etc.
298
+ * @param {string} path - Directory path
299
+ * @returns {Promise<Array>}
300
+ */
301
+ async listDetailed(path = '.')
302
+ {
303
+ return this._commands.listDetailed(path);
304
+ }
305
+
264
306
  /**
265
307
  * Close connection
266
308
  * @returns {Promise<void>}
package/lib/commands.js CHANGED
@@ -36,7 +36,9 @@ class FTPCommands {
36
36
  if (!commandSent) {
37
37
  commandSent = true;
38
38
  this.client._debug(`Data connection established for upload`);
39
- this.connection.sendCommand(`STOR ${remotePath}`, true).catch(reject);
39
+ // Just send command, don't wait for completion
40
+ this.client.socket.write(`STOR ${remotePath}\r\n`);
41
+ this.client._commandCount++;
40
42
 
41
43
  // Write data to data socket
42
44
  this.client.dataSocket.write(buffer);
@@ -47,25 +49,10 @@ class FTPCommands {
47
49
  this.client.dataSocket.on('error', reject);
48
50
 
49
51
  this.client.dataSocket.on('close', () => {
50
- // Wait for final response from control socket
51
- const finalHandler = (line) => {
52
- const code = parseInt(line.substring(0, 3));
53
- if (code === 226 || code === 250) {
54
- this.client.removeListener('response', finalHandler);
55
- this.client._debug(`Upload completed successfully`);
56
- resolve();
57
- } else if (code >= 400) {
58
- this.client.removeListener('response', finalHandler);
59
- reject(new Error(`Upload failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
60
- }
61
- };
62
- this.client.on('response', finalHandler);
63
-
64
- // Timeout if no response
65
- setTimeout(() => {
66
- this.client.removeListener('response', finalHandler);
67
- resolve();
68
- }, this.client.timeout || 5000);
52
+ // Upload complete
53
+ this.client._debug(`Upload completed successfully`);
54
+ // Small delay to let 226 response arrive before next command
55
+ setTimeout(() => resolve(), 10);
69
56
  });
70
57
  });
71
58
  }
@@ -85,46 +72,40 @@ class FTPCommands {
85
72
  return new Promise((resolve, reject) => {
86
73
  const chunks = [];
87
74
  let commandSent = false;
75
+ let dataComplete = false;
76
+ let commandComplete = false;
77
+
78
+ const checkComplete = () => {
79
+ if (dataComplete && commandComplete) {
80
+ const result = Buffer.concat(chunks);
81
+ this.client._debug(`Download completed: ${result.length} bytes`);
82
+ resolve(result);
83
+ }
84
+ };
88
85
 
89
86
  this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
90
87
  // Send RETR command to start download (expects 150, then 226)
91
88
  if (!commandSent) {
92
89
  commandSent = true;
93
90
  this.client._debug(`Data connection established for download`);
94
- this.connection.sendCommand(`RETR ${remotePath}`, true).catch(reject);
91
+ // Just send command, don't wait for completion - data socket will handle it
92
+ this.client.socket.write(`RETR ${remotePath}\r\n`);
93
+ this.client._commandCount++;
95
94
  }
96
95
  });
97
96
 
98
97
  this.client.dataSocket.on('data', (chunk) => {
99
98
  chunks.push(chunk);
100
- this.client._debug(`Received ${chunk.length} bytes`);
101
99
  });
102
100
 
103
101
  this.client.dataSocket.on('error', reject);
104
102
 
105
103
  this.client.dataSocket.on('close', () => {
106
- // Wait for final 226 response
107
- const finalHandler = (line) => {
108
- const code = parseInt(line.substring(0, 3));
109
- if (code === 226 || code === 250) {
110
- this.client.removeListener('response', finalHandler);
111
- const result = Buffer.concat(chunks);
112
- this.client._debug(`Download completed: ${result.length} bytes`);
113
- resolve(result);
114
- } else if (code >= 400) {
115
- this.client.removeListener('response', finalHandler);
116
- reject(new Error(`Download failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
117
- }
118
- };
119
- this.client.on('response', finalHandler);
120
-
121
- // Timeout if no response
122
- setTimeout(() => {
123
- this.client.removeListener('response', finalHandler);
124
- if (chunks.length > 0) {
125
- resolve(Buffer.concat(chunks));
126
- }
127
- }, this.client.timeout || 5000);
104
+ // Data transfer complete, resolve immediately
105
+ const result = Buffer.concat(chunks);
106
+ this.client._debug(`Download completed: ${result.length} bytes`);
107
+ // Small delay to let 226 response arrive before next command
108
+ setTimeout(() => resolve(result), 10);
128
109
  });
129
110
  });
130
111
  }
@@ -151,7 +132,9 @@ class FTPCommands {
151
132
  if (!commandSent) {
152
133
  commandSent = true;
153
134
  this.client._debug(`Data connection established for streaming download`);
154
- this.connection.sendCommand(`RETR ${remotePath}`, true).catch(reject);
135
+ // Just send command, don't wait for completion
136
+ this.client.socket.write(`RETR ${remotePath}\r\n`);
137
+ this.client._commandCount++;
155
138
  }
156
139
  });
157
140
 
@@ -166,33 +149,11 @@ class FTPCommands {
166
149
  });
167
150
 
168
151
  this.client.dataSocket.on('close', () => {
169
- // Wait for final 226 response
170
- const finalHandler = (line) => {
171
- const code = parseInt(line.substring(0, 3));
172
- if (code === 226 || code === 250) {
173
- this.client.removeListener('response', finalHandler);
174
- writeStream.end();
175
- this.client._debug(`Streaming download completed: ${totalBytes} bytes`);
176
- resolve(totalBytes);
177
- } else if (code >= 400) {
178
- this.client.removeListener('response', finalHandler);
179
- writeStream.end();
180
- reject(new Error(`Download failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
181
- }
182
- };
183
- this.client.on('response', finalHandler);
184
-
185
- // Timeout if no response
186
- setTimeout(() => {
187
- this.client.removeListener('response', finalHandler);
188
- if (totalBytes > 0) {
189
- writeStream.end();
190
- resolve(totalBytes);
191
- } else {
192
- writeStream.end();
193
- reject(new Error('Download timeout'));
194
- }
195
- }, this.client.timeout || 5000);
152
+ // Streaming complete
153
+ writeStream.end();
154
+ this.client._debug(`Streaming download completed: ${totalBytes} bytes`);
155
+ // Small delay to let 226 response arrive before next command
156
+ setTimeout(() => resolve(totalBytes), 10);
196
157
  });
197
158
  });
198
159
  }
@@ -213,7 +174,9 @@ class FTPCommands {
213
174
  this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
214
175
  if (!commandSent) {
215
176
  commandSent = true;
216
- this.connection.sendCommand(`LIST ${path}`, true).catch(reject);
177
+ // Just send command, don't wait for completion
178
+ this.client.socket.write(`LIST ${path}\r\n`);
179
+ this.client._commandCount++;
217
180
  }
218
181
  });
219
182
 
@@ -224,21 +187,9 @@ class FTPCommands {
224
187
  this.client.dataSocket.on('error', reject);
225
188
 
226
189
  this.client.dataSocket.on('close', () => {
227
- // Wait for final 226 response
228
- const finalHandler = (line) => {
229
- const code = parseInt(line.substring(0, 3));
230
- if (code === 226 || code === 250) {
231
- this.client.removeListener('response', finalHandler);
232
- resolve(Buffer.concat(chunks).toString('utf8'));
233
- }
234
- };
235
- this.client.on('response', finalHandler);
236
-
237
- // Timeout fallback
238
- setTimeout(() => {
239
- this.client.removeListener('response', finalHandler);
240
- resolve(Buffer.concat(chunks).toString('utf8'));
241
- }, this.client.timeout || 3000);
190
+ // Data transfer complete, resolve immediately
191
+ // Small delay to let 226 response arrive before next command
192
+ setTimeout(() => resolve(Buffer.concat(chunks).toString('utf8')), 10);
242
193
  });
243
194
  });
244
195
  }
@@ -280,6 +231,57 @@ class FTPCommands {
280
231
  await this.connection.sendCommand(`DELE ${path}`);
281
232
  }
282
233
 
234
+ /**
235
+ * Remove directory
236
+ * @param {string} path - Directory path
237
+ * @param {boolean} recursive - Delete all contents recursively (default: false)
238
+ * @returns {Promise<void>}
239
+ */
240
+ async removeDir(path, recursive = false) {
241
+ if (!recursive) {
242
+ // Remove empty directory only
243
+ await this.connection.sendCommand(`RMD ${path}`);
244
+ return;
245
+ }
246
+
247
+ // Recursive delete - get contents and delete everything
248
+ try {
249
+ const listing = await this.list(path);
250
+ const lines = listing.split('\n').filter(line => line.trim());
251
+
252
+ // Process each line - faster than listDetailed
253
+ for (const line of lines) {
254
+ // Skip . and .. and empty lines
255
+ if (!line || line.includes(' .') || line.includes(' ..')) continue;
256
+
257
+ // Extract filename (last part of line)
258
+ const parts = line.trim().split(/\s+/);
259
+ const name = parts[parts.length - 1];
260
+ if (name === '.' || name === '..') continue;
261
+
262
+ const fullPath = `${path}/${name}`.replace(/\/+/g, '/');
263
+ const isDir = line.startsWith('d');
264
+
265
+ if (isDir) {
266
+ // Directory - recurse
267
+ await this.removeDir(fullPath, true);
268
+ } else {
269
+ // File - delete
270
+ try {
271
+ await this.delete(fullPath);
272
+ } catch (err) {
273
+ this.client._debug(`Could not delete file ${fullPath}: ${err.message}`);
274
+ }
275
+ }
276
+ }
277
+
278
+ // Remove the now-empty directory
279
+ await this.connection.sendCommand(`RMD ${path}`);
280
+ } catch (err) {
281
+ throw new Error(`Failed to remove directory ${path}: ${err.message}`);
282
+ }
283
+ }
284
+
283
285
  /**
284
286
  * Rename file
285
287
  * @param {string} from - Current name
@@ -406,6 +408,71 @@ class FTPCommands {
406
408
  const response = await this.connection.sendCommand(`MDTM ${path}`);
407
409
  return parseMdtmResponse(response.message);
408
410
  }
411
+
412
+ /**
413
+ * Change file permissions (Unix/Linux servers only)
414
+ * @param {string} path - File or directory path
415
+ * @param {string|number} mode - Permissions (e.g., '755', 0755, or 'rwxr-xr-x')
416
+ * @returns {Promise<void>}
417
+ */
418
+ async chmod(path, mode) {
419
+ // Convert numeric mode to octal string if needed
420
+ const modeStr = typeof mode === 'number' ? mode.toString(8) : String(mode).replace(/[^0-7]/g, '');
421
+
422
+ if (!/^[0-7]{3,4}$/.test(modeStr)) {
423
+ throw new Error(`Invalid chmod mode: ${mode}. Use octal format like '755' or 0755`);
424
+ }
425
+
426
+ this.client._debug(`Changing permissions of ${path} to ${modeStr}`);
427
+ await this.connection.sendCommand(`SITE CHMOD ${modeStr} ${path}`);
428
+ }
429
+
430
+ /**
431
+ * Execute a SITE command (server-specific commands)
432
+ * @param {string} command - SITE command to execute (without 'SITE' prefix)
433
+ * @returns {Promise<Object>}
434
+ */
435
+ async site(command) {
436
+ this.client._debug(`Executing SITE command: ${command}`);
437
+ return await this.connection.sendCommand(`SITE ${command}`);
438
+ }
439
+
440
+ /**
441
+ * Parse directory listing into structured objects
442
+ * @param {string} path - Directory path
443
+ * @returns {Promise<Array>} Array of file/directory objects
444
+ */
445
+ async listDetailed(path = '.') {
446
+ const listing = await this.list(path);
447
+ const lines = listing.split('\n').filter(line => line.trim() && !line.startsWith('total'));
448
+
449
+ return lines.map(line => {
450
+ // Parse Unix-style LIST format
451
+ const match = line.match(/^([drwxlst-]{10})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+\s+\S+\s+\S+)\s+(.+)$/);
452
+
453
+ if (match) {
454
+ const [, perms, links, owner, group, size, date, name] = match;
455
+ return {
456
+ name,
457
+ type: perms[0] === 'd' ? 'directory' : (perms[0] === 'l' ? 'symlink' : 'file'),
458
+ permissions: perms,
459
+ owner,
460
+ group,
461
+ size: parseInt(size),
462
+ date,
463
+ raw: line
464
+ };
465
+ }
466
+
467
+ // Fallback for non-standard formats - try to extract name
468
+ const parts = line.trim().split(/\s+/);
469
+ return {
470
+ name: parts[parts.length - 1],
471
+ type: 'unknown',
472
+ raw: line
473
+ };
474
+ }).filter(item => item.name && item.name !== '.' && item.name !== '..');
475
+ }
409
476
  }
410
477
 
411
478
  module.exports = FTPCommands;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molex-ftp-client",
3
- "version": "2.1.0",
3
+ "version": "2.2.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": {
@@ -0,0 +1,235 @@
1
+ const FTPClient = require('./index.js');
2
+
3
+ // Test configuration
4
+ const CONFIG = {
5
+ host: '',
6
+ port: 21,
7
+ user: '',
8
+ password: ''
9
+ };
10
+
11
+ const TEST_DIR = '/molex-ftp-testing';
12
+
13
+ // Test runner
14
+ async function runTests() {
15
+ const client = new FTPClient({
16
+ debug: true,
17
+ timeout: 30000
18
+ });
19
+
20
+ console.log('\n🧪 Starting FTP Client Comprehensive Test Suite\n');
21
+ console.log('=' .repeat(60));
22
+
23
+ try {
24
+ // Test 1: Connect
25
+ console.log('\n✅ TEST 1: Connect to FTP server');
26
+ await client.connect(CONFIG);
27
+ console.log(` Connected to ${CONFIG.host}:${CONFIG.port}`);
28
+
29
+ // Test 2: Get current directory
30
+ console.log('\n✅ TEST 2: Get current working directory');
31
+ const currentDir = await client.pwd();
32
+ console.log(` Current directory: ${currentDir}`);
33
+
34
+ // Test 3: Create test directory
35
+ console.log(`\n✅ TEST 3: Create test directory ${TEST_DIR}`);
36
+ try {
37
+ await client.mkdir(TEST_DIR);
38
+ console.log(` Created ${TEST_DIR}`);
39
+ } catch (err) {
40
+ console.log(` Directory already exists or error: ${err.message}`);
41
+ }
42
+
43
+ // Test 4: Change to test directory
44
+ console.log(`\n✅ TEST 4: Change to test directory`);
45
+ await client.cd(TEST_DIR);
46
+ const newDir = await client.pwd();
47
+ console.log(` Changed to: ${newDir}`);
48
+
49
+ // Test 5: Upload a text file
50
+ console.log('\n✅ TEST 5: Upload text file');
51
+ const testContent = 'Hello from molex-ftp-client!\nTimestamp: ' + new Date().toISOString();
52
+ await client.upload(testContent, 'test-file.txt');
53
+ console.log(' Uploaded test-file.txt');
54
+
55
+ // Test 6: Upload with auto-directory creation
56
+ console.log('\n✅ TEST 6: Upload with auto-directory creation');
57
+ await client.upload('Nested file content', 'subdir/nested/deep.txt', true);
58
+ console.log(' Uploaded subdir/nested/deep.txt (created directories)');
59
+
60
+ // Test 7: Check if file exists
61
+ console.log('\n✅ TEST 7: Check if file exists');
62
+ const exists = await client.exists('test-file.txt');
63
+ console.log(` test-file.txt exists: ${exists}`);
64
+
65
+ // Test 8: Get file stats
66
+ console.log('\n✅ TEST 8: Get file statistics');
67
+ const stat = await client.stat('test-file.txt');
68
+ console.log(` Stats:`, stat);
69
+
70
+ // Test 9: Get file size
71
+ console.log('\n✅ TEST 9: Get file size');
72
+ const size = await client.size('test-file.txt');
73
+ console.log(` Size: ${size} bytes`);
74
+
75
+ // Test 10: Download file
76
+ console.log('\n✅ TEST 10: Download file');
77
+ const downloaded = await client.download('test-file.txt');
78
+ console.log(` Downloaded ${downloaded.length} bytes`);
79
+ console.log(` Content: ${downloaded.toString().substring(0, 50)}...`);
80
+
81
+ // Test 11: List directory
82
+ console.log('\n✅ TEST 11: List directory (raw)');
83
+ const listing = await client.list('.');
84
+ console.log(` Listing:\n${listing.split('\n').slice(0, 5).join('\n')}`);
85
+
86
+ // Test 12: List directory detailed
87
+ console.log('\n✅ TEST 12: List directory (detailed)');
88
+ const detailedListing = await client.listDetailed('.');
89
+ console.log(` Found ${detailedListing.length} items:`);
90
+ detailedListing.forEach(item => {
91
+ console.log(` - ${item.type === 'directory' ? '📁' : '📄'} ${item.name} (${item.permissions || 'N/A'})`);
92
+ });
93
+
94
+ // Test 13: Rename file
95
+ console.log('\n✅ TEST 13: Rename file');
96
+ await client.rename('test-file.txt', 'renamed-file.txt');
97
+ console.log(' Renamed test-file.txt → renamed-file.txt');
98
+
99
+ // Test 14: Get modified time
100
+ console.log('\n✅ TEST 14: Get file modification time');
101
+ try {
102
+ const modTime = await client.modifiedTime('renamed-file.txt');
103
+ console.log(` Modified: ${modTime}`);
104
+ } catch (err) {
105
+ console.log(` Not supported or error: ${err.message}`);
106
+ }
107
+
108
+ // Test 15: Create multiple directories
109
+ console.log('\n✅ TEST 15: Create nested directories');
110
+ await client.mkdir('test-dir-1');
111
+ await client.mkdir('test-dir-2');
112
+ await client.ensureDir('deep/nested/structure');
113
+ console.log(' Created test-dir-1, test-dir-2, and deep/nested/structure');
114
+
115
+ // Test 16: Upload file with Buffer
116
+ console.log('\n✅ TEST 16: Upload file using Buffer');
117
+ const buffer = Buffer.from('Binary content test', 'utf8');
118
+ await client.upload(buffer, 'test-dir-1/buffer-test.bin');
119
+ console.log(' Uploaded buffer-test.bin to test-dir-1');
120
+
121
+ // Test 17: Download with stream
122
+ console.log('\n✅ TEST 17: Download using stream');
123
+ const { PassThrough } = require('stream');
124
+ const stream = new PassThrough();
125
+ const chunks = [];
126
+ stream.on('data', chunk => chunks.push(chunk));
127
+ const bytes = await client.downloadStream('renamed-file.txt', stream);
128
+ console.log(` Downloaded ${bytes} bytes via stream`);
129
+
130
+ // Test 17b: Large file upload/download performance test
131
+ console.log('\n✅ TEST 17b: Large file performance test');
132
+ const largeData = Buffer.alloc(1024 * 1024, 'x'); // 1MB file
133
+ console.log(` Uploading 1MB file...`);
134
+ const uploadStart = Date.now();
135
+ await client.upload(largeData, 'large-test.bin');
136
+ const uploadTime = Date.now() - uploadStart;
137
+ console.log(` Upload: ${(largeData.length / uploadTime / 1024).toFixed(2)} MB/s (${uploadTime}ms)`);
138
+
139
+ console.log(` Downloading 1MB file...`);
140
+ const downloadStart = Date.now();
141
+ const largeDownload = await client.download('large-test.bin');
142
+ const downloadTime = Date.now() - downloadStart;
143
+ console.log(` Download: ${(largeDownload.length / downloadTime / 1024).toFixed(2)} MB/s (${downloadTime}ms)`);
144
+
145
+ await client.delete('large-test.bin');
146
+ console.log(` Cleaned up large-test.bin`);
147
+
148
+ // Test 18: Try chmod (may not work on all servers)
149
+ console.log('\n✅ TEST 18: Change file permissions (chmod)');
150
+ try {
151
+ await client.chmod('renamed-file.txt', '644');
152
+ console.log(' Changed permissions to 644');
153
+ } catch (err) {
154
+ console.log(` Not supported or error: ${err.message}`);
155
+ }
156
+
157
+ // Test 19: Execute SITE command
158
+ console.log('\n✅ TEST 19: Execute SITE command');
159
+ try {
160
+ const response = await client.site('HELP');
161
+ console.log(` SITE HELP response: ${response.message.substring(0, 50)}...`);
162
+ } catch (err) {
163
+ console.log(` Not supported or error: ${err.message}`);
164
+ }
165
+
166
+ // Test 20: Check connection state
167
+ console.log('\n✅ TEST 20: Get client state');
168
+ const state = client.getStats();
169
+ console.log(` Connected: ${state.connected}, Authenticated: ${state.authenticated}`);
170
+ console.log(` Commands executed: ${state.commandCount}`);
171
+
172
+ // Test 21: Delete single file
173
+ console.log('\n✅ TEST 21: Delete single file');
174
+ await client.delete('test-dir-1/buffer-test.bin');
175
+ console.log(' Deleted buffer-test.bin');
176
+
177
+ // Test 22: Remove empty directory
178
+ console.log('\n✅ TEST 22: Remove empty directory');
179
+ await client.removeDir('test-dir-1');
180
+ console.log(' Removed test-dir-1');
181
+
182
+ // Test 23: Remove directory recursively
183
+ console.log('\n✅ TEST 23: Remove directory recursively');
184
+ await client.removeDir('subdir', true);
185
+ console.log(' Recursively removed subdir and all contents');
186
+
187
+ // Test 24: Final directory listing
188
+ console.log('\n✅ TEST 24: Final directory listing');
189
+ const finalListing = await client.listDetailed('.');
190
+ console.log(` Items remaining: ${finalListing.length}`);
191
+
192
+ // Cleanup: Remove test directory
193
+ console.log('\n🧹 CLEANUP: Removing test directory');
194
+ await client.cd('..');
195
+ await client.removeDir(TEST_DIR, true);
196
+ console.log(` Removed ${TEST_DIR} and all contents`);
197
+
198
+ // Close connection
199
+ console.log('\n✅ Closing connection');
200
+ await client.close();
201
+ console.log(' Connection closed');
202
+
203
+ console.log('\n' + '='.repeat(60));
204
+ console.log('🎉 ALL TESTS PASSED!');
205
+ console.log('='.repeat(60) + '\n');
206
+
207
+ } catch (err) {
208
+ console.error('\n❌ TEST FAILED:', err.message);
209
+ console.error('Stack:', err.stack);
210
+
211
+ // Try to cleanup and close
212
+ try {
213
+ console.log('\n🧹 Attempting cleanup...');
214
+ await client.cd('/');
215
+ try {
216
+ await client.removeDir(TEST_DIR, true);
217
+ console.log(' Cleaned up test directory');
218
+ } catch (e) {
219
+ console.log(' Could not clean up:', e.message);
220
+ }
221
+ await client.close();
222
+ } catch (e) {
223
+ console.log(' Could not close connection:', e.message);
224
+ }
225
+
226
+ process.exit(1);
227
+ }
228
+ }
229
+
230
+ // Run the tests
231
+ console.log('Starting comprehensive FTP client tests...');
232
+ runTests().catch(err => {
233
+ console.error('Fatal error:', err);
234
+ process.exit(1);
235
+ });
package/benchmark.js DELETED
@@ -1,86 +0,0 @@
1
- /**
2
- * Performance benchmark for FTP client
3
- * Compare different performance presets
4
- */
5
-
6
- const FTPClient = require('./index.js');
7
-
8
- async function benchmark() {
9
- const testData = 'x'.repeat(50000); // 50KB test data
10
- const presets = ['DEFAULT', 'LOW_LATENCY', 'HIGH_THROUGHPUT', 'BALANCED'];
11
-
12
- console.log('\n=== FTP Client Performance Benchmark ===\n');
13
- console.log('Test data size:', testData.length, 'bytes\n');
14
-
15
- for (const preset of presets) {
16
- console.log(`Testing ${preset} preset...`);
17
-
18
- const client = new FTPClient({
19
- debug: false,
20
- timeout: 60000,
21
- performancePreset: preset
22
- });
23
-
24
- try {
25
- // Connect
26
- const connectStart = Date.now();
27
- await client.connect({
28
- host: 'ftp.example.com',
29
- port: 21,
30
- user: 'username',
31
- password: 'password'
32
- });
33
- const connectTime = Date.now() - connectStart;
34
-
35
- // Upload
36
- const uploadStart = Date.now();
37
- await client.upload(testData, '/benchmark-test.txt', true);
38
- const uploadTime = Date.now() - uploadStart;
39
-
40
- // Download
41
- const downloadStart = Date.now();
42
- const data = await client.download('/benchmark-test.txt');
43
- const downloadTime = Date.now() - downloadStart;
44
-
45
- // Cleanup
46
- await client.delete('/benchmark-test.txt');
47
- await client.close();
48
-
49
- console.log(` Connect: ${connectTime}ms`);
50
- console.log(` Upload: ${uploadTime}ms`);
51
- console.log(` Download: ${downloadTime}ms`);
52
- console.log(` Total: ${connectTime + uploadTime + downloadTime}ms`);
53
- console.log('');
54
-
55
- } catch (err) {
56
- console.error(` Error: ${err.message}\n`);
57
- await client.close();
58
- }
59
-
60
- // Wait between tests
61
- await new Promise(resolve => setTimeout(resolve, 1000));
62
- }
63
-
64
- console.log('=== Benchmark Complete ===\n');
65
- console.log('Recommendation:');
66
- console.log(' - Use LOW_LATENCY for small files (< 1MB)');
67
- console.log(' - Use HIGH_THROUGHPUT for large files (> 10MB)');
68
- console.log(' - Use BALANCED for mixed workloads\n');
69
- }
70
-
71
- // Only run if called directly
72
- if (require.main === module) {
73
- console.log('\n⚠️ Update the benchmark() function with your FTP credentials to test.\n');
74
- console.log('Example:');
75
- console.log(' await client.connect({');
76
- console.log(' host: "ftp.example.com",');
77
- console.log(' port: 21,');
78
- console.log(' user: "username",');
79
- console.log(' password: "password"');
80
- console.log(' });\n');
81
-
82
- // Uncomment to run benchmark:
83
- // benchmark();
84
- }
85
-
86
- module.exports = benchmark;