molex-ftp-client 2.1.0 → 2.2.2
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 +18 -0
- package/README.md +59 -10
- package/lib/FTPClient.js +62 -0
- package/lib/commands.js +155 -88
- package/lib/connection.js +9 -0
- package/package.json +4 -1
- package/test-comprehensive.js +235 -0
- package/benchmark.js +0 -86
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
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/molex-ftp-client)
|
|
4
|
+
[](https://www.npmjs.com/package/molex-ftp-client)
|
|
5
|
+
[](https://opensource.org/licenses/ISC)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](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`
|
|
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
|
|
90
|
-
const bytes = await client.downloadStream('/remote.bin',
|
|
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
|
-
|
|
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
|
|
206
|
-
const bytes = await client.downloadStream('/backup.zip',
|
|
207
|
-
console.log(`
|
|
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
|
|
@@ -242,6 +253,26 @@ class FTPClient extends EventEmitter
|
|
|
242
253
|
};
|
|
243
254
|
}
|
|
244
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Get current client state for debugging
|
|
258
|
+
* @returns {Object}
|
|
259
|
+
*/
|
|
260
|
+
getState()
|
|
261
|
+
{
|
|
262
|
+
return {
|
|
263
|
+
connected: this.connected,
|
|
264
|
+
authenticated: this.authenticated,
|
|
265
|
+
host: this._connection?.host || null,
|
|
266
|
+
port: this._connection?.port || null,
|
|
267
|
+
user: this._connection?.user || null,
|
|
268
|
+
commandCount: this._commandCount,
|
|
269
|
+
lastCommand: this._lastCommand,
|
|
270
|
+
timeout: this.timeout,
|
|
271
|
+
debug: this.debug,
|
|
272
|
+
keepAlive: this.keepAlive
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
245
276
|
/**
|
|
246
277
|
* Check if connected and authenticated
|
|
247
278
|
* @returns {boolean}
|
|
@@ -261,6 +292,37 @@ class FTPClient extends EventEmitter
|
|
|
261
292
|
this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
262
293
|
}
|
|
263
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Change file permissions (Unix/Linux servers only)
|
|
297
|
+
* @param {string} path - File or directory path
|
|
298
|
+
* @param {string|number} mode - Permissions (e.g., '755', 0755)
|
|
299
|
+
* @returns {Promise<void>}
|
|
300
|
+
*/
|
|
301
|
+
async chmod(path, mode)
|
|
302
|
+
{
|
|
303
|
+
return this._commands.chmod(path, mode);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Execute a SITE command (server-specific commands)
|
|
308
|
+
* @param {string} command - SITE command to execute
|
|
309
|
+
* @returns {Promise<Object>}
|
|
310
|
+
*/
|
|
311
|
+
async site(command)
|
|
312
|
+
{
|
|
313
|
+
return this._commands.site(command);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get detailed directory listing with permissions, owner, size, etc.
|
|
318
|
+
* @param {string} path - Directory path
|
|
319
|
+
* @returns {Promise<Array>}
|
|
320
|
+
*/
|
|
321
|
+
async listDetailed(path = '.')
|
|
322
|
+
{
|
|
323
|
+
return this._commands.listDetailed(path);
|
|
324
|
+
}
|
|
325
|
+
|
|
264
326
|
/**
|
|
265
327
|
* Close connection
|
|
266
328
|
* @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
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
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/lib/connection.js
CHANGED
|
@@ -18,6 +18,11 @@ class FTPConnection {
|
|
|
18
18
|
* @returns {Promise<void>}
|
|
19
19
|
*/
|
|
20
20
|
async connect({ host, port = 21, user = 'anonymous', password = 'anonymous@' }) {
|
|
21
|
+
// Store connection parameters for getState()
|
|
22
|
+
this.host = host;
|
|
23
|
+
this.port = port;
|
|
24
|
+
this.user = user;
|
|
25
|
+
|
|
21
26
|
this.client._debug(`Connecting to ${host}:${port} as ${user}`);
|
|
22
27
|
|
|
23
28
|
return new Promise((resolve, reject) => {
|
|
@@ -171,6 +176,10 @@ class FTPConnection {
|
|
|
171
176
|
}
|
|
172
177
|
this.client.connected = false;
|
|
173
178
|
this.client.authenticated = false;
|
|
179
|
+
// Reset connection parameters
|
|
180
|
+
this.host = null;
|
|
181
|
+
this.port = null;
|
|
182
|
+
this.user = null;
|
|
174
183
|
this.client._debug('Connection closed');
|
|
175
184
|
}
|
|
176
185
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "molex-ftp-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
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": {
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"homepage": "https://github.com/tonywied17/molex-ftp-client#readme",
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=12.0.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -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;
|