molex-ftp-client 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +398 -0
  3. package/index.js +476 -0
  4. package/package.json +34 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Tony Wiedman / MolexWorks
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,398 @@
1
+ # molex-ftp-client
2
+
3
+ Lightweight FTP client built with native Node.js TCP sockets (net module).
4
+
5
+ ## Features
6
+
7
+ - ✅ **Zero dependencies** - Uses only native Node.js modules
8
+ - ✅ **Promise-based API** - Modern async/await support
9
+ - ✅ **Passive mode** (PASV) for data transfers
10
+ - ✅ **Debug logging** - Optional verbose logging for troubleshooting
11
+ - ✅ **Connection keep-alive** - Automatic TCP keep-alive
12
+ - ✅ **Configurable timeouts** - Prevent hanging connections
13
+ - ✅ **Event-based** - Listen to FTP responses and events
14
+ - ✅ **Upload/download** files with Buffer support
15
+ - ✅ **Directory operations** (list, cd, mkdir, pwd)
16
+ - ✅ **File operations** (delete, rename, size, exists, modifiedTime)
17
+ - ✅ **Connection statistics** - Track command count and status
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install molex-ftp-client
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```javascript
28
+ const FTPClient = require('molex-ftp-client');
29
+
30
+ const client = new FTPClient({
31
+ debug: true, // Enable debug logging (default: false)
32
+ timeout: 30000, // Command timeout in ms (default: 30000)
33
+ keepAlive: true // Enable TCP keep-alive (default: true)
34
+ });
35
+
36
+ try {
37
+ // Connect to FTP server
38
+ await client.connect({
39
+ host: 'ftp.example.com',
40
+ port: 21,
41
+ user: 'username',
42
+ password: 'password'
43
+ });
44
+
45
+ // Upload file
46
+ await client.upload('Hello World!', '/remote/path/file.txt');
47
+
48
+ // Download file
49
+ const data = await client.download('/remote/path/file.txt');
50
+ console.log(data.toString());
51
+
52
+ // Close connection
53
+ await client.close();
54
+ } catch (err) {
55
+ console.error('FTP Error:', err);
56
+ }
57
+ ```
58
+
59
+ ## Constructor Options
60
+
61
+ ```javascript
62
+ const client = new FTPClient({
63
+ debug: false, // Enable debug logging
64
+ timeout: 30000, // Command timeout in milliseconds
65
+ keepAlive: true, // Enable TCP keep-alive
66
+ logger: console.log // Custom logger function
67
+ });
68
+ ```
69
+
70
+ ## API
71
+
72
+ ### Connection
73
+
74
+ #### `connect(options)`
75
+
76
+ Connect to FTP server.
77
+
78
+ ```javascript
79
+ await client.connect({
80
+ host: 'ftp.example.com', // Required
81
+ port: 21, // Default: 21
82
+ user: 'username', // Default: 'anonymous'
83
+ password: 'password' // Default: 'anonymous@'
84
+ });
85
+ ```
86
+
87
+ Returns: `Promise<void>`
88
+
89
+ #### `close()` / `disconnect()`
90
+
91
+ Close connection to FTP server.
92
+
93
+ ```javascript
94
+ await client.close();
95
+ ```
96
+
97
+ Returns: `Promise<void>`
98
+
99
+ ### File Operations
100
+
101
+ #### `upload(data, remotePath)`
102
+
103
+ Upload file to server.
104
+
105
+ ```javascript
106
+ // Upload string
107
+ await client.upload('Hello World!', '/path/file.txt');
108
+
109
+ // Upload Buffer
110
+ const buffer = Buffer.from('data');
111
+ await client.upload(buffer, '/path/file.bin');
112
+ ```
113
+
114
+ Returns: `Promise<void>`
115
+
116
+ #### `download(remotePath)`
117
+
118
+ Download file from server.
119
+
120
+ ```javascript
121
+ const data = await client.download('/path/file.txt');
122
+ console.log(data.toString()); // Convert Buffer to string
123
+ ```
124
+
125
+ Returns: `Promise<Buffer>`
126
+
127
+ #### `delete(path)`
128
+
129
+ Delete file.
130
+
131
+ ```javascript
132
+ await client.delete('/path/file.txt');
133
+ ```
134
+
135
+ Returns: `Promise<void>`
136
+
137
+ #### `rename(from, to)`
138
+
139
+ Rename or move file.
140
+
141
+ ```javascript
142
+ await client.rename('/old/path.txt', '/new/path.txt');
143
+ ```
144
+
145
+ Returns: `Promise<void>`
146
+
147
+ #### `size(path)`
148
+
149
+ Get file size in bytes.
150
+
151
+ ```javascript
152
+ const bytes = await client.size('/path/file.txt');
153
+ console.log(`File size: ${bytes} bytes`);
154
+ ```
155
+
156
+ Returns: `Promise<number>`
157
+
158
+ #### `exists(path)`
159
+
160
+ Check if file exists.
161
+
162
+ ```javascript
163
+ const exists = await client.exists('/path/file.txt');
164
+ console.log(exists ? 'File exists' : 'File not found');
165
+ ```
166
+
167
+ Returns: `Promise<boolean>`
168
+
169
+ #### `modifiedTime(path)`
170
+
171
+ Get file modification time.
172
+
173
+ ```javascript
174
+ const date = await client.modifiedTime('/path/file.txt');
175
+ console.log(`Last modified: ${date.toISOString()}`);
176
+ ```
177
+
178
+ Returns: `Promise<Date>`
179
+
180
+ ### Directory Operations
181
+
182
+ #### `list(path)`
183
+
184
+ List directory contents.
185
+
186
+ ```javascript
187
+ const listing = await client.list('/remote/path');
188
+ console.log(listing);
189
+ ```
190
+
191
+ Returns: `Promise<string>` - Raw directory listing
192
+
193
+ #### `cd(path)`
194
+
195
+ Change working directory.
196
+
197
+ ```javascript
198
+ await client.cd('/remote/path');
199
+ ```
200
+
201
+ Returns: `Promise<void>`
202
+
203
+ #### `pwd()`
204
+
205
+ Get current working directory.
206
+
207
+ ```javascript
208
+ const dir = await client.pwd();
209
+ console.log(`Current directory: ${dir}`);
210
+ ```
211
+
212
+ Returns: `Promise<string>`
213
+
214
+ #### `mkdir(path)`
215
+
216
+ Create directory.
217
+
218
+ ```javascript
219
+ await client.mkdir('/remote/newdir');
220
+ ```
221
+
222
+ Returns: `Promise<void>`
223
+
224
+ ### Utilities
225
+
226
+ #### `getStats()`
227
+
228
+ Get connection statistics.
229
+
230
+ ```javascript
231
+ const stats = client.getStats();
232
+ console.log(stats);
233
+ // {
234
+ // connected: true,
235
+ // authenticated: true,
236
+ // commandCount: 5,
237
+ // lastCommand: 'LIST .'
238
+ // }
239
+ ```
240
+
241
+ Returns: `Object`
242
+
243
+ #### `setDebug(enabled)`
244
+
245
+ Enable or disable debug mode at runtime.
246
+
247
+ ```javascript
248
+ client.setDebug(true); // Enable debug logging
249
+ client.setDebug(false); // Disable debug logging
250
+ ```
251
+
252
+ ## Events
253
+
254
+ The client extends EventEmitter and emits the following events:
255
+
256
+ ### `connected`
257
+
258
+ Fired when TCP connection is established.
259
+
260
+ ```javascript
261
+ client.on('connected', () => {
262
+ console.log('Connected to FTP server');
263
+ });
264
+ ```
265
+
266
+ ### `response`
267
+
268
+ Fired for each FTP response (useful for debugging).
269
+
270
+ ```javascript
271
+ client.on('response', (line) => {
272
+ console.log('FTP:', line);
273
+ });
274
+ ```
275
+
276
+ ### `error`
277
+
278
+ Fired on connection errors.
279
+
280
+ ```javascript
281
+ client.on('error', (err) => {
282
+ console.error('FTP Error:', err);
283
+ });
284
+ ```
285
+
286
+ ### `close`
287
+
288
+ Fired when connection is closed.
289
+
290
+ ```javascript
291
+ client.on('close', () => {
292
+ console.log('Connection closed');
293
+ });
294
+ ```
295
+
296
+ ## Debug Mode
297
+
298
+ Enable debug logging to troubleshoot FTP issues:
299
+
300
+ ```javascript
301
+ const client = new FTPClient({ debug: true });
302
+
303
+ client.on('response', (line) => {
304
+ console.log('FTP Response:', line);
305
+ });
306
+
307
+ await client.connect({ host: 'ftp.example.com', user: 'user', password: 'pass' });
308
+ // [FTP Debug] Connecting to ftp.example.com:21 as user
309
+ // [FTP Debug] TCP connection established
310
+ // [FTP Debug] <<< 220 Welcome to FTP server
311
+ // [FTP Debug] >>> USER user
312
+ // [FTP Debug] <<< 331 Password required
313
+ // [FTP Debug] >>> PASS ********
314
+ // [FTP Debug] <<< 230 Login successful
315
+ // [FTP Debug] Authentication successful
316
+ ```
317
+
318
+ ## Error Handling
319
+
320
+ All methods return promises and will reject on errors:
321
+
322
+ ```javascript
323
+ try {
324
+ await client.upload('data', '/readonly/file.txt');
325
+ } catch (err) {
326
+ if (err.message.includes('FTP Error 550')) {
327
+ console.error('Permission denied');
328
+ } else {
329
+ console.error('Upload failed:', err.message);
330
+ }
331
+ }
332
+ ```
333
+
334
+ ## Complete Example
335
+
336
+ ```javascript
337
+ const FTPClient = require('molex-ftp-client');
338
+
339
+ async function backupFile() {
340
+ const client = new FTPClient({
341
+ debug: true,
342
+ timeout: 60000
343
+ });
344
+
345
+ try {
346
+ // Connect
347
+ await client.connect({
348
+ host: 'ftp.myserver.com',
349
+ port: 21,
350
+ user: 'admin',
351
+ password: 'secret123'
352
+ });
353
+
354
+ console.log('Current directory:', await client.pwd());
355
+
356
+ // Check if file exists
357
+ const exists = await client.exists('/backup/data.json');
358
+ if (exists) {
359
+ // Download existing file
360
+ const oldData = await client.download('/backup/data.json');
361
+ console.log('Old backup size:', oldData.length, 'bytes');
362
+
363
+ // Get modification time
364
+ const modTime = await client.modifiedTime('/backup/data.json');
365
+ console.log('Last modified:', modTime.toISOString());
366
+
367
+ // Rename old backup
368
+ await client.rename('/backup/data.json', '/backup/data.old.json');
369
+ }
370
+
371
+ // Upload new backup
372
+ const newData = JSON.stringify({ timestamp: Date.now(), data: [1, 2, 3] });
373
+ await client.upload(newData, '/backup/data.json');
374
+ console.log('Backup uploaded successfully');
375
+
376
+ // Verify
377
+ const size = await client.size('/backup/data.json');
378
+ console.log('New backup size:', size, 'bytes');
379
+
380
+ // Get stats
381
+ const stats = client.getStats();
382
+ console.log('Commands executed:', stats.commandCount);
383
+
384
+ // Close connection
385
+ await client.close();
386
+ } catch (err) {
387
+ console.error('Backup failed:', err.message);
388
+ await client.close();
389
+ }
390
+ }
391
+
392
+ backupFile();
393
+ ```
394
+
395
+ ## License
396
+
397
+ ISC © Tony Wiedman / MolexWorks
398
+
package/index.js ADDED
@@ -0,0 +1,476 @@
1
+ const net = require('net');
2
+ const { EventEmitter } = require('events');
3
+
4
+ /**
5
+ * Lightweight FTP Client using native Node.js TCP sockets (net module)
6
+ */
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
+
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
+
465
+ /**
466
+ * Disconnect (alias for close)
467
+ * @returns {Promise<void>}
468
+ */
469
+ async disconnect() {
470
+ return this.close();
471
+ }
472
+ }
473
+
474
+ module.exports = FTPClient;
475
+ module.exports.FTPClient = FTPClient;
476
+ module.exports.default = FTPClient;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "molex-ftp-client",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight FTP client using native Node.js TCP sockets (net module) with zero dependencies",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test.js"
8
+ },
9
+ "keywords": [
10
+ "ftp",
11
+ "client",
12
+ "tcp",
13
+ "net",
14
+ "native",
15
+ "zero-dependencies",
16
+ "upload",
17
+ "download",
18
+ "file-transfer",
19
+ "passive-mode"
20
+ ],
21
+ "author": "Tony Wiedman",
22
+ "license": "ISC",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/molexworks/ftp-client"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/molexworks/ftp-client/issues"
29
+ },
30
+ "homepage": "https://github.com/molexworks/ftp-client#readme",
31
+ "engines": {
32
+ "node": ">=12.0.0"
33
+ }
34
+ }