molex-ftp-client 1.2.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +109 -331
- package/benchmark.js +86 -0
- package/lib/FTPClient.js +103 -46
- package/lib/commands.js +132 -44
- package/lib/connection.js +11 -8
- package/lib/performance.js +29 -0
- package/lib/utils.js +0 -19
- package/package.json +1 -1
package/benchmark.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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;
|
package/lib/FTPClient.js
CHANGED
|
@@ -5,27 +5,29 @@ const FTPCommands = require('./commands');
|
|
|
5
5
|
/**
|
|
6
6
|
* Lightweight FTP Client using native Node.js TCP sockets (net module)
|
|
7
7
|
*/
|
|
8
|
-
class FTPClient extends EventEmitter
|
|
9
|
-
|
|
8
|
+
class FTPClient extends EventEmitter
|
|
9
|
+
{
|
|
10
|
+
constructor(options = {})
|
|
11
|
+
{
|
|
10
12
|
super();
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
// Connection state
|
|
13
15
|
this.socket = null;
|
|
14
16
|
this.dataSocket = null;
|
|
15
17
|
this.buffer = '';
|
|
16
18
|
this.connected = false;
|
|
17
19
|
this.authenticated = false;
|
|
18
|
-
|
|
20
|
+
|
|
19
21
|
// Configuration
|
|
20
22
|
this.debug = options.debug || false;
|
|
21
23
|
this.timeout = options.timeout || 30000;
|
|
22
24
|
this.keepAlive = options.keepAlive !== false;
|
|
23
25
|
this._log = options.logger || console.log;
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
// Statistics
|
|
26
28
|
this._commandCount = 0;
|
|
27
29
|
this._lastCommand = null;
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
// Initialize subsystems
|
|
30
32
|
this._connection = new FTPConnection(this);
|
|
31
33
|
this._commands = new FTPCommands(this);
|
|
@@ -35,8 +37,10 @@ class FTPClient extends EventEmitter {
|
|
|
35
37
|
* Log message if debug is enabled
|
|
36
38
|
* @private
|
|
37
39
|
*/
|
|
38
|
-
_debug(...args)
|
|
39
|
-
|
|
40
|
+
_debug(...args)
|
|
41
|
+
{
|
|
42
|
+
if (this.debug && this._log)
|
|
43
|
+
{
|
|
40
44
|
this._log('[FTP Debug]', ...args);
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -50,7 +54,12 @@ class FTPClient extends EventEmitter {
|
|
|
50
54
|
* @param {string} [options.password='anonymous@'] - Password
|
|
51
55
|
* @returns {Promise<void>}
|
|
52
56
|
*/
|
|
53
|
-
async connect(options)
|
|
57
|
+
async connect(options)
|
|
58
|
+
{
|
|
59
|
+
if (!options || !options.host)
|
|
60
|
+
{
|
|
61
|
+
throw new Error('Connection options with host are required');
|
|
62
|
+
}
|
|
54
63
|
return this._connection.connect(options);
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -58,10 +67,20 @@ class FTPClient extends EventEmitter {
|
|
|
58
67
|
* Upload file to FTP server
|
|
59
68
|
* @param {string|Buffer} data - File data
|
|
60
69
|
* @param {string} remotePath - Remote file path
|
|
70
|
+
* @param {boolean} ensureDir - Ensure parent directory exists (default: false)
|
|
61
71
|
* @returns {Promise<void>}
|
|
62
72
|
*/
|
|
63
|
-
async upload(data, remotePath)
|
|
64
|
-
|
|
73
|
+
async upload(data, remotePath, ensureDir = false)
|
|
74
|
+
{
|
|
75
|
+
if (!data)
|
|
76
|
+
{
|
|
77
|
+
throw new Error('Data is required for upload');
|
|
78
|
+
}
|
|
79
|
+
if (!remotePath)
|
|
80
|
+
{
|
|
81
|
+
throw new Error('Remote path is required for upload');
|
|
82
|
+
}
|
|
83
|
+
return this._commands.upload(data, remotePath, ensureDir);
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
/**
|
|
@@ -69,16 +88,41 @@ class FTPClient extends EventEmitter {
|
|
|
69
88
|
* @param {string} remotePath - Remote file path
|
|
70
89
|
* @returns {Promise<Buffer>}
|
|
71
90
|
*/
|
|
72
|
-
async download(remotePath)
|
|
91
|
+
async download(remotePath)
|
|
92
|
+
{
|
|
93
|
+
if (!remotePath)
|
|
94
|
+
{
|
|
95
|
+
throw new Error('Remote path is required for download');
|
|
96
|
+
}
|
|
73
97
|
return this._commands.download(remotePath);
|
|
74
98
|
}
|
|
75
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Download file from FTP server as a stream (memory efficient for large files)
|
|
102
|
+
* @param {string} remotePath - Remote file path
|
|
103
|
+
* @param {Stream} writeStream - Writable stream to pipe data to
|
|
104
|
+
* @returns {Promise<number>} - Total bytes transferred
|
|
105
|
+
*/
|
|
106
|
+
async downloadStream(remotePath, writeStream)
|
|
107
|
+
{
|
|
108
|
+
if (!remotePath)
|
|
109
|
+
{
|
|
110
|
+
throw new Error('Remote path is required for download');
|
|
111
|
+
}
|
|
112
|
+
if (!writeStream || typeof writeStream.write !== 'function')
|
|
113
|
+
{
|
|
114
|
+
throw new Error('Valid writable stream is required');
|
|
115
|
+
}
|
|
116
|
+
return this._commands.downloadStream(remotePath, writeStream);
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
/**
|
|
77
120
|
* List directory contents
|
|
78
121
|
* @param {string} [path='.'] - Directory path
|
|
79
122
|
* @returns {Promise<string>}
|
|
80
123
|
*/
|
|
81
|
-
async list(path = '.')
|
|
124
|
+
async list(path = '.')
|
|
125
|
+
{
|
|
82
126
|
return this._commands.list(path);
|
|
83
127
|
}
|
|
84
128
|
|
|
@@ -87,7 +131,8 @@ class FTPClient extends EventEmitter {
|
|
|
87
131
|
* @param {string} path - Directory path
|
|
88
132
|
* @returns {Promise<void>}
|
|
89
133
|
*/
|
|
90
|
-
async cd(path)
|
|
134
|
+
async cd(path)
|
|
135
|
+
{
|
|
91
136
|
return this._commands.cd(path);
|
|
92
137
|
}
|
|
93
138
|
|
|
@@ -95,7 +140,8 @@ class FTPClient extends EventEmitter {
|
|
|
95
140
|
* Get current working directory
|
|
96
141
|
* @returns {Promise<string>}
|
|
97
142
|
*/
|
|
98
|
-
async pwd()
|
|
143
|
+
async pwd()
|
|
144
|
+
{
|
|
99
145
|
return this._commands.pwd();
|
|
100
146
|
}
|
|
101
147
|
|
|
@@ -104,7 +150,8 @@ class FTPClient extends EventEmitter {
|
|
|
104
150
|
* @param {string} path - Directory path
|
|
105
151
|
* @returns {Promise<void>}
|
|
106
152
|
*/
|
|
107
|
-
async mkdir(path)
|
|
153
|
+
async mkdir(path)
|
|
154
|
+
{
|
|
108
155
|
return this._commands.mkdir(path);
|
|
109
156
|
}
|
|
110
157
|
|
|
@@ -113,7 +160,8 @@ class FTPClient extends EventEmitter {
|
|
|
113
160
|
* @param {string} path - File path
|
|
114
161
|
* @returns {Promise<void>}
|
|
115
162
|
*/
|
|
116
|
-
async delete(path)
|
|
163
|
+
async delete(path)
|
|
164
|
+
{
|
|
117
165
|
return this._commands.delete(path);
|
|
118
166
|
}
|
|
119
167
|
|
|
@@ -123,7 +171,8 @@ class FTPClient extends EventEmitter {
|
|
|
123
171
|
* @param {string} to - New name
|
|
124
172
|
* @returns {Promise<void>}
|
|
125
173
|
*/
|
|
126
|
-
async rename(from, to)
|
|
174
|
+
async rename(from, to)
|
|
175
|
+
{
|
|
127
176
|
return this._commands.rename(from, to);
|
|
128
177
|
}
|
|
129
178
|
|
|
@@ -132,7 +181,8 @@ class FTPClient extends EventEmitter {
|
|
|
132
181
|
* @param {string} path - File path
|
|
133
182
|
* @returns {Promise<number>}
|
|
134
183
|
*/
|
|
135
|
-
async size(path)
|
|
184
|
+
async size(path)
|
|
185
|
+
{
|
|
136
186
|
return this._commands.size(path);
|
|
137
187
|
}
|
|
138
188
|
|
|
@@ -141,38 +191,31 @@ class FTPClient extends EventEmitter {
|
|
|
141
191
|
* @param {string} path - File or directory path
|
|
142
192
|
* @returns {Promise<boolean>}
|
|
143
193
|
*/
|
|
144
|
-
async exists(path)
|
|
194
|
+
async exists(path)
|
|
195
|
+
{
|
|
145
196
|
return this._commands.exists(path);
|
|
146
197
|
}
|
|
147
198
|
|
|
148
199
|
/**
|
|
149
|
-
*
|
|
150
|
-
* @param {string}
|
|
151
|
-
* @
|
|
152
|
-
* @returns {Promise<void>}
|
|
153
|
-
*/
|
|
154
|
-
async ensureDir(dirPath, recursive = true) {
|
|
155
|
-
return this._commands.ensureDir(dirPath, recursive);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Ensure parent directory exists for a file path
|
|
160
|
-
* @param {string} filePath - File path
|
|
161
|
-
* @returns {Promise<void>}
|
|
200
|
+
* Get file/directory information
|
|
201
|
+
* @param {string} path - Path to check
|
|
202
|
+
* @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
|
|
162
203
|
*/
|
|
163
|
-
async
|
|
164
|
-
|
|
204
|
+
async stat(path)
|
|
205
|
+
{
|
|
206
|
+
return this._commands.stat(path);
|
|
165
207
|
}
|
|
166
208
|
|
|
167
209
|
/**
|
|
168
|
-
*
|
|
169
|
-
* @param {string
|
|
170
|
-
* @param {
|
|
171
|
-
* @param {boolean}
|
|
210
|
+
* Ensure directory exists, creating it if necessary
|
|
211
|
+
* @param {string} dirPath - Directory or file path to ensure exists
|
|
212
|
+
* @param {boolean} recursive - Create parent directories if needed (default: true)
|
|
213
|
+
* @param {boolean} isFilePath - If true, ensures parent directory of file path (default: false)
|
|
172
214
|
* @returns {Promise<void>}
|
|
173
215
|
*/
|
|
174
|
-
async
|
|
175
|
-
|
|
216
|
+
async ensureDir(dirPath, recursive = true, isFilePath = false)
|
|
217
|
+
{
|
|
218
|
+
return this._commands.ensureDir(dirPath, recursive, isFilePath);
|
|
176
219
|
}
|
|
177
220
|
|
|
178
221
|
/**
|
|
@@ -180,7 +223,8 @@ class FTPClient extends EventEmitter {
|
|
|
180
223
|
* @param {string} path - File path
|
|
181
224
|
* @returns {Promise<Date>}
|
|
182
225
|
*/
|
|
183
|
-
async modifiedTime(path)
|
|
226
|
+
async modifiedTime(path)
|
|
227
|
+
{
|
|
184
228
|
return this._commands.modifiedTime(path);
|
|
185
229
|
}
|
|
186
230
|
|
|
@@ -188,7 +232,8 @@ class FTPClient extends EventEmitter {
|
|
|
188
232
|
* Get connection statistics
|
|
189
233
|
* @returns {Object}
|
|
190
234
|
*/
|
|
191
|
-
getStats()
|
|
235
|
+
getStats()
|
|
236
|
+
{
|
|
192
237
|
return {
|
|
193
238
|
connected: this.connected,
|
|
194
239
|
authenticated: this.authenticated,
|
|
@@ -197,11 +242,21 @@ class FTPClient extends EventEmitter {
|
|
|
197
242
|
};
|
|
198
243
|
}
|
|
199
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Check if connected and authenticated
|
|
247
|
+
* @returns {boolean}
|
|
248
|
+
*/
|
|
249
|
+
isConnected()
|
|
250
|
+
{
|
|
251
|
+
return this.connected && this.authenticated;
|
|
252
|
+
}
|
|
253
|
+
|
|
200
254
|
/**
|
|
201
255
|
* Enable or disable debug mode
|
|
202
256
|
* @param {boolean} enabled - Enable debug mode
|
|
203
257
|
*/
|
|
204
|
-
setDebug(enabled)
|
|
258
|
+
setDebug(enabled)
|
|
259
|
+
{
|
|
205
260
|
this.debug = enabled;
|
|
206
261
|
this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
207
262
|
}
|
|
@@ -210,7 +265,8 @@ class FTPClient extends EventEmitter {
|
|
|
210
265
|
* Close connection
|
|
211
266
|
* @returns {Promise<void>}
|
|
212
267
|
*/
|
|
213
|
-
async close()
|
|
268
|
+
async close()
|
|
269
|
+
{
|
|
214
270
|
return this._connection.close();
|
|
215
271
|
}
|
|
216
272
|
|
|
@@ -218,7 +274,8 @@ class FTPClient extends EventEmitter {
|
|
|
218
274
|
* Disconnect (alias for close)
|
|
219
275
|
* @returns {Promise<void>}
|
|
220
276
|
*/
|
|
221
|
-
async disconnect()
|
|
277
|
+
async disconnect()
|
|
278
|
+
{
|
|
222
279
|
return this.close();
|
|
223
280
|
}
|
|
224
281
|
}
|
package/lib/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const net = require('net');
|
|
2
1
|
const { normalizePath, getParentDir, parseMdtmResponse } = require('./utils');
|
|
2
|
+
const { createOptimizedSocket } = require('./performance');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* FTP command implementations
|
|
@@ -14,9 +14,16 @@ class FTPCommands {
|
|
|
14
14
|
* Upload file to FTP server
|
|
15
15
|
* @param {string|Buffer} data - File data
|
|
16
16
|
* @param {string} remotePath - Remote file path
|
|
17
|
+
* @param {boolean} ensureDir - Ensure parent directory exists (default: false)
|
|
17
18
|
* @returns {Promise<void>}
|
|
18
19
|
*/
|
|
19
|
-
async upload(data, remotePath) {
|
|
20
|
+
async upload(data, remotePath, ensureDir = false) {
|
|
21
|
+
if (!this.client.connected || !this.client.authenticated) {
|
|
22
|
+
throw new Error('Not connected to FTP server');
|
|
23
|
+
}
|
|
24
|
+
if (ensureDir) {
|
|
25
|
+
await this.ensureDir(remotePath, true, true);
|
|
26
|
+
}
|
|
20
27
|
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
21
28
|
this.client._debug(`Uploading ${buffer.length} bytes to ${remotePath}`);
|
|
22
29
|
const { host, port } = await this.connection.enterPassiveMode();
|
|
@@ -24,7 +31,7 @@ class FTPCommands {
|
|
|
24
31
|
return new Promise((resolve, reject) => {
|
|
25
32
|
let commandSent = false;
|
|
26
33
|
|
|
27
|
-
this.client.dataSocket =
|
|
34
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
28
35
|
// Send STOR command to start upload (expects 150, then 226)
|
|
29
36
|
if (!commandSent) {
|
|
30
37
|
commandSent = true;
|
|
@@ -49,7 +56,7 @@ class FTPCommands {
|
|
|
49
56
|
resolve();
|
|
50
57
|
} else if (code >= 400) {
|
|
51
58
|
this.client.removeListener('response', finalHandler);
|
|
52
|
-
reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
|
|
59
|
+
reject(new Error(`Upload failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
|
|
53
60
|
}
|
|
54
61
|
};
|
|
55
62
|
this.client.on('response', finalHandler);
|
|
@@ -58,7 +65,7 @@ class FTPCommands {
|
|
|
58
65
|
setTimeout(() => {
|
|
59
66
|
this.client.removeListener('response', finalHandler);
|
|
60
67
|
resolve();
|
|
61
|
-
}, 5000);
|
|
68
|
+
}, this.client.timeout || 5000);
|
|
62
69
|
});
|
|
63
70
|
});
|
|
64
71
|
}
|
|
@@ -69,6 +76,9 @@ class FTPCommands {
|
|
|
69
76
|
* @returns {Promise<Buffer>}
|
|
70
77
|
*/
|
|
71
78
|
async download(remotePath) {
|
|
79
|
+
if (!this.client.connected || !this.client.authenticated) {
|
|
80
|
+
throw new Error('Not connected to FTP server');
|
|
81
|
+
}
|
|
72
82
|
this.client._debug(`Downloading ${remotePath}`);
|
|
73
83
|
const { host, port } = await this.connection.enterPassiveMode();
|
|
74
84
|
|
|
@@ -76,7 +86,7 @@ class FTPCommands {
|
|
|
76
86
|
const chunks = [];
|
|
77
87
|
let commandSent = false;
|
|
78
88
|
|
|
79
|
-
this.client.dataSocket =
|
|
89
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
80
90
|
// Send RETR command to start download (expects 150, then 226)
|
|
81
91
|
if (!commandSent) {
|
|
82
92
|
commandSent = true;
|
|
@@ -103,7 +113,7 @@ class FTPCommands {
|
|
|
103
113
|
resolve(result);
|
|
104
114
|
} else if (code >= 400) {
|
|
105
115
|
this.client.removeListener('response', finalHandler);
|
|
106
|
-
reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
|
|
116
|
+
reject(new Error(`Download failed - FTP Error ${code}: ${line.substring(4)} (path: ${remotePath})`));
|
|
107
117
|
}
|
|
108
118
|
};
|
|
109
119
|
this.client.on('response', finalHandler);
|
|
@@ -114,7 +124,75 @@ class FTPCommands {
|
|
|
114
124
|
if (chunks.length > 0) {
|
|
115
125
|
resolve(Buffer.concat(chunks));
|
|
116
126
|
}
|
|
117
|
-
}, 5000);
|
|
127
|
+
}, this.client.timeout || 5000);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Download file from FTP server as a stream
|
|
134
|
+
* More memory efficient for large files
|
|
135
|
+
* @param {string} remotePath - Remote file path
|
|
136
|
+
* @param {Stream} writeStream - Writable stream to pipe data to
|
|
137
|
+
* @returns {Promise<number>} - Total bytes transferred
|
|
138
|
+
*/
|
|
139
|
+
async downloadStream(remotePath, writeStream) {
|
|
140
|
+
if (!this.client.connected || !this.client.authenticated) {
|
|
141
|
+
throw new Error('Not connected to FTP server');
|
|
142
|
+
}
|
|
143
|
+
this.client._debug(`Streaming download: ${remotePath}`);
|
|
144
|
+
const { host, port } = await this.connection.enterPassiveMode();
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
let totalBytes = 0;
|
|
148
|
+
let commandSent = false;
|
|
149
|
+
|
|
150
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
151
|
+
if (!commandSent) {
|
|
152
|
+
commandSent = true;
|
|
153
|
+
this.client._debug(`Data connection established for streaming download`);
|
|
154
|
+
this.connection.sendCommand(`RETR ${remotePath}`, true).catch(reject);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.client.dataSocket.on('data', (chunk) => {
|
|
159
|
+
totalBytes += chunk.length;
|
|
160
|
+
writeStream.write(chunk);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.client.dataSocket.on('error', (err) => {
|
|
164
|
+
writeStream.end();
|
|
165
|
+
reject(err);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
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);
|
|
118
196
|
});
|
|
119
197
|
});
|
|
120
198
|
}
|
|
@@ -132,7 +210,7 @@ class FTPCommands {
|
|
|
132
210
|
const chunks = [];
|
|
133
211
|
let commandSent = false;
|
|
134
212
|
|
|
135
|
-
this.client.dataSocket =
|
|
213
|
+
this.client.dataSocket = createOptimizedSocket({ host, port }, () => {
|
|
136
214
|
if (!commandSent) {
|
|
137
215
|
commandSent = true;
|
|
138
216
|
this.connection.sendCommand(`LIST ${path}`, true).catch(reject);
|
|
@@ -160,7 +238,7 @@ class FTPCommands {
|
|
|
160
238
|
setTimeout(() => {
|
|
161
239
|
this.client.removeListener('response', finalHandler);
|
|
162
240
|
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
163
|
-
}, 3000);
|
|
241
|
+
}, this.client.timeout || 3000);
|
|
164
242
|
});
|
|
165
243
|
});
|
|
166
244
|
}
|
|
@@ -219,7 +297,7 @@ class FTPCommands {
|
|
|
219
297
|
* @returns {Promise<number>}
|
|
220
298
|
*/
|
|
221
299
|
async size(path) {
|
|
222
|
-
this.client._debug(`Getting size of ${path}`)
|
|
300
|
+
this.client._debug(`Getting size of ${path}`);
|
|
223
301
|
const response = await this.connection.sendCommand(`SIZE ${path}`);
|
|
224
302
|
return parseInt(response.message);
|
|
225
303
|
}
|
|
@@ -230,25 +308,61 @@ class FTPCommands {
|
|
|
230
308
|
* @returns {Promise<boolean>}
|
|
231
309
|
*/
|
|
232
310
|
async exists(path) {
|
|
311
|
+
const info = await this.stat(path);
|
|
312
|
+
return info.exists;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get file/directory information
|
|
317
|
+
* @param {string} path - Path to check
|
|
318
|
+
* @returns {Promise<Object>} - { exists, size, isFile, isDirectory }
|
|
319
|
+
*/
|
|
320
|
+
async stat(path) {
|
|
233
321
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
322
|
+
// First try SIZE command (works for files)
|
|
323
|
+
const size = await this.size(path);
|
|
324
|
+
return { exists: true, size, isFile: true, isDirectory: false };
|
|
236
325
|
} catch (err) {
|
|
237
|
-
|
|
326
|
+
// SIZE failed, might be a directory - try CWD
|
|
327
|
+
try {
|
|
328
|
+
const currentDir = await this.pwd();
|
|
329
|
+
await this.cd(path);
|
|
330
|
+
// Restore original directory
|
|
331
|
+
await this.cd(currentDir);
|
|
332
|
+
return { exists: true, size: null, isFile: false, isDirectory: true };
|
|
333
|
+
} catch (cdErr) {
|
|
334
|
+
// Both SIZE and CWD failed - try listing parent directory
|
|
335
|
+
try {
|
|
336
|
+
const dir = getParentDir(path);
|
|
337
|
+
const basename = path.split('/').pop();
|
|
338
|
+
const listing = await this.list(dir);
|
|
339
|
+
const found = listing.split('\n').some(line => line.includes(basename));
|
|
340
|
+
return { exists: found, size: null, isFile: null, isDirectory: null };
|
|
341
|
+
} catch (listErr) {
|
|
342
|
+
return { exists: false, size: null, isFile: null, isDirectory: null };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
238
345
|
}
|
|
239
346
|
}
|
|
240
347
|
|
|
241
348
|
/**
|
|
242
349
|
* Ensure directory exists, creating it if necessary
|
|
243
|
-
* @param {string} dirPath - Directory path to ensure exists
|
|
350
|
+
* @param {string} dirPath - Directory or file path to ensure exists
|
|
244
351
|
* @param {boolean} recursive - Create parent directories if needed (default: true)
|
|
352
|
+
* @param {boolean} isFilePath - If true, ensures parent directory of file path (default: false)
|
|
245
353
|
* @returns {Promise<void>}
|
|
246
354
|
*/
|
|
247
|
-
async ensureDir(dirPath, recursive = true) {
|
|
248
|
-
this
|
|
355
|
+
async ensureDir(dirPath, recursive = true, isFilePath = false) {
|
|
356
|
+
// If this is a file path, extract the parent directory
|
|
357
|
+
const targetPath = isFilePath ? getParentDir(dirPath) : dirPath;
|
|
358
|
+
if (!targetPath || targetPath === '.' || targetPath === '/') {
|
|
359
|
+
return; // Root or current directory always exists
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.client._debug(`Ensuring directory exists: ${targetPath}`);
|
|
249
363
|
|
|
250
364
|
// Normalize path
|
|
251
|
-
const normalized = normalizePath(
|
|
365
|
+
const normalized = normalizePath(targetPath);
|
|
252
366
|
if (normalized === '/' || normalized === '.') {
|
|
253
367
|
return; // Root or current directory always exists
|
|
254
368
|
}
|
|
@@ -282,32 +396,6 @@ class FTPCommands {
|
|
|
282
396
|
}
|
|
283
397
|
}
|
|
284
398
|
|
|
285
|
-
/**
|
|
286
|
-
* Ensure parent directory exists for a file path
|
|
287
|
-
* @param {string} filePath - File path
|
|
288
|
-
* @returns {Promise<void>}
|
|
289
|
-
*/
|
|
290
|
-
async ensureParentDir(filePath) {
|
|
291
|
-
const parentDir = getParentDir(filePath);
|
|
292
|
-
if (parentDir && parentDir !== '.' && parentDir !== '/') {
|
|
293
|
-
await this.ensureDir(parentDir);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Upload file and ensure parent directory exists
|
|
299
|
-
* @param {string|Buffer} data - File data
|
|
300
|
-
* @param {string} remotePath - Remote file path
|
|
301
|
-
* @param {boolean} ensureDir - Ensure parent directory exists (default: false)
|
|
302
|
-
* @returns {Promise<void>}
|
|
303
|
-
*/
|
|
304
|
-
async uploadFile(data, remotePath, ensureDir = false) {
|
|
305
|
-
if (ensureDir) {
|
|
306
|
-
await this.ensureParentDir(remotePath);
|
|
307
|
-
}
|
|
308
|
-
return this.upload(data, remotePath);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
399
|
/**
|
|
312
400
|
* Get file modification time
|
|
313
401
|
* @param {string} path - File path
|