recker 1.0.5 → 1.0.7
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/README.md +1 -1
- package/dist/ai/adaptive-timeout.d.ts +51 -0
- package/dist/ai/adaptive-timeout.d.ts.map +1 -0
- package/dist/ai/adaptive-timeout.js +208 -0
- package/dist/ai/client.d.ts +24 -0
- package/dist/ai/client.d.ts.map +1 -0
- package/dist/ai/client.js +289 -0
- package/dist/ai/index.d.ts +10 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +6 -0
- package/dist/ai/providers/anthropic.d.ts +64 -0
- package/dist/ai/providers/anthropic.d.ts.map +1 -0
- package/dist/ai/providers/anthropic.js +367 -0
- package/dist/ai/providers/base.d.ts +49 -0
- package/dist/ai/providers/base.d.ts.map +1 -0
- package/dist/ai/providers/base.js +145 -0
- package/dist/ai/providers/index.d.ts +7 -0
- package/dist/ai/providers/index.d.ts.map +1 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/openai.d.ts +65 -0
- package/dist/ai/providers/openai.d.ts.map +1 -0
- package/dist/ai/providers/openai.js +298 -0
- package/dist/ai/rate-limiter.d.ts +44 -0
- package/dist/ai/rate-limiter.d.ts.map +1 -0
- package/dist/ai/rate-limiter.js +212 -0
- package/dist/bench/generator.d.ts +19 -0
- package/dist/bench/generator.d.ts.map +1 -0
- package/dist/bench/generator.js +86 -0
- package/dist/bench/stats.d.ts +35 -0
- package/dist/bench/stats.d.ts.map +1 -0
- package/dist/bench/stats.js +60 -0
- package/dist/cli/handler.d.ts +11 -0
- package/dist/cli/handler.d.ts.map +1 -0
- package/dist/cli/handler.js +92 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +255 -0
- package/dist/cli/presets.d.ts +2 -0
- package/dist/cli/presets.d.ts.map +1 -0
- package/dist/cli/presets.js +67 -0
- package/dist/cli/tui/ai-chat.d.ts +3 -0
- package/dist/cli/tui/ai-chat.d.ts.map +1 -0
- package/dist/cli/tui/ai-chat.js +100 -0
- package/dist/cli/tui/load-dashboard.d.ts +3 -0
- package/dist/cli/tui/load-dashboard.d.ts.map +1 -0
- package/dist/cli/tui/load-dashboard.js +117 -0
- package/dist/cli/tui/shell.d.ts +27 -0
- package/dist/cli/tui/shell.d.ts.map +1 -0
- package/dist/cli/tui/shell.js +386 -0
- package/dist/cli/tui/websocket.d.ts +2 -0
- package/dist/cli/tui/websocket.d.ts.map +1 -0
- package/dist/cli/tui/websocket.js +87 -0
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/core/client.d.ts +1 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +4 -2
- package/dist/core/request-promise.d.ts +1 -1
- package/dist/core/request-promise.d.ts.map +1 -1
- package/dist/mcp/contract.d.ts +1 -1
- package/dist/mcp/contract.d.ts.map +1 -1
- package/dist/protocols/ftp.d.ts +28 -5
- package/dist/protocols/ftp.d.ts.map +1 -1
- package/dist/protocols/ftp.js +549 -136
- package/dist/protocols/sftp.d.ts +4 -2
- package/dist/protocols/sftp.d.ts.map +1 -1
- package/dist/protocols/sftp.js +16 -2
- package/dist/protocols/telnet.d.ts +37 -5
- package/dist/protocols/telnet.d.ts.map +1 -1
- package/dist/protocols/telnet.js +434 -58
- package/dist/scrape/document.d.ts.map +1 -1
- package/dist/scrape/document.js +7 -12
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/mock-udp-server.d.ts +44 -0
- package/dist/testing/mock-udp-server.d.ts.map +1 -0
- package/dist/testing/mock-udp-server.js +188 -0
- package/dist/transport/base-udp.d.ts +36 -0
- package/dist/transport/base-udp.d.ts.map +1 -0
- package/dist/transport/base-udp.js +188 -0
- package/dist/transport/udp-response.d.ts +65 -0
- package/dist/transport/udp-response.d.ts.map +1 -0
- package/dist/transport/udp-response.js +269 -0
- package/dist/transport/udp.d.ts +22 -0
- package/dist/transport/udp.d.ts.map +1 -0
- package/dist/transport/udp.js +260 -0
- package/dist/types/ai.d.ts +268 -0
- package/dist/types/ai.d.ts.map +1 -0
- package/dist/types/ai.js +1 -0
- package/dist/types/udp.d.ts +138 -0
- package/dist/types/udp.d.ts.map +1 -0
- package/dist/types/udp.js +1 -0
- package/dist/udp/index.d.ts +6 -0
- package/dist/udp/index.d.ts.map +1 -0
- package/dist/udp/index.js +3 -0
- package/dist/utils/chart.d.ts +15 -0
- package/dist/utils/chart.d.ts.map +1 -0
- package/dist/utils/chart.js +94 -0
- package/dist/utils/colors.d.ts +27 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/optional-require.d.ts +20 -0
- package/dist/utils/optional-require.d.ts.map +1 -0
- package/dist/utils/optional-require.js +105 -0
- package/package.json +53 -12
package/dist/protocols/ftp.js
CHANGED
|
@@ -1,27 +1,97 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Socket } from 'node:net';
|
|
2
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
2
3
|
import { Readable, Writable } from 'node:stream';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { createWriteStream, createReadStream } from 'node:fs';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
const ResponseCode = {
|
|
7
|
+
RESTART_MARKER: 110,
|
|
8
|
+
SERVICE_READY_IN: 120,
|
|
9
|
+
DATA_CONNECTION_OPEN: 125,
|
|
10
|
+
FILE_STATUS_OK: 150,
|
|
11
|
+
OK: 200,
|
|
12
|
+
SUPERFLUOUS: 202,
|
|
13
|
+
SYSTEM_STATUS: 211,
|
|
14
|
+
DIRECTORY_STATUS: 212,
|
|
15
|
+
FILE_STATUS: 213,
|
|
16
|
+
HELP_MESSAGE: 214,
|
|
17
|
+
SYSTEM_TYPE: 215,
|
|
18
|
+
SERVICE_READY: 220,
|
|
19
|
+
SERVICE_CLOSING: 221,
|
|
20
|
+
DATA_CONNECTION_READY: 225,
|
|
21
|
+
CLOSING_DATA_CONNECTION: 226,
|
|
22
|
+
ENTERING_PASSIVE: 227,
|
|
23
|
+
ENTERING_EXTENDED_PASSIVE: 229,
|
|
24
|
+
USER_LOGGED_IN: 230,
|
|
25
|
+
AUTH_OK: 234,
|
|
26
|
+
FILE_ACTION_OK: 250,
|
|
27
|
+
PATHNAME_CREATED: 257,
|
|
28
|
+
NEED_PASSWORD: 331,
|
|
29
|
+
NEED_ACCOUNT: 332,
|
|
30
|
+
FILE_ACTION_PENDING: 350,
|
|
31
|
+
SERVICE_UNAVAILABLE: 421,
|
|
32
|
+
CANT_OPEN_DATA: 425,
|
|
33
|
+
CONNECTION_CLOSED: 426,
|
|
34
|
+
FILE_BUSY: 450,
|
|
35
|
+
LOCAL_ERROR: 451,
|
|
36
|
+
INSUFFICIENT_SPACE: 452,
|
|
37
|
+
SYNTAX_ERROR: 500,
|
|
38
|
+
SYNTAX_ERROR_PARAMS: 501,
|
|
39
|
+
NOT_IMPLEMENTED: 502,
|
|
40
|
+
BAD_SEQUENCE: 503,
|
|
41
|
+
NOT_IMPLEMENTED_PARAM: 504,
|
|
42
|
+
NOT_LOGGED_IN: 530,
|
|
43
|
+
NEED_ACCOUNT_STORE: 532,
|
|
44
|
+
FILE_NOT_FOUND: 550,
|
|
45
|
+
PAGE_TYPE_UNKNOWN: 551,
|
|
46
|
+
EXCEEDED_ALLOCATION: 552,
|
|
47
|
+
FILE_NAME_NOT_ALLOWED: 553,
|
|
48
|
+
};
|
|
49
|
+
export class FTP extends EventEmitter {
|
|
50
|
+
controlSocket = null;
|
|
5
51
|
config;
|
|
6
52
|
connected = false;
|
|
53
|
+
secureConnection = false;
|
|
54
|
+
responseBuffer = '';
|
|
7
55
|
onProgress;
|
|
56
|
+
currentTransferBytes = 0;
|
|
57
|
+
socketFactory = null;
|
|
8
58
|
constructor(config) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
59
|
+
super();
|
|
60
|
+
const isImplicit = config.secure === 'implicit';
|
|
61
|
+
const defaultPort = isImplicit ? 990 : 21;
|
|
62
|
+
this.config = {
|
|
63
|
+
host: config.host,
|
|
64
|
+
port: config.port ?? defaultPort,
|
|
65
|
+
user: config.user ?? 'anonymous',
|
|
66
|
+
password: config.password ?? 'anonymous@',
|
|
67
|
+
secure: config.secure ?? false,
|
|
68
|
+
timeout: config.timeout ?? 30000,
|
|
69
|
+
verbose: config.verbose ?? false,
|
|
70
|
+
tlsOptions: config.tlsOptions,
|
|
71
|
+
};
|
|
72
|
+
this.socketFactory = config._socketFactory ?? null;
|
|
73
|
+
}
|
|
74
|
+
createSocket() {
|
|
75
|
+
return this.socketFactory ? this.socketFactory() : new Socket();
|
|
14
76
|
}
|
|
15
77
|
async connect() {
|
|
16
78
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
79
|
+
if (this.config.secure === 'implicit') {
|
|
80
|
+
await this.connectImplicitTLS();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
await this.connectPlain();
|
|
84
|
+
if (this.config.secure === true) {
|
|
85
|
+
await this.upgradeToTLS();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const welcome = await this.readResponse();
|
|
89
|
+
this.debug('Welcome: %s', welcome.message);
|
|
90
|
+
if (welcome.code !== ResponseCode.SERVICE_READY) {
|
|
91
|
+
throw new Error(`Server not ready: ${welcome.message}`);
|
|
92
|
+
}
|
|
93
|
+
await this.authenticate();
|
|
94
|
+
await this.sendCommand('TYPE I');
|
|
25
95
|
this.connected = true;
|
|
26
96
|
return {
|
|
27
97
|
success: true,
|
|
@@ -29,6 +99,7 @@ export class FTP {
|
|
|
29
99
|
};
|
|
30
100
|
}
|
|
31
101
|
catch (error) {
|
|
102
|
+
this.cleanup();
|
|
32
103
|
return {
|
|
33
104
|
success: false,
|
|
34
105
|
message: error instanceof Error ? error.message : 'Connection failed'
|
|
@@ -36,20 +107,15 @@ export class FTP {
|
|
|
36
107
|
}
|
|
37
108
|
}
|
|
38
109
|
isConnected() {
|
|
39
|
-
return this.connected && !this.
|
|
110
|
+
return this.connected && this.controlSocket !== null && !this.controlSocket.destroyed;
|
|
40
111
|
}
|
|
41
112
|
async list(path = '/') {
|
|
42
113
|
this.ensureConnected();
|
|
43
114
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
size: file.size,
|
|
49
|
-
modifiedAt: file.modifiedAt,
|
|
50
|
-
permissions: file.permissions?.toString(),
|
|
51
|
-
rawModifiedAt: file.rawModifiedAt
|
|
52
|
-
}));
|
|
115
|
+
const dataSocket = await this.openDataConnection();
|
|
116
|
+
await this.sendCommand(`LIST ${path}`);
|
|
117
|
+
const data = await this.readDataConnection(dataSocket);
|
|
118
|
+
const items = this.parseDirectoryListing(data);
|
|
53
119
|
return {
|
|
54
120
|
success: true,
|
|
55
121
|
data: items
|
|
@@ -65,25 +131,11 @@ export class FTP {
|
|
|
65
131
|
async download(remotePath, localPath) {
|
|
66
132
|
this.ensureConnected();
|
|
67
133
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
bytes: info.bytes,
|
|
72
|
-
bytesOverall: info.bytesOverall,
|
|
73
|
-
name: info.name,
|
|
74
|
-
type: 'download'
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
await this.client.downloadTo(localPath, remotePath);
|
|
79
|
-
this.client.trackProgress();
|
|
80
|
-
return {
|
|
81
|
-
success: true,
|
|
82
|
-
message: `Downloaded ${remotePath} to ${localPath}`
|
|
83
|
-
};
|
|
134
|
+
const writeStream = createWriteStream(localPath);
|
|
135
|
+
const result = await this.downloadToStream(remotePath, writeStream);
|
|
136
|
+
return result;
|
|
84
137
|
}
|
|
85
138
|
catch (error) {
|
|
86
|
-
this.client.trackProgress();
|
|
87
139
|
return {
|
|
88
140
|
success: false,
|
|
89
141
|
message: error instanceof Error ? error.message : 'Download failed'
|
|
@@ -93,25 +145,42 @@ export class FTP {
|
|
|
93
145
|
async downloadToStream(remotePath, stream) {
|
|
94
146
|
this.ensureConnected();
|
|
95
147
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
148
|
+
const sizeResult = await this.size(remotePath);
|
|
149
|
+
const totalSize = sizeResult.data ?? 0;
|
|
150
|
+
const dataSocket = await this.openDataConnection();
|
|
151
|
+
await this.sendCommand(`RETR ${remotePath}`);
|
|
152
|
+
this.currentTransferBytes = 0;
|
|
153
|
+
const fileName = remotePath.split('/').pop() || remotePath;
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
dataSocket.on('data', (chunk) => {
|
|
156
|
+
this.currentTransferBytes += chunk.length;
|
|
157
|
+
stream.write(chunk);
|
|
158
|
+
if (this.onProgress) {
|
|
159
|
+
this.onProgress({
|
|
160
|
+
bytes: chunk.length,
|
|
161
|
+
bytesOverall: this.currentTransferBytes,
|
|
162
|
+
name: fileName,
|
|
163
|
+
type: 'download'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
dataSocket.on('end', () => {
|
|
168
|
+
stream.end();
|
|
169
|
+
resolve();
|
|
104
170
|
});
|
|
171
|
+
dataSocket.on('error', reject);
|
|
172
|
+
});
|
|
173
|
+
const response = await this.readResponse();
|
|
174
|
+
if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
|
|
175
|
+
response.code !== ResponseCode.FILE_ACTION_OK) {
|
|
176
|
+
throw new Error(response.message);
|
|
105
177
|
}
|
|
106
|
-
await this.client.downloadTo(stream, remotePath);
|
|
107
|
-
this.client.trackProgress();
|
|
108
178
|
return {
|
|
109
179
|
success: true,
|
|
110
180
|
message: `Downloaded ${remotePath}`
|
|
111
181
|
};
|
|
112
182
|
}
|
|
113
183
|
catch (error) {
|
|
114
|
-
this.client.trackProgress();
|
|
115
184
|
return {
|
|
116
185
|
success: false,
|
|
117
186
|
message: error instanceof Error ? error.message : 'Download failed'
|
|
@@ -128,7 +197,13 @@ export class FTP {
|
|
|
128
197
|
callback();
|
|
129
198
|
}
|
|
130
199
|
});
|
|
131
|
-
await this.
|
|
200
|
+
const result = await this.downloadToStream(remotePath, stream);
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
message: result.message
|
|
205
|
+
};
|
|
206
|
+
}
|
|
132
207
|
return {
|
|
133
208
|
success: true,
|
|
134
209
|
data: Buffer.concat(chunks)
|
|
@@ -144,25 +219,10 @@ export class FTP {
|
|
|
144
219
|
async upload(localPath, remotePath) {
|
|
145
220
|
this.ensureConnected();
|
|
146
221
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.onProgress?.({
|
|
150
|
-
bytes: info.bytes,
|
|
151
|
-
bytesOverall: info.bytesOverall,
|
|
152
|
-
name: info.name,
|
|
153
|
-
type: 'upload'
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
await this.client.uploadFrom(localPath, remotePath);
|
|
158
|
-
this.client.trackProgress();
|
|
159
|
-
return {
|
|
160
|
-
success: true,
|
|
161
|
-
message: `Uploaded ${localPath} to ${remotePath}`
|
|
162
|
-
};
|
|
222
|
+
const readStream = createReadStream(localPath);
|
|
223
|
+
return await this.uploadFromStream(readStream, remotePath);
|
|
163
224
|
}
|
|
164
225
|
catch (error) {
|
|
165
|
-
this.client.trackProgress();
|
|
166
226
|
return {
|
|
167
227
|
success: false,
|
|
168
228
|
message: error instanceof Error ? error.message : 'Upload failed'
|
|
@@ -172,25 +232,41 @@ export class FTP {
|
|
|
172
232
|
async uploadFromStream(stream, remotePath) {
|
|
173
233
|
this.ensureConnected();
|
|
174
234
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
const dataSocket = await this.openDataConnection();
|
|
236
|
+
await this.sendCommand(`STOR ${remotePath}`);
|
|
237
|
+
this.currentTransferBytes = 0;
|
|
238
|
+
const fileName = remotePath.split('/').pop() || remotePath;
|
|
239
|
+
await new Promise((resolve, reject) => {
|
|
240
|
+
stream.on('data', (chunk) => {
|
|
241
|
+
this.currentTransferBytes += chunk.length;
|
|
242
|
+
dataSocket.write(chunk);
|
|
243
|
+
if (this.onProgress) {
|
|
244
|
+
this.onProgress({
|
|
245
|
+
bytes: chunk.length,
|
|
246
|
+
bytesOverall: this.currentTransferBytes,
|
|
247
|
+
name: fileName,
|
|
248
|
+
type: 'upload'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
183
251
|
});
|
|
252
|
+
stream.on('end', () => {
|
|
253
|
+
dataSocket.end();
|
|
254
|
+
resolve();
|
|
255
|
+
});
|
|
256
|
+
stream.on('error', reject);
|
|
257
|
+
dataSocket.on('error', reject);
|
|
258
|
+
});
|
|
259
|
+
const response = await this.readResponse();
|
|
260
|
+
if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
|
|
261
|
+
response.code !== ResponseCode.FILE_ACTION_OK) {
|
|
262
|
+
throw new Error(response.message);
|
|
184
263
|
}
|
|
185
|
-
await this.client.uploadFrom(stream, remotePath);
|
|
186
|
-
this.client.trackProgress();
|
|
187
264
|
return {
|
|
188
265
|
success: true,
|
|
189
266
|
message: `Uploaded to ${remotePath}`
|
|
190
267
|
};
|
|
191
268
|
}
|
|
192
269
|
catch (error) {
|
|
193
|
-
this.client.trackProgress();
|
|
194
270
|
return {
|
|
195
271
|
success: false,
|
|
196
272
|
message: error instanceof Error ? error.message : 'Upload failed'
|
|
@@ -198,30 +274,20 @@ export class FTP {
|
|
|
198
274
|
}
|
|
199
275
|
}
|
|
200
276
|
async uploadFromBuffer(data, remotePath) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const stream = Readable.from(buffer);
|
|
205
|
-
await this.client.uploadFrom(stream, remotePath);
|
|
206
|
-
return {
|
|
207
|
-
success: true,
|
|
208
|
-
message: `Uploaded to ${remotePath}`
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
return {
|
|
213
|
-
success: false,
|
|
214
|
-
message: error instanceof Error ? error.message : 'Upload failed'
|
|
215
|
-
};
|
|
216
|
-
}
|
|
277
|
+
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
278
|
+
const stream = Readable.from(buffer);
|
|
279
|
+
return this.uploadFromStream(stream, remotePath);
|
|
217
280
|
}
|
|
218
281
|
async delete(remotePath) {
|
|
219
282
|
this.ensureConnected();
|
|
220
283
|
try {
|
|
221
|
-
await this.
|
|
284
|
+
const response = await this.sendCommand(`DELE ${remotePath}`);
|
|
222
285
|
return {
|
|
223
|
-
success:
|
|
224
|
-
|
|
286
|
+
success: response.code === ResponseCode.FILE_ACTION_OK,
|
|
287
|
+
code: response.code,
|
|
288
|
+
message: response.code === ResponseCode.FILE_ACTION_OK
|
|
289
|
+
? `Deleted ${remotePath}`
|
|
290
|
+
: response.message
|
|
225
291
|
};
|
|
226
292
|
}
|
|
227
293
|
catch (error) {
|
|
@@ -234,10 +300,29 @@ export class FTP {
|
|
|
234
300
|
async mkdir(remotePath, recursive = true) {
|
|
235
301
|
this.ensureConnected();
|
|
236
302
|
try {
|
|
237
|
-
|
|
303
|
+
if (recursive) {
|
|
304
|
+
const parts = remotePath.split('/').filter(Boolean);
|
|
305
|
+
let currentPath = '';
|
|
306
|
+
for (const part of parts) {
|
|
307
|
+
currentPath += '/' + part;
|
|
308
|
+
try {
|
|
309
|
+
await this.sendCommand(`MKD ${currentPath}`);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
message: `Created directory ${remotePath}`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const response = await this.sendCommand(`MKD ${remotePath}`);
|
|
238
320
|
return {
|
|
239
|
-
success:
|
|
240
|
-
|
|
321
|
+
success: response.code === ResponseCode.PATHNAME_CREATED,
|
|
322
|
+
code: response.code,
|
|
323
|
+
message: response.code === ResponseCode.PATHNAME_CREATED
|
|
324
|
+
? `Created directory ${remotePath}`
|
|
325
|
+
: response.message
|
|
241
326
|
};
|
|
242
327
|
}
|
|
243
328
|
catch (error) {
|
|
@@ -250,10 +335,13 @@ export class FTP {
|
|
|
250
335
|
async rmdir(remotePath) {
|
|
251
336
|
this.ensureConnected();
|
|
252
337
|
try {
|
|
253
|
-
await this.
|
|
338
|
+
const response = await this.sendCommand(`RMD ${remotePath}`);
|
|
254
339
|
return {
|
|
255
|
-
success:
|
|
256
|
-
|
|
340
|
+
success: response.code === ResponseCode.FILE_ACTION_OK,
|
|
341
|
+
code: response.code,
|
|
342
|
+
message: response.code === ResponseCode.FILE_ACTION_OK
|
|
343
|
+
? `Removed directory ${remotePath}`
|
|
344
|
+
: response.message
|
|
257
345
|
};
|
|
258
346
|
}
|
|
259
347
|
catch (error) {
|
|
@@ -266,10 +354,17 @@ export class FTP {
|
|
|
266
354
|
async rename(oldPath, newPath) {
|
|
267
355
|
this.ensureConnected();
|
|
268
356
|
try {
|
|
269
|
-
await this.
|
|
357
|
+
const rnfrResponse = await this.sendCommand(`RNFR ${oldPath}`);
|
|
358
|
+
if (rnfrResponse.code !== ResponseCode.FILE_ACTION_PENDING) {
|
|
359
|
+
throw new Error(rnfrResponse.message);
|
|
360
|
+
}
|
|
361
|
+
const rntoResponse = await this.sendCommand(`RNTO ${newPath}`);
|
|
270
362
|
return {
|
|
271
|
-
success:
|
|
272
|
-
|
|
363
|
+
success: rntoResponse.code === ResponseCode.FILE_ACTION_OK,
|
|
364
|
+
code: rntoResponse.code,
|
|
365
|
+
message: rntoResponse.code === ResponseCode.FILE_ACTION_OK
|
|
366
|
+
? `Renamed ${oldPath} to ${newPath}`
|
|
367
|
+
: rntoResponse.message
|
|
273
368
|
};
|
|
274
369
|
}
|
|
275
370
|
catch (error) {
|
|
@@ -282,10 +377,12 @@ export class FTP {
|
|
|
282
377
|
async pwd() {
|
|
283
378
|
this.ensureConnected();
|
|
284
379
|
try {
|
|
285
|
-
const
|
|
380
|
+
const response = await this.sendCommand('PWD');
|
|
381
|
+
const match = response.message.match(/"([^"]+)"/);
|
|
382
|
+
const path = match ? match[1] : '/';
|
|
286
383
|
return {
|
|
287
384
|
success: true,
|
|
288
|
-
data:
|
|
385
|
+
data: path
|
|
289
386
|
};
|
|
290
387
|
}
|
|
291
388
|
catch (error) {
|
|
@@ -298,10 +395,13 @@ export class FTP {
|
|
|
298
395
|
async cd(remotePath) {
|
|
299
396
|
this.ensureConnected();
|
|
300
397
|
try {
|
|
301
|
-
await this.
|
|
398
|
+
const response = await this.sendCommand(`CWD ${remotePath}`);
|
|
302
399
|
return {
|
|
303
|
-
success:
|
|
304
|
-
|
|
400
|
+
success: response.code === ResponseCode.FILE_ACTION_OK,
|
|
401
|
+
code: response.code,
|
|
402
|
+
message: response.code === ResponseCode.FILE_ACTION_OK
|
|
403
|
+
? `Changed to ${remotePath}`
|
|
404
|
+
: response.message
|
|
305
405
|
};
|
|
306
406
|
}
|
|
307
407
|
catch (error) {
|
|
@@ -314,9 +414,10 @@ export class FTP {
|
|
|
314
414
|
async size(remotePath) {
|
|
315
415
|
this.ensureConnected();
|
|
316
416
|
try {
|
|
317
|
-
const
|
|
417
|
+
const response = await this.sendCommand(`SIZE ${remotePath}`);
|
|
418
|
+
const size = parseInt(response.message.split(' ').pop() || '0', 10);
|
|
318
419
|
return {
|
|
319
|
-
success:
|
|
420
|
+
success: response.code === ResponseCode.FILE_STATUS,
|
|
320
421
|
data: size
|
|
321
422
|
};
|
|
322
423
|
}
|
|
@@ -332,8 +433,10 @@ export class FTP {
|
|
|
332
433
|
try {
|
|
333
434
|
const parentPath = remotePath.split('/').slice(0, -1).join('/') || '/';
|
|
334
435
|
const fileName = remotePath.split('/').pop() || '';
|
|
335
|
-
const
|
|
336
|
-
|
|
436
|
+
const result = await this.list(parentPath);
|
|
437
|
+
if (!result.success || !result.data)
|
|
438
|
+
return false;
|
|
439
|
+
return result.data.some((f) => f.name === fileName);
|
|
337
440
|
}
|
|
338
441
|
catch {
|
|
339
442
|
return false;
|
|
@@ -345,28 +448,338 @@ export class FTP {
|
|
|
345
448
|
}
|
|
346
449
|
async close() {
|
|
347
450
|
this.connected = false;
|
|
348
|
-
this.
|
|
451
|
+
if (this.controlSocket) {
|
|
452
|
+
try {
|
|
453
|
+
await this.sendCommand('QUIT');
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
}
|
|
457
|
+
this.cleanup();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
getSocket() {
|
|
461
|
+
if (!this.controlSocket) {
|
|
462
|
+
throw new Error('Not connected. Call connect() first.');
|
|
463
|
+
}
|
|
464
|
+
return this.controlSocket;
|
|
465
|
+
}
|
|
466
|
+
async connectPlain() {
|
|
467
|
+
return new Promise((resolve, reject) => {
|
|
468
|
+
const socket = this.createSocket();
|
|
469
|
+
socket.setTimeout(this.config.timeout);
|
|
470
|
+
socket.on('connect', () => {
|
|
471
|
+
this.controlSocket = socket;
|
|
472
|
+
resolve();
|
|
473
|
+
});
|
|
474
|
+
socket.on('error', (err) => {
|
|
475
|
+
reject(err);
|
|
476
|
+
});
|
|
477
|
+
socket.on('timeout', () => {
|
|
478
|
+
socket.destroy();
|
|
479
|
+
reject(new Error('Connection timeout'));
|
|
480
|
+
});
|
|
481
|
+
socket.connect(this.config.port, this.config.host);
|
|
482
|
+
});
|
|
349
483
|
}
|
|
350
|
-
|
|
351
|
-
return
|
|
484
|
+
async connectImplicitTLS() {
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const options = {
|
|
487
|
+
host: this.config.host,
|
|
488
|
+
port: this.config.port,
|
|
489
|
+
timeout: this.config.timeout,
|
|
490
|
+
rejectUnauthorized: false,
|
|
491
|
+
...this.config.tlsOptions,
|
|
492
|
+
};
|
|
493
|
+
const socket = tlsConnect(options, () => {
|
|
494
|
+
this.controlSocket = socket;
|
|
495
|
+
this.secureConnection = true;
|
|
496
|
+
resolve();
|
|
497
|
+
});
|
|
498
|
+
socket.on('error', reject);
|
|
499
|
+
socket.setTimeout(this.config.timeout);
|
|
500
|
+
socket.on('timeout', () => {
|
|
501
|
+
socket.destroy();
|
|
502
|
+
reject(new Error('Connection timeout'));
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async upgradeToTLS() {
|
|
507
|
+
const authResponse = await this.sendCommand('AUTH TLS');
|
|
508
|
+
if (authResponse.code !== ResponseCode.AUTH_OK) {
|
|
509
|
+
throw new Error(`AUTH TLS failed: ${authResponse.message}`);
|
|
510
|
+
}
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const options = {
|
|
513
|
+
socket: this.controlSocket,
|
|
514
|
+
rejectUnauthorized: false,
|
|
515
|
+
...this.config.tlsOptions,
|
|
516
|
+
};
|
|
517
|
+
const tlsSocket = tlsConnect(options, () => {
|
|
518
|
+
this.controlSocket = tlsSocket;
|
|
519
|
+
this.secureConnection = true;
|
|
520
|
+
resolve();
|
|
521
|
+
});
|
|
522
|
+
tlsSocket.on('error', reject);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async authenticate() {
|
|
526
|
+
const userResponse = await this.sendCommand(`USER ${this.config.user}`);
|
|
527
|
+
if (userResponse.code === ResponseCode.USER_LOGGED_IN) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (userResponse.code !== ResponseCode.NEED_PASSWORD) {
|
|
531
|
+
throw new Error(`Authentication failed: ${userResponse.message}`);
|
|
532
|
+
}
|
|
533
|
+
const passResponse = await this.sendCommand(`PASS ${this.config.password}`);
|
|
534
|
+
if (passResponse.code !== ResponseCode.USER_LOGGED_IN) {
|
|
535
|
+
throw new Error(`Authentication failed: ${passResponse.message}`);
|
|
536
|
+
}
|
|
537
|
+
if (this.secureConnection) {
|
|
538
|
+
await this.sendCommand('PBSZ 0');
|
|
539
|
+
await this.sendCommand('PROT P');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async openDataConnection() {
|
|
543
|
+
const pasvResponse = await this.sendCommand('PASV');
|
|
544
|
+
if (pasvResponse.code !== ResponseCode.ENTERING_PASSIVE) {
|
|
545
|
+
throw new Error(`PASV failed: ${pasvResponse.message}`);
|
|
546
|
+
}
|
|
547
|
+
const match = pasvResponse.message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
|
|
548
|
+
if (!match) {
|
|
549
|
+
throw new Error('Failed to parse PASV response');
|
|
550
|
+
}
|
|
551
|
+
const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
|
|
552
|
+
const port = parseInt(match[5], 10) * 256 + parseInt(match[6], 10);
|
|
553
|
+
this.debug('PASV: %s:%d', host, port);
|
|
554
|
+
return new Promise((resolve, reject) => {
|
|
555
|
+
if (this.secureConnection) {
|
|
556
|
+
const options = {
|
|
557
|
+
host,
|
|
558
|
+
port,
|
|
559
|
+
rejectUnauthorized: false,
|
|
560
|
+
...this.config.tlsOptions,
|
|
561
|
+
};
|
|
562
|
+
const socket = tlsConnect(options, () => {
|
|
563
|
+
resolve(socket);
|
|
564
|
+
});
|
|
565
|
+
socket.on('error', reject);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const socket = this.createSocket();
|
|
569
|
+
socket.on('connect', () => resolve(socket));
|
|
570
|
+
socket.on('error', reject);
|
|
571
|
+
socket.connect(port, host);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async readDataConnection(socket) {
|
|
576
|
+
return new Promise((resolve, reject) => {
|
|
577
|
+
const chunks = [];
|
|
578
|
+
socket.on('data', (chunk) => {
|
|
579
|
+
chunks.push(chunk);
|
|
580
|
+
});
|
|
581
|
+
socket.on('end', async () => {
|
|
582
|
+
const data = Buffer.concat(chunks).toString('utf8');
|
|
583
|
+
try {
|
|
584
|
+
const response = await this.readResponse();
|
|
585
|
+
if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
|
|
586
|
+
response.code !== ResponseCode.FILE_ACTION_OK) {
|
|
587
|
+
reject(new Error(response.message));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
resolve(data);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
reject(err);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
socket.on('error', reject);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async sendCommand(command) {
|
|
601
|
+
if (!this.controlSocket) {
|
|
602
|
+
throw new Error('Not connected');
|
|
603
|
+
}
|
|
604
|
+
const displayCommand = command.startsWith('PASS ')
|
|
605
|
+
? 'PASS ****'
|
|
606
|
+
: command;
|
|
607
|
+
this.debug('>>> %s', displayCommand);
|
|
608
|
+
return new Promise((resolve, reject) => {
|
|
609
|
+
this.controlSocket.write(command + '\r\n', 'utf8', async (err) => {
|
|
610
|
+
if (err) {
|
|
611
|
+
reject(err);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const response = await this.readResponse();
|
|
616
|
+
if (response.code >= 400) {
|
|
617
|
+
reject(new Error(response.message));
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
resolve(response);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
reject(error);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async readResponse() {
|
|
630
|
+
return new Promise((resolve, reject) => {
|
|
631
|
+
const onData = (data) => {
|
|
632
|
+
this.responseBuffer += data.toString('utf8');
|
|
633
|
+
const lines = this.responseBuffer.split('\r\n');
|
|
634
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
635
|
+
const line = lines[i];
|
|
636
|
+
const match = line.match(/^(\d{3})([ -])(.*)/);
|
|
637
|
+
if (match) {
|
|
638
|
+
const code = parseInt(match[1], 10);
|
|
639
|
+
const isFinal = match[2] === ' ';
|
|
640
|
+
const message = match[3];
|
|
641
|
+
if (isFinal) {
|
|
642
|
+
this.responseBuffer = lines.slice(i + 1).join('\r\n');
|
|
643
|
+
this.controlSocket.removeListener('data', onData);
|
|
644
|
+
this.controlSocket.removeListener('error', onError);
|
|
645
|
+
this.debug('<<< %d %s', code, message);
|
|
646
|
+
resolve({ code, message: `${code} ${message}` });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const onError = (err) => {
|
|
653
|
+
this.controlSocket.removeListener('data', onData);
|
|
654
|
+
reject(err);
|
|
655
|
+
};
|
|
656
|
+
this.controlSocket.on('data', onData);
|
|
657
|
+
this.controlSocket.on('error', onError);
|
|
658
|
+
onData(Buffer.alloc(0));
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
parseDirectoryListing(data) {
|
|
662
|
+
const lines = data.split('\r\n').filter(Boolean);
|
|
663
|
+
const items = [];
|
|
664
|
+
for (const line of lines) {
|
|
665
|
+
const item = this.parseListLine(line);
|
|
666
|
+
if (item) {
|
|
667
|
+
items.push(item);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return items;
|
|
671
|
+
}
|
|
672
|
+
parseListLine(line) {
|
|
673
|
+
const unixMatch = line.match(/^([d\-l])([rwx\-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+[\d:]+)\s+(.+)$/);
|
|
674
|
+
if (unixMatch) {
|
|
675
|
+
const typeChar = unixMatch[1];
|
|
676
|
+
const permissions = unixMatch[2];
|
|
677
|
+
const size = parseInt(unixMatch[3], 10);
|
|
678
|
+
const rawDate = unixMatch[4];
|
|
679
|
+
const name = unixMatch[5];
|
|
680
|
+
return {
|
|
681
|
+
name,
|
|
682
|
+
type: this.mapFileType(typeChar),
|
|
683
|
+
size,
|
|
684
|
+
permissions,
|
|
685
|
+
rawModifiedAt: rawDate,
|
|
686
|
+
modifiedAt: this.parseDate(rawDate)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const dosMatch = line.match(/^(\d{2}-\d{2}-\d{2})\s+(\d{1,2}:\d{2}[AP]M)\s+(<DIR>|\d+)\s+(.+)$/i);
|
|
690
|
+
if (dosMatch) {
|
|
691
|
+
const date = dosMatch[1];
|
|
692
|
+
const time = dosMatch[2];
|
|
693
|
+
const sizeOrDir = dosMatch[3];
|
|
694
|
+
const name = dosMatch[4];
|
|
695
|
+
const isDir = sizeOrDir.toUpperCase() === '<DIR>';
|
|
696
|
+
return {
|
|
697
|
+
name,
|
|
698
|
+
type: isDir ? 'directory' : 'file',
|
|
699
|
+
size: isDir ? 0 : parseInt(sizeOrDir, 10),
|
|
700
|
+
rawModifiedAt: `${date} ${time}`,
|
|
701
|
+
modifiedAt: this.parseDosDate(date, time)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (line.trim() && !line.includes(' ')) {
|
|
705
|
+
return {
|
|
706
|
+
name: line.trim(),
|
|
707
|
+
type: 'unknown',
|
|
708
|
+
size: 0
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
mapFileType(typeChar) {
|
|
714
|
+
switch (typeChar) {
|
|
715
|
+
case '-': return 'file';
|
|
716
|
+
case 'd': return 'directory';
|
|
717
|
+
case 'l': return 'link';
|
|
718
|
+
default: return 'unknown';
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
parseDate(rawDate) {
|
|
722
|
+
try {
|
|
723
|
+
const now = new Date();
|
|
724
|
+
const parts = rawDate.trim().split(/\s+/);
|
|
725
|
+
if (parts.length >= 3) {
|
|
726
|
+
const month = parts[0];
|
|
727
|
+
const day = parseInt(parts[1], 10);
|
|
728
|
+
const timeOrYear = parts[2];
|
|
729
|
+
if (timeOrYear.includes(':')) {
|
|
730
|
+
const [hours, minutes] = timeOrYear.split(':').map(Number);
|
|
731
|
+
const date = new Date(`${month} ${day} ${now.getFullYear()} ${hours}:${minutes}`);
|
|
732
|
+
if (date > now) {
|
|
733
|
+
date.setFullYear(date.getFullYear() - 1);
|
|
734
|
+
}
|
|
735
|
+
return date;
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
return new Date(`${month} ${day} ${timeOrYear}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
}
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
parseDosDate(date, time) {
|
|
747
|
+
try {
|
|
748
|
+
const [month, day, year] = date.split('-').map(Number);
|
|
749
|
+
const fullYear = year < 70 ? 2000 + year : 1900 + year;
|
|
750
|
+
const timeMatch = time.match(/(\d{1,2}):(\d{2})([AP]M)/i);
|
|
751
|
+
if (timeMatch) {
|
|
752
|
+
let hours = parseInt(timeMatch[1], 10);
|
|
753
|
+
const minutes = parseInt(timeMatch[2], 10);
|
|
754
|
+
const isPM = timeMatch[3].toUpperCase() === 'PM';
|
|
755
|
+
if (isPM && hours < 12)
|
|
756
|
+
hours += 12;
|
|
757
|
+
if (!isPM && hours === 12)
|
|
758
|
+
hours = 0;
|
|
759
|
+
return new Date(fullYear, month - 1, day, hours, minutes);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
}
|
|
764
|
+
return undefined;
|
|
352
765
|
}
|
|
353
766
|
ensureConnected() {
|
|
354
|
-
if (!this.connected || this.
|
|
767
|
+
if (!this.connected || !this.controlSocket || this.controlSocket.destroyed) {
|
|
355
768
|
throw new Error('Not connected to FTP server. Call connect() first.');
|
|
356
769
|
}
|
|
357
770
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
771
|
+
cleanup() {
|
|
772
|
+
if (this.controlSocket) {
|
|
773
|
+
this.controlSocket.destroy();
|
|
774
|
+
this.controlSocket = null;
|
|
775
|
+
}
|
|
776
|
+
this.connected = false;
|
|
777
|
+
this.secureConnection = false;
|
|
778
|
+
this.responseBuffer = '';
|
|
779
|
+
}
|
|
780
|
+
debug(format, ...args) {
|
|
781
|
+
if (this.config.verbose) {
|
|
782
|
+
console.log(`[FTP] ${format}`, ...args);
|
|
370
783
|
}
|
|
371
784
|
}
|
|
372
785
|
}
|