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/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
- constructor(options = {}) {
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
- if (this.debug && this._log) {
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
- return this._commands.upload(data, remotePath);
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
- * Ensure directory exists, creating it if necessary
150
- * @param {string} dirPath - Directory path to ensure exists
151
- * @param {boolean} recursive - Create parent directories if needed (default: true)
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 ensureParentDir(filePath) {
164
- return this._commands.ensureParentDir(filePath);
204
+ async stat(path)
205
+ {
206
+ return this._commands.stat(path);
165
207
  }
166
208
 
167
209
  /**
168
- * Upload file and ensure parent directory exists
169
- * @param {string|Buffer} data - File data
170
- * @param {string} remotePath - Remote file path
171
- * @param {boolean} ensureDir - Ensure parent directory exists (default: false)
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 uploadFile(data, remotePath, ensureDir = false) {
175
- return this._commands.uploadFile(data, remotePath, ensureDir);
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 = net.createConnection({ host, port }, () => {
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 = net.createConnection({ host, port }, () => {
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 = net.createConnection({ host, port }, () => {
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
- await this.size(path);
235
- return true;
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
- return false;
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.client._debug(`Ensuring directory exists: ${dirPath}`);
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(dirPath);
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