molex-ftp-client 1.0.1 → 1.2.1
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 +61 -0
- package/README.md +86 -3
- package/index.js +11 -469
- package/lib/FTPClient.js +226 -0
- package/lib/commands.js +323 -0
- package/lib/connection.js +176 -0
- package/lib/utils.js +84 -0
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.2.1] - 2026-02-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Improved README documentation
|
|
7
|
+
- Better examples for `ensureDir()`, `ensureParentDir()`, and `uploadFile()`
|
|
8
|
+
- Added architecture section explaining modular structure
|
|
9
|
+
- Enhanced feature list highlighting directory management capabilities
|
|
10
|
+
- Reorganized API documentation for better clarity
|
|
11
|
+
|
|
12
|
+
## [1.2.0] - 2026-02-02
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Major refactoring**: Improved separation of concerns
|
|
16
|
+
- `index.js` now serves as simple entry point
|
|
17
|
+
- Implementation moved to organized `lib/` structure:
|
|
18
|
+
- `lib/FTPClient.js` - Main class definition
|
|
19
|
+
- `lib/connection.js` - Connection and authentication logic
|
|
20
|
+
- `lib/commands.js` - All FTP command implementations
|
|
21
|
+
- `lib/utils.js` - Helper functions
|
|
22
|
+
- Better code maintainability and readability
|
|
23
|
+
- No breaking changes - API remains identical
|
|
24
|
+
|
|
25
|
+
## [1.1.0] - 2026-02-02
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `ensureDir(dirPath, recursive)` - Ensure directory exists, creating parent directories if needed
|
|
29
|
+
- `ensureParentDir(filePath)` - Ensure parent directory exists for a file path
|
|
30
|
+
- `uploadFile(data, remotePath, ensureDir)` - Upload with automatic directory creation
|
|
31
|
+
- Utility library (`lib/utils.js`) for better code organization
|
|
32
|
+
- Helper functions for FTP command parsing and path manipulation
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Refactored internal code structure for better maintainability
|
|
36
|
+
- Improved path normalization across all directory operations
|
|
37
|
+
- Better error handling for directory creation
|
|
38
|
+
|
|
39
|
+
### Improved
|
|
40
|
+
- Cleaner API for common operations
|
|
41
|
+
- Reduced boilerplate code needed for directory handling
|
|
42
|
+
- More consistent error messages
|
|
43
|
+
|
|
44
|
+
## [1.0.1] - 2026-02-02
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Updated repository URLs to correct GitHub location
|
|
48
|
+
|
|
49
|
+
## [1.0.0] - 2026-02-02
|
|
50
|
+
|
|
51
|
+
### Initial Release
|
|
52
|
+
- Zero dependencies FTP client using native Node.js TCP sockets
|
|
53
|
+
- Promise-based API with async/await support
|
|
54
|
+
- Passive mode (PASV) for data transfers
|
|
55
|
+
- Debug logging with configurable options
|
|
56
|
+
- Connection keep-alive and timeout configuration
|
|
57
|
+
- Upload/download files with Buffer support
|
|
58
|
+
- Directory operations (list, cd, mkdir, pwd)
|
|
59
|
+
- File operations (delete, rename, size, exists, modifiedTime)
|
|
60
|
+
- Connection statistics tracking
|
|
61
|
+
- Event-based architecture
|
package/README.md
CHANGED
|
@@ -12,9 +12,11 @@ Lightweight FTP client built with native Node.js TCP sockets (net module).
|
|
|
12
12
|
- ✅ **Configurable timeouts** - Prevent hanging connections
|
|
13
13
|
- ✅ **Event-based** - Listen to FTP responses and events
|
|
14
14
|
- ✅ **Upload/download** files with Buffer support
|
|
15
|
-
- ✅ **
|
|
15
|
+
- ✅ **Smart directory management** - Auto-create nested directories
|
|
16
|
+
- ✅ **Directory operations** (list, cd, mkdir, pwd, ensureDir)
|
|
16
17
|
- ✅ **File operations** (delete, rename, size, exists, modifiedTime)
|
|
17
18
|
- ✅ **Connection statistics** - Track command count and status
|
|
19
|
+
- ✅ **Clean architecture** - Modular structure with separation of concerns
|
|
18
20
|
|
|
19
21
|
## Installation
|
|
20
22
|
|
|
@@ -113,6 +115,27 @@ await client.upload(buffer, '/path/file.bin');
|
|
|
113
115
|
|
|
114
116
|
Returns: `Promise<void>`
|
|
115
117
|
|
|
118
|
+
#### `uploadFile(data, remotePath, ensureDir)`
|
|
119
|
+
|
|
120
|
+
Upload file and optionally ensure parent directory exists. This is the recommended method when uploading to deep paths.
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// Upload with automatic directory creation (recommended)
|
|
124
|
+
await client.uploadFile('data', '/deep/nested/path/file.txt', true);
|
|
125
|
+
|
|
126
|
+
// Upload without directory creation
|
|
127
|
+
await client.uploadFile('data', '/file.txt', false);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Parameters:**
|
|
131
|
+
- `data` (string|Buffer): File content
|
|
132
|
+
- `remotePath` (string): Remote file path
|
|
133
|
+
- `ensureDir` (boolean): Create parent directories if needed (default: false)
|
|
134
|
+
|
|
135
|
+
**Why use this?** Simplifies uploads to nested paths by automatically creating any missing parent directories.
|
|
136
|
+
|
|
137
|
+
Returns: `Promise<void>`
|
|
138
|
+
|
|
116
139
|
#### `download(remotePath)`
|
|
117
140
|
|
|
118
141
|
Download file from server.
|
|
@@ -221,6 +244,46 @@ await client.mkdir('/remote/newdir');
|
|
|
221
244
|
|
|
222
245
|
Returns: `Promise<void>`
|
|
223
246
|
|
|
247
|
+
#### `ensureDir(dirPath, recursive)`
|
|
248
|
+
|
|
249
|
+
Ensure directory exists, creating it (and parent directories) if necessary. Idempotent - safe to call multiple times.
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
// Create nested directories recursively (default)
|
|
253
|
+
await client.ensureDir('/deep/nested/path');
|
|
254
|
+
|
|
255
|
+
// Create single directory only (will fail if parent doesn't exist)
|
|
256
|
+
await client.ensureDir('/newdir', false);
|
|
257
|
+
|
|
258
|
+
// Idempotent - no error if directory already exists
|
|
259
|
+
await client.ensureDir('/existing/path'); // No error
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Parameters:**
|
|
263
|
+
- `dirPath` (string): Directory path to ensure exists
|
|
264
|
+
- `recursive` (boolean): Create parent directories if needed (default: true)
|
|
265
|
+
|
|
266
|
+
**Use case:** Preparing directory structure before multiple file uploads.
|
|
267
|
+
|
|
268
|
+
Returns: `Promise<void>`
|
|
269
|
+
|
|
270
|
+
#### `ensureParentDir(filePath)`
|
|
271
|
+
|
|
272
|
+
Ensure the parent directory exists for a given file path. Recursively creates all missing parent directories.
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
// Ensures /path/to exists before uploading
|
|
276
|
+
await client.ensureParentDir('/path/to/file.txt');
|
|
277
|
+
await client.upload('data', '/path/to/file.txt');
|
|
278
|
+
|
|
279
|
+
// Tip: Use uploadFile() instead for convenience
|
|
280
|
+
await client.uploadFile('data', '/path/to/file.txt', true); // Equivalent
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Use case:** Manual directory management before upload. Consider using `uploadFile()` with `ensureDir=true` for a simpler approach.
|
|
284
|
+
|
|
285
|
+
Returns: `Promise<void>`
|
|
286
|
+
|
|
224
287
|
### Utilities
|
|
225
288
|
|
|
226
289
|
#### `getStats()`
|
|
@@ -368,9 +431,9 @@ async function backupFile() {
|
|
|
368
431
|
await client.rename('/backup/data.json', '/backup/data.old.json');
|
|
369
432
|
}
|
|
370
433
|
|
|
371
|
-
// Upload new backup
|
|
434
|
+
// Upload new backup (with automatic directory creation)
|
|
372
435
|
const newData = JSON.stringify({ timestamp: Date.now(), data: [1, 2, 3] });
|
|
373
|
-
await client.
|
|
436
|
+
await client.uploadFile(newData, '/backup/data.json', true);
|
|
374
437
|
console.log('Backup uploaded successfully');
|
|
375
438
|
|
|
376
439
|
// Verify
|
|
@@ -392,6 +455,26 @@ async function backupFile() {
|
|
|
392
455
|
backupFile();
|
|
393
456
|
```
|
|
394
457
|
|
|
458
|
+
## Architecture
|
|
459
|
+
|
|
460
|
+
The library is organized with clean separation of concerns:
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
molex-ftp-client/
|
|
464
|
+
├── index.js # Entry point - exports FTPClient
|
|
465
|
+
├── lib/
|
|
466
|
+
│ ├── FTPClient.js # Main class definition & public API
|
|
467
|
+
│ ├── connection.js # Connection & authentication logic
|
|
468
|
+
│ ├── commands.js # FTP command implementations
|
|
469
|
+
│ └── utils.js # Helper functions (parsing, paths)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Benefits:**
|
|
473
|
+
- 📁 **Modular structure** - Easy to maintain and extend
|
|
474
|
+
- 🔍 **Clear responsibilities** - Each file has a single purpose
|
|
475
|
+
- 🧪 **Testable** - Isolated components for unit testing
|
|
476
|
+
- 📖 **Readable** - Simple entry point with organized implementation
|
|
477
|
+
|
|
395
478
|
## License
|
|
396
479
|
|
|
397
480
|
ISC © Tony Wiedman / MolexWorks
|
package/index.js
CHANGED
|
@@ -1,475 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
1
|
+
/*
|
|
2
|
+
* File: c:\Users\tonyw\Desktop\PRIVS\molex-ftp-client\index.js
|
|
3
|
+
* Project: c:\Users\tonyw\Desktop\PRIVS\molex-ftp-client
|
|
4
|
+
* Created Date: Monday February 2nd 2026
|
|
5
|
+
* Author: Tony Wiedman
|
|
6
|
+
* -----
|
|
7
|
+
* Last Modified: Mon February 2nd 2026 1:23:44
|
|
8
|
+
* Modified By: Tony Wiedman
|
|
9
|
+
* -----
|
|
10
|
+
* Copyright (c) 2026 MolexWorks
|
|
6
11
|
*/
|
|
7
|
-
class FTPClient extends EventEmitter {
|
|
8
|
-
constructor(options = {}) {
|
|
9
|
-
super();
|
|
10
|
-
this.socket = null;
|
|
11
|
-
this.dataSocket = null;
|
|
12
|
-
this.buffer = '';
|
|
13
|
-
this.connected = false;
|
|
14
|
-
this.authenticated = false;
|
|
15
|
-
this.debug = options.debug || false;
|
|
16
|
-
this.timeout = options.timeout || 30000;
|
|
17
|
-
this.keepAlive = options.keepAlive !== false;
|
|
18
|
-
this._log = options.logger || console.log;
|
|
19
|
-
this._commandCount = 0;
|
|
20
|
-
this._lastCommand = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Log message if debug is enabled
|
|
25
|
-
* @private
|
|
26
|
-
*/
|
|
27
|
-
_debug(...args) {
|
|
28
|
-
if (this.debug && this._log) {
|
|
29
|
-
this._log('[FTP Debug]', ...args);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Connect to FTP server
|
|
35
|
-
* @param {Object} options - Connection options
|
|
36
|
-
* @param {string} options.host - FTP server host
|
|
37
|
-
* @param {number} [options.port=21] - FTP server port
|
|
38
|
-
* @param {string} [options.user='anonymous'] - Username
|
|
39
|
-
* @param {string} [options.password='anonymous@'] - Password
|
|
40
|
-
* @returns {Promise<void>}
|
|
41
|
-
*/
|
|
42
|
-
async connect({ host, port = 21, user = 'anonymous', password = 'anonymous@' }) {
|
|
43
|
-
this._debug(`Connecting to ${host}:${port} as ${user}`);
|
|
44
|
-
return new Promise((resolve, reject) => {
|
|
45
|
-
this.socket = net.createConnection({ host, port }, () => {
|
|
46
|
-
this.connected = true;
|
|
47
|
-
this._debug('TCP connection established');
|
|
48
|
-
if (this.keepAlive) {
|
|
49
|
-
this.socket.setKeepAlive(true, 10000);
|
|
50
|
-
}
|
|
51
|
-
this.emit('connected');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
this.socket.setEncoding('utf8');
|
|
55
|
-
this.socket.on('data', async (data) => {
|
|
56
|
-
this.buffer += data;
|
|
57
|
-
const lines = this.buffer.split('\r\n');
|
|
58
|
-
this.buffer = lines.pop();
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
if (line) {
|
|
62
|
-
this._debug('<<<', line);
|
|
63
|
-
this.emit('response', line);
|
|
64
|
-
const code = parseInt(line.substring(0, 3));
|
|
65
|
-
|
|
66
|
-
// Handle initial connection
|
|
67
|
-
if (code === 220 && !this.authenticated) {
|
|
68
|
-
try {
|
|
69
|
-
this._debug('Authenticating...');
|
|
70
|
-
await this._sendCommand(`USER ${user}`);
|
|
71
|
-
await this._sendCommand(`PASS ${password}`);
|
|
72
|
-
this.authenticated = true;
|
|
73
|
-
this._debug('Authentication successful');
|
|
74
|
-
resolve();
|
|
75
|
-
} catch (err) {
|
|
76
|
-
reject(err);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
this.socket.on('error', (err) => {
|
|
84
|
-
this.emit('error', err);
|
|
85
|
-
reject(err);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.socket.on('close', () => {
|
|
89
|
-
this.connected = false;
|
|
90
|
-
this.authenticated = false;
|
|
91
|
-
this.emit('close');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
setTimeout(() => reject(new Error('Connection timeout')), 10000);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Send FTP command and wait for response
|
|
100
|
-
* @param {string} command - FTP command
|
|
101
|
-
* @param {boolean} allowPreliminary - Allow 1xx preliminary responses
|
|
102
|
-
* @returns {Promise<Object>}
|
|
103
|
-
*/
|
|
104
|
-
_sendCommand(command, allowPreliminary = false) {
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
if (!this.connected) {
|
|
107
|
-
return reject(new Error('Not connected'));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
this._commandCount++;
|
|
111
|
-
this._lastCommand = command;
|
|
112
|
-
const cmdToLog = command.startsWith('PASS ') ? 'PASS ********' : command;
|
|
113
|
-
this._debug('>>>', cmdToLog);
|
|
114
|
-
|
|
115
|
-
const timeoutId = setTimeout(() => {
|
|
116
|
-
this.removeListener('response', responseHandler);
|
|
117
|
-
reject(new Error(`Command timeout: ${cmdToLog}`));
|
|
118
|
-
}, this.timeout);
|
|
119
|
-
|
|
120
|
-
const responseHandler = (line) => {
|
|
121
|
-
clearTimeout(timeoutId);
|
|
122
|
-
const code = parseInt(line.substring(0, 3));
|
|
123
|
-
const message = line.substring(4);
|
|
124
|
-
|
|
125
|
-
// Check if this is a complete response (not a multi-line response in progress)
|
|
126
|
-
if (line.charAt(3) === ' ') {
|
|
127
|
-
// 1xx = Preliminary positive reply (command okay, another command expected)
|
|
128
|
-
// 2xx = Positive completion reply
|
|
129
|
-
// 3xx = Positive intermediate reply (command okay, awaiting more info)
|
|
130
|
-
// 4xx/5xx = Negative replies (errors)
|
|
131
|
-
|
|
132
|
-
if (code >= 100 && code < 200 && allowPreliminary) {
|
|
133
|
-
// Don't remove listener, wait for final response
|
|
134
|
-
this._debug('Preliminary response, waiting for completion...');
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
clearTimeout(timeoutId);
|
|
139
|
-
this.removeListener('response', responseHandler);
|
|
140
|
-
|
|
141
|
-
if (code >= 200 && code < 400) {
|
|
142
|
-
resolve({ code, message, raw: line });
|
|
143
|
-
} else {
|
|
144
|
-
this._debug(`Error response: ${code}`);
|
|
145
|
-
reject(new Error(`FTP Error ${code}: ${message}`));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
this.on('response', responseHandler);
|
|
151
|
-
this.socket.write(command + '\r\n');
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Enter passive mode and get data connection info
|
|
157
|
-
* @returns {Promise<Object>}
|
|
158
|
-
*/
|
|
159
|
-
async _enterPassiveMode() {
|
|
160
|
-
const response = await this._sendCommand('PASV');
|
|
161
|
-
const match = response.message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
|
|
162
|
-
|
|
163
|
-
if (!match) {
|
|
164
|
-
throw new Error('Failed to parse PASV response');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
|
|
168
|
-
const port = parseInt(match[5]) * 256 + parseInt(match[6]);
|
|
169
|
-
|
|
170
|
-
return { host, port };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Upload file to FTP server
|
|
175
|
-
* @param {string|Buffer} data - File data
|
|
176
|
-
* @param {string} remotePath - Remote file path
|
|
177
|
-
* @returns {Promise<void>}
|
|
178
|
-
*/
|
|
179
|
-
async upload(data, remotePath) {
|
|
180
|
-
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
181
|
-
this._debug(`Uploading ${buffer.length} bytes to ${remotePath}`);
|
|
182
|
-
const { host, port } = await this._enterPassiveMode();
|
|
183
|
-
|
|
184
|
-
return new Promise((resolve, reject) => {
|
|
185
|
-
let commandSent = false;
|
|
186
|
-
|
|
187
|
-
this.dataSocket = net.createConnection({ host, port }, () => {
|
|
188
|
-
// Send STOR command to start upload (expects 150, then 226)
|
|
189
|
-
if (!commandSent) {
|
|
190
|
-
commandSent = true;
|
|
191
|
-
this._debug(`Data connection established for upload`);
|
|
192
|
-
this._sendCommand(`STOR ${remotePath}`, true).catch(reject);
|
|
193
|
-
|
|
194
|
-
// Write data to data socket
|
|
195
|
-
this.dataSocket.write(buffer);
|
|
196
|
-
this.dataSocket.end();
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
this.dataSocket.on('error', reject);
|
|
201
|
-
|
|
202
|
-
this.dataSocket.on('close', () => {
|
|
203
|
-
// Wait for final response from control socket
|
|
204
|
-
const finalHandler = (line) => {
|
|
205
|
-
const code = parseInt(line.substring(0, 3));
|
|
206
|
-
if (code === 226 || code === 250) {
|
|
207
|
-
this.removeListener('response', finalHandler);
|
|
208
|
-
this._debug(`Upload completed successfully`);
|
|
209
|
-
resolve();
|
|
210
|
-
} else if (code >= 400) {
|
|
211
|
-
this.removeListener('response', finalHandler);
|
|
212
|
-
reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
this.on('response', finalHandler);
|
|
216
|
-
|
|
217
|
-
// Timeout if no response
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
this.removeListener('response', finalHandler);
|
|
220
|
-
resolve();
|
|
221
|
-
}, 5000);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Download file from FTP server
|
|
228
|
-
* @param {string} remotePath - Remote file path
|
|
229
|
-
* @returns {Promise<Buffer>}
|
|
230
|
-
*/
|
|
231
|
-
async download(remotePath) {
|
|
232
|
-
this._debug(`Downloading ${remotePath}`);
|
|
233
|
-
const { host, port } = await this._enterPassiveMode();
|
|
234
|
-
|
|
235
|
-
return new Promise((resolve, reject) => {
|
|
236
|
-
const chunks = [];
|
|
237
|
-
let commandSent = false;
|
|
238
|
-
|
|
239
|
-
this.dataSocket = net.createConnection({ host, port }, () => {
|
|
240
|
-
// Send RETR command to start download (expects 150, then 226)
|
|
241
|
-
if (!commandSent) {
|
|
242
|
-
commandSent = true;
|
|
243
|
-
this._debug(`Data connection established for download`);
|
|
244
|
-
this._sendCommand(`RETR ${remotePath}`, true).catch(reject);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
this.dataSocket.on('data', (chunk) => {
|
|
249
|
-
chunks.push(chunk);
|
|
250
|
-
this._debug(`Received ${chunk.length} bytes`);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
this.dataSocket.on('error', reject);
|
|
254
|
-
|
|
255
|
-
this.dataSocket.on('close', () => {
|
|
256
|
-
// Wait for final 226 response
|
|
257
|
-
const finalHandler = (line) => {
|
|
258
|
-
const code = parseInt(line.substring(0, 3));
|
|
259
|
-
if (code === 226 || code === 250) {
|
|
260
|
-
this.removeListener('response', finalHandler);
|
|
261
|
-
const result = Buffer.concat(chunks);
|
|
262
|
-
this._debug(`Download completed: ${result.length} bytes`);
|
|
263
|
-
resolve(result);
|
|
264
|
-
} else if (code >= 400) {
|
|
265
|
-
this.removeListener('response', finalHandler);
|
|
266
|
-
reject(new Error(`FTP Error ${code}: ${line.substring(4)}`));
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
this.on('response', finalHandler);
|
|
270
|
-
|
|
271
|
-
// Timeout if no response
|
|
272
|
-
setTimeout(() => {
|
|
273
|
-
this.removeListener('response', finalHandler);
|
|
274
|
-
if (chunks.length > 0) {
|
|
275
|
-
resolve(Buffer.concat(chunks));
|
|
276
|
-
}
|
|
277
|
-
}, 5000);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* List directory contents
|
|
284
|
-
* @param {string} [path='.'] - Directory path
|
|
285
|
-
* @returns {Promise<string>}
|
|
286
|
-
*/
|
|
287
|
-
async list(path = '.') {
|
|
288
|
-
this._debug(`Listing directory: ${path}`);
|
|
289
|
-
const { host, port } = await this._enterPassiveMode();
|
|
290
|
-
|
|
291
|
-
return new Promise((resolve, reject) => {
|
|
292
|
-
const chunks = [];
|
|
293
|
-
let commandSent = false;
|
|
294
|
-
|
|
295
|
-
this.dataSocket = net.createConnection({ host, port }, () => {
|
|
296
|
-
if (!commandSent) {
|
|
297
|
-
commandSent = true;
|
|
298
|
-
this._sendCommand(`LIST ${path}`, true).catch(reject);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
this.dataSocket.on('data', (chunk) => {
|
|
303
|
-
chunks.push(chunk);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
this.dataSocket.on('error', reject);
|
|
307
|
-
|
|
308
|
-
this.dataSocket.on('close', () => {
|
|
309
|
-
// Wait for final 226 response
|
|
310
|
-
const finalHandler = (line) => {
|
|
311
|
-
const code = parseInt(line.substring(0, 3));
|
|
312
|
-
if (code === 226 || code === 250) {
|
|
313
|
-
this.removeListener('response', finalHandler);
|
|
314
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
this.on('response', finalHandler);
|
|
318
|
-
|
|
319
|
-
// Timeout fallback
|
|
320
|
-
setTimeout(() => {
|
|
321
|
-
this.removeListener('response', finalHandler);
|
|
322
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
323
|
-
}, 3000);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Change working directory
|
|
330
|
-
* @param {string} path - Directory path
|
|
331
|
-
* @returns {Promise<void>}
|
|
332
|
-
*/
|
|
333
|
-
async cd(path) {
|
|
334
|
-
await this._sendCommand(`CWD ${path}`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Get current working directory
|
|
339
|
-
* @returns {Promise<string>}
|
|
340
|
-
*/
|
|
341
|
-
async pwd() {
|
|
342
|
-
const response = await this._sendCommand('PWD');
|
|
343
|
-
const match = response.message.match(/"(.+)"/);
|
|
344
|
-
return match ? match[1] : '/';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Create directory
|
|
349
|
-
* @param {string} path - Directory path
|
|
350
|
-
* @returns {Promise<void>}
|
|
351
|
-
*/
|
|
352
|
-
async mkdir(path) {
|
|
353
|
-
await this._sendCommand(`MKD ${path}`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Delete file
|
|
358
|
-
* @param {string} path - File path
|
|
359
|
-
* @returns {Promise<void>}
|
|
360
|
-
*/
|
|
361
|
-
async delete(path) {
|
|
362
|
-
await this._sendCommand(`DELE ${path}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Rename file
|
|
367
|
-
* @param {string} from - Current name
|
|
368
|
-
* @param {string} to - New name
|
|
369
|
-
* @returns {Promise<void>}
|
|
370
|
-
*/
|
|
371
|
-
async rename(from, to) {
|
|
372
|
-
await this._sendCommand(`RNFR ${from}`);
|
|
373
|
-
await this._sendCommand(`RNTO ${to}`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Get file size
|
|
378
|
-
* @param {string} path - File path
|
|
379
|
-
* @returns {Promise<number>}
|
|
380
|
-
*/
|
|
381
|
-
async size(path) {
|
|
382
|
-
this._debug(`Getting size of ${path}`)
|
|
383
|
-
const response = await this._sendCommand(`SIZE ${path}`);
|
|
384
|
-
return parseInt(response.message);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Check if file or directory exists
|
|
389
|
-
* @param {string} path - File or directory path
|
|
390
|
-
* @returns {Promise<boolean>}
|
|
391
|
-
*/
|
|
392
|
-
async exists(path) {
|
|
393
|
-
try {
|
|
394
|
-
await this.size(path);
|
|
395
|
-
return true;
|
|
396
|
-
} catch (err) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Get file modification time
|
|
403
|
-
* @param {string} path - File path
|
|
404
|
-
* @returns {Promise<Date>}
|
|
405
|
-
*/
|
|
406
|
-
async modifiedTime(path) {
|
|
407
|
-
this._debug(`Getting modification time of ${path}`);
|
|
408
|
-
const response = await this._sendCommand(`MDTM ${path}`);
|
|
409
|
-
// Parse MDTM response: YYYYMMDDhhmmss
|
|
410
|
-
const match = response.message.match(/(\d{14})/);
|
|
411
|
-
if (match) {
|
|
412
|
-
const str = match[1];
|
|
413
|
-
const year = parseInt(str.substring(0, 4));
|
|
414
|
-
const month = parseInt(str.substring(4, 6)) - 1;
|
|
415
|
-
const day = parseInt(str.substring(6, 8));
|
|
416
|
-
const hour = parseInt(str.substring(8, 10));
|
|
417
|
-
const minute = parseInt(str.substring(10, 12));
|
|
418
|
-
const second = parseInt(str.substring(12, 14));
|
|
419
|
-
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
420
|
-
}
|
|
421
|
-
throw new Error('Failed to parse MDTM response');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Get connection statistics
|
|
426
|
-
* @returns {Object}
|
|
427
|
-
*/
|
|
428
|
-
getStats() {
|
|
429
|
-
return {
|
|
430
|
-
connected: this.connected,
|
|
431
|
-
authenticated: this.authenticated,
|
|
432
|
-
commandCount: this._commandCount,
|
|
433
|
-
lastCommand: this._lastCommand
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Enable or disable debug mode
|
|
439
|
-
* @param {boolean} enabled - Enable debug mode
|
|
440
|
-
*/
|
|
441
|
-
setDebug(enabled) {
|
|
442
|
-
this.debug = enabled;
|
|
443
|
-
this._debug(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
444
|
-
}
|
|
445
12
|
|
|
446
|
-
/**
|
|
447
|
-
* Close connection
|
|
448
|
-
* @returns {Promise<void>}
|
|
449
|
-
*/
|
|
450
|
-
async close() {
|
|
451
|
-
if (this.connected) {
|
|
452
|
-
this._debug('Closing connection...');
|
|
453
|
-
try {
|
|
454
|
-
await this._sendCommand('QUIT');
|
|
455
|
-
} catch (err) {
|
|
456
|
-
this._debug('Error during QUIT:', err.message);
|
|
457
|
-
}
|
|
458
|
-
this.socket.end();
|
|
459
|
-
this.connected = false;
|
|
460
|
-
this.authenticated = false;
|
|
461
|
-
this._debug('Connection closed');
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
13
|
|
|
465
|
-
|
|
466
|
-
* Disconnect (alias for close)
|
|
467
|
-
* @returns {Promise<void>}
|
|
468
|
-
*/
|
|
469
|
-
async disconnect() {
|
|
470
|
-
return this.close();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
14
|
+
const FTPClient = require('./lib/FTPClient');
|
|
473
15
|
|
|
474
16
|
module.exports = FTPClient;
|
|
475
17
|
module.exports.FTPClient = FTPClient;
|