molex-ftp-client 2.0.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 +34 -3
- package/README.md +138 -397
- package/lib/FTPClient.js +54 -5
- package/lib/commands.js +183 -111
- package/lib/connection.js +2 -7
- package/lib/performance.js +16 -67
- package/package.json +1 -1
- package/test-comprehensive.js +235 -0
- package/benchmark.js +0 -86
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
|
|
@@ -168,6 +165,17 @@ class FTPClient extends EventEmitter
|
|
|
168
165
|
return this._commands.delete(path);
|
|
169
166
|
}
|
|
170
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
|
+
|
|
171
179
|
/**
|
|
172
180
|
* Rename file
|
|
173
181
|
* @param {string} from - Current name
|
|
@@ -199,6 +207,16 @@ class FTPClient extends EventEmitter
|
|
|
199
207
|
return this._commands.exists(path);
|
|
200
208
|
}
|
|
201
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Get file/directory information
|
|
212
|
+
* @param {string} path - Path to check
|
|
213
|
+
* @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
|
|
214
|
+
*/
|
|
215
|
+
async stat(path)
|
|
216
|
+
{
|
|
217
|
+
return this._commands.stat(path);
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
/**
|
|
203
221
|
* Ensure directory exists, creating it if necessary
|
|
204
222
|
* @param {string} dirPath - Directory or file path to ensure exists
|
|
@@ -254,6 +272,37 @@ class FTPClient extends EventEmitter
|
|
|
254
272
|
this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
255
273
|
}
|
|
256
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
|
+
|
|
257
306
|
/**
|
|
258
307
|
* Close connection
|
|
259
308
|
* @returns {Promise<void>}
|
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,15 +31,14 @@ 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;
|
|
42
38
|
this.client._debug(`Data connection established for upload`);
|
|
43
|
-
|
|
39
|
+
// Just send command, don't wait for completion
|
|
40
|
+
this.client.socket.write(`STOR ${remotePath}\r\n`);
|
|
41
|
+
this.client._commandCount++;
|
|
44
42
|
|
|
45
43
|
// Write data to data socket
|
|
46
44
|
this.client.dataSocket.write(buffer);
|
|
@@ -51,25 +49,10 @@ class FTPCommands {
|
|
|
51
49
|
this.client.dataSocket.on('error', reject);
|
|
52
50
|
|
|
53
51
|
this.client.dataSocket.on('close', () => {
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.client.removeListener('response', finalHandler);
|
|
59
|
-
this.client._debug(`Upload completed successfully`);
|
|
60
|
-
resolve();
|
|
61
|
-
} else if (code >= 400) {
|
|
62
|
-
this.client.removeListener('response', finalHandler);
|
|
63
|
-
reject(new Error(`Upload failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
this.client.on('response', finalHandler);
|
|
67
|
-
|
|
68
|
-
// Timeout if no response
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
this.client.removeListener('response', finalHandler);
|
|
71
|
-
resolve();
|
|
72
|
-
}, 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);
|
|
73
56
|
});
|
|
74
57
|
});
|
|
75
58
|
}
|
|
@@ -89,49 +72,40 @@ class FTPCommands {
|
|
|
89
72
|
return new Promise((resolve, reject) => {
|
|
90
73
|
const chunks = [];
|
|
91
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
|
+
};
|
|
92
85
|
|
|
93
|
-
this.client.dataSocket =
|
|
94
|
-
|
|
95
|
-
optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
|
|
96
|
-
|
|
86
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
97
87
|
// Send RETR command to start download (expects 150, then 226)
|
|
98
88
|
if (!commandSent) {
|
|
99
89
|
commandSent = true;
|
|
100
90
|
this.client._debug(`Data connection established for download`);
|
|
101
|
-
|
|
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++;
|
|
102
94
|
}
|
|
103
95
|
});
|
|
104
96
|
|
|
105
97
|
this.client.dataSocket.on('data', (chunk) => {
|
|
106
98
|
chunks.push(chunk);
|
|
107
|
-
this.client._debug(`Received ${chunk.length} bytes`);
|
|
108
99
|
});
|
|
109
100
|
|
|
110
101
|
this.client.dataSocket.on('error', reject);
|
|
111
102
|
|
|
112
103
|
this.client.dataSocket.on('close', () => {
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const result = Buffer.concat(chunks);
|
|
119
|
-
this.client._debug(`Download completed: ${result.length} bytes`);
|
|
120
|
-
resolve(result);
|
|
121
|
-
} else if (code >= 400) {
|
|
122
|
-
this.client.removeListener('response', finalHandler);
|
|
123
|
-
reject(new Error(`Download failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
this.client.on('response', finalHandler);
|
|
127
|
-
|
|
128
|
-
// Timeout if no response
|
|
129
|
-
setTimeout(() => {
|
|
130
|
-
this.client.removeListener('response', finalHandler);
|
|
131
|
-
if (chunks.length > 0) {
|
|
132
|
-
resolve(Buffer.concat(chunks));
|
|
133
|
-
}
|
|
134
|
-
}, 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);
|
|
135
109
|
});
|
|
136
110
|
});
|
|
137
111
|
}
|
|
@@ -154,14 +128,13 @@ class FTPCommands {
|
|
|
154
128
|
let totalBytes = 0;
|
|
155
129
|
let commandSent = false;
|
|
156
130
|
|
|
157
|
-
this.client.dataSocket =
|
|
158
|
-
|
|
159
|
-
optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
|
|
160
|
-
|
|
131
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
161
132
|
if (!commandSent) {
|
|
162
133
|
commandSent = true;
|
|
163
134
|
this.client._debug(`Data connection established for streaming download`);
|
|
164
|
-
|
|
135
|
+
// Just send command, don't wait for completion
|
|
136
|
+
this.client.socket.write(`RETR ${remotePath}\r\n`);
|
|
137
|
+
this.client._commandCount++;
|
|
165
138
|
}
|
|
166
139
|
});
|
|
167
140
|
|
|
@@ -176,33 +149,11 @@ class FTPCommands {
|
|
|
176
149
|
});
|
|
177
150
|
|
|
178
151
|
this.client.dataSocket.on('close', () => {
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
writeStream.end();
|
|
185
|
-
this.client._debug(`Streaming download completed: ${totalBytes} bytes`);
|
|
186
|
-
resolve(totalBytes);
|
|
187
|
-
} else if (code >= 400) {
|
|
188
|
-
this.client.removeListener('response', finalHandler);
|
|
189
|
-
writeStream.end();
|
|
190
|
-
reject(new Error(`Download failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
this.client.on('response', finalHandler);
|
|
194
|
-
|
|
195
|
-
// Timeout if no response
|
|
196
|
-
setTimeout(() => {
|
|
197
|
-
this.client.removeListener('response', finalHandler);
|
|
198
|
-
if (totalBytes > 0) {
|
|
199
|
-
writeStream.end();
|
|
200
|
-
resolve(totalBytes);
|
|
201
|
-
} else {
|
|
202
|
-
writeStream.end();
|
|
203
|
-
reject(new Error('Download timeout'));
|
|
204
|
-
}
|
|
205
|
-
}, 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);
|
|
206
157
|
});
|
|
207
158
|
});
|
|
208
159
|
}
|
|
@@ -220,13 +171,12 @@ class FTPCommands {
|
|
|
220
171
|
const chunks = [];
|
|
221
172
|
let commandSent = false;
|
|
222
173
|
|
|
223
|
-
this.client.dataSocket =
|
|
224
|
-
|
|
225
|
-
optimizeSocket(this.client.dataSocket, this.client.performanceOptions);
|
|
226
|
-
|
|
174
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
227
175
|
if (!commandSent) {
|
|
228
176
|
commandSent = true;
|
|
229
|
-
|
|
177
|
+
// Just send command, don't wait for completion
|
|
178
|
+
this.client.socket.write(`LIST ${path}\r\n`);
|
|
179
|
+
this.client._commandCount++;
|
|
230
180
|
}
|
|
231
181
|
});
|
|
232
182
|
|
|
@@ -237,21 +187,9 @@ class FTPCommands {
|
|
|
237
187
|
this.client.dataSocket.on('error', reject);
|
|
238
188
|
|
|
239
189
|
this.client.dataSocket.on('close', () => {
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (code === 226 || code === 250) {
|
|
244
|
-
this.client.removeListener('response', finalHandler);
|
|
245
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
this.client.on('response', finalHandler);
|
|
249
|
-
|
|
250
|
-
// Timeout fallback
|
|
251
|
-
setTimeout(() => {
|
|
252
|
-
this.client.removeListener('response', finalHandler);
|
|
253
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
254
|
-
}, 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);
|
|
255
193
|
});
|
|
256
194
|
});
|
|
257
195
|
}
|
|
@@ -293,6 +231,57 @@ class FTPCommands {
|
|
|
293
231
|
await this.connection.sendCommand(`DELE ${path}`);
|
|
294
232
|
}
|
|
295
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
|
+
|
|
296
285
|
/**
|
|
297
286
|
* Rename file
|
|
298
287
|
* @param {string} from - Current name
|
|
@@ -321,10 +310,20 @@ class FTPCommands {
|
|
|
321
310
|
* @returns {Promise<boolean>}
|
|
322
311
|
*/
|
|
323
312
|
async exists(path) {
|
|
313
|
+
const info = await this.stat(path);
|
|
314
|
+
return info.exists;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get file/directory information
|
|
319
|
+
* @param {string} path - Path to check
|
|
320
|
+
* @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
|
|
321
|
+
*/
|
|
322
|
+
async stat(path) {
|
|
324
323
|
try {
|
|
325
324
|
// First try SIZE command (works for files)
|
|
326
|
-
await this.size(path);
|
|
327
|
-
return true;
|
|
325
|
+
const size = await this.size(path);
|
|
326
|
+
return { exists: true, size, isFile: true, isDirectory: false };
|
|
328
327
|
} catch (err) {
|
|
329
328
|
// SIZE failed, might be a directory - try CWD
|
|
330
329
|
try {
|
|
@@ -332,10 +331,18 @@ class FTPCommands {
|
|
|
332
331
|
await this.cd(path);
|
|
333
332
|
// Restore original directory
|
|
334
333
|
await this.cd(currentDir);
|
|
335
|
-
return true;
|
|
334
|
+
return { exists: true, size: null, isFile: false, isDirectory: true };
|
|
336
335
|
} catch (cdErr) {
|
|
337
|
-
// Both SIZE and CWD failed -
|
|
338
|
-
|
|
336
|
+
// Both SIZE and CWD failed - try listing parent directory
|
|
337
|
+
try {
|
|
338
|
+
const dir = getParentDir(path);
|
|
339
|
+
const basename = path.split('/').pop();
|
|
340
|
+
const listing = await this.list(dir);
|
|
341
|
+
const found = listing.split('\n').some(line => line.includes(basename));
|
|
342
|
+
return { exists: found, size: null, isFile: null, isDirectory: null };
|
|
343
|
+
} catch (listErr) {
|
|
344
|
+
return { exists: false, size: null, isFile: null, isDirectory: null };
|
|
345
|
+
}
|
|
339
346
|
}
|
|
340
347
|
}
|
|
341
348
|
}
|
|
@@ -401,6 +408,71 @@ class FTPCommands {
|
|
|
401
408
|
const response = await this.connection.sendCommand(`MDTM ${path}`);
|
|
402
409
|
return parseMdtmResponse(response.message);
|
|
403
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
|
+
}
|
|
404
476
|
}
|
|
405
477
|
|
|
406
478
|
module.exports = FTPCommands;
|
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
|
};
|