@zero-transfer/core 0.1.4 → 0.1.6
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/dist/index.cjs +4792 -35
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +3155 -0
- package/dist/index.d.ts +3155 -3
- package/dist/index.mjs +4688 -2
- package/dist/index.mjs.map +1 -0
- package/package.json +2 -5
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,4688 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// src/client/ZeroTransfer.ts
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
// src/errors/ZeroTransferError.ts
|
|
5
|
+
var ZeroTransferError = class extends Error {
|
|
6
|
+
/** Stable machine-readable error code. */
|
|
7
|
+
code;
|
|
8
|
+
/** Protocol active when the error occurred. */
|
|
9
|
+
protocol;
|
|
10
|
+
/** Remote host associated with the failing operation. */
|
|
11
|
+
host;
|
|
12
|
+
/** Protocol command associated with the failure, if any. */
|
|
13
|
+
command;
|
|
14
|
+
/** FTP response code associated with the failure. */
|
|
15
|
+
ftpCode;
|
|
16
|
+
/** SFTP status code associated with the failure. */
|
|
17
|
+
sftpCode;
|
|
18
|
+
/** Remote path associated with the failure. */
|
|
19
|
+
path;
|
|
20
|
+
/** Whether retry policy may safely retry this failure. */
|
|
21
|
+
retryable;
|
|
22
|
+
/** Additional structured details for diagnostics. */
|
|
23
|
+
details;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a structured SDK error.
|
|
26
|
+
*
|
|
27
|
+
* @param details - Code, message, retryability, and optional protocol context.
|
|
28
|
+
*/
|
|
29
|
+
constructor(details) {
|
|
30
|
+
super(details.message, details.cause === void 0 ? void 0 : { cause: details.cause });
|
|
31
|
+
this.name = new.target.name;
|
|
32
|
+
this.code = details.code;
|
|
33
|
+
this.retryable = details.retryable;
|
|
34
|
+
if (details.protocol !== void 0) this.protocol = details.protocol;
|
|
35
|
+
if (details.host !== void 0) this.host = details.host;
|
|
36
|
+
if (details.command !== void 0) this.command = details.command;
|
|
37
|
+
if (details.ftpCode !== void 0) this.ftpCode = details.ftpCode;
|
|
38
|
+
if (details.sftpCode !== void 0) this.sftpCode = details.sftpCode;
|
|
39
|
+
if (details.path !== void 0) this.path = details.path;
|
|
40
|
+
if (details.details !== void 0) this.details = details.details;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serializes the error into a plain object suitable for logs or API responses.
|
|
44
|
+
*
|
|
45
|
+
* @returns A JSON-safe object containing public structured error fields.
|
|
46
|
+
*/
|
|
47
|
+
toJSON() {
|
|
48
|
+
return {
|
|
49
|
+
name: this.name,
|
|
50
|
+
code: this.code,
|
|
51
|
+
message: this.message,
|
|
52
|
+
protocol: this.protocol,
|
|
53
|
+
host: this.host,
|
|
54
|
+
command: this.command,
|
|
55
|
+
ftpCode: this.ftpCode,
|
|
56
|
+
sftpCode: this.sftpCode,
|
|
57
|
+
path: this.path,
|
|
58
|
+
retryable: this.retryable,
|
|
59
|
+
details: this.details
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function withDefaultCode(details, code) {
|
|
64
|
+
return {
|
|
65
|
+
...details,
|
|
66
|
+
code: details.code ?? code
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var ConnectionError = class extends ZeroTransferError {
|
|
70
|
+
/**
|
|
71
|
+
* Creates a connection failure.
|
|
72
|
+
*
|
|
73
|
+
* @param details - Error context with optional host, protocol, and retryability details.
|
|
74
|
+
*/
|
|
75
|
+
constructor(details) {
|
|
76
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_CONNECTION_ERROR"));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var AuthenticationError = class extends ZeroTransferError {
|
|
80
|
+
/**
|
|
81
|
+
* Creates an authentication failure.
|
|
82
|
+
*
|
|
83
|
+
* @param details - Error context with optional host, protocol, and command details.
|
|
84
|
+
*/
|
|
85
|
+
constructor(details) {
|
|
86
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_AUTHENTICATION_ERROR"));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var AuthorizationError = class extends ZeroTransferError {
|
|
90
|
+
/**
|
|
91
|
+
* Creates an authorization failure.
|
|
92
|
+
*
|
|
93
|
+
* @param details - Error context with optional path and protocol details.
|
|
94
|
+
*/
|
|
95
|
+
constructor(details) {
|
|
96
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_AUTHORIZATION_ERROR"));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var PathNotFoundError = class extends ZeroTransferError {
|
|
100
|
+
/**
|
|
101
|
+
* Creates a missing-path failure.
|
|
102
|
+
*
|
|
103
|
+
* @param details - Error context with optional path and protocol details.
|
|
104
|
+
*/
|
|
105
|
+
constructor(details) {
|
|
106
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_PATH_NOT_FOUND"));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var PathAlreadyExistsError = class extends ZeroTransferError {
|
|
110
|
+
/**
|
|
111
|
+
* Creates an already-exists failure.
|
|
112
|
+
*
|
|
113
|
+
* @param details - Error context with optional path and command details.
|
|
114
|
+
*/
|
|
115
|
+
constructor(details) {
|
|
116
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_PATH_ALREADY_EXISTS"));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var PermissionDeniedError = class extends ZeroTransferError {
|
|
120
|
+
/**
|
|
121
|
+
* Creates a permission failure.
|
|
122
|
+
*
|
|
123
|
+
* @param details - Error context with optional path, command, and protocol details.
|
|
124
|
+
*/
|
|
125
|
+
constructor(details) {
|
|
126
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_PERMISSION_DENIED"));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var TimeoutError = class extends ZeroTransferError {
|
|
130
|
+
/**
|
|
131
|
+
* Creates a timeout failure.
|
|
132
|
+
*
|
|
133
|
+
* @param details - Error context with optional duration and retryability details.
|
|
134
|
+
*/
|
|
135
|
+
constructor(details) {
|
|
136
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_TIMEOUT"));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var AbortError = class extends ZeroTransferError {
|
|
140
|
+
/**
|
|
141
|
+
* Creates an aborted-operation failure.
|
|
142
|
+
*
|
|
143
|
+
* @param details - Error context with optional operation and path details.
|
|
144
|
+
*/
|
|
145
|
+
constructor(details) {
|
|
146
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_ABORTED"));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var ProtocolError = class extends ZeroTransferError {
|
|
150
|
+
/**
|
|
151
|
+
* Creates a protocol failure.
|
|
152
|
+
*
|
|
153
|
+
* @param details - Error context with optional response code and command details.
|
|
154
|
+
*/
|
|
155
|
+
constructor(details) {
|
|
156
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_PROTOCOL_ERROR"));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var ParseError = class extends ZeroTransferError {
|
|
160
|
+
/**
|
|
161
|
+
* Creates a parser failure.
|
|
162
|
+
*
|
|
163
|
+
* @param details - Error context with malformed input details.
|
|
164
|
+
*/
|
|
165
|
+
constructor(details) {
|
|
166
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_PARSE_ERROR"));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var TransferError = class extends ZeroTransferError {
|
|
170
|
+
/**
|
|
171
|
+
* Creates a transfer failure.
|
|
172
|
+
*
|
|
173
|
+
* @param details - Error context with optional path, bytes, and retryability details.
|
|
174
|
+
*/
|
|
175
|
+
constructor(details) {
|
|
176
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_TRANSFER_ERROR"));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var VerificationError = class extends ZeroTransferError {
|
|
180
|
+
/**
|
|
181
|
+
* Creates a verification failure.
|
|
182
|
+
*
|
|
183
|
+
* @param details - Error context with checksum, size, or timestamp mismatch details.
|
|
184
|
+
*/
|
|
185
|
+
constructor(details) {
|
|
186
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_VERIFICATION_ERROR"));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var UnsupportedFeatureError = class extends ZeroTransferError {
|
|
190
|
+
/**
|
|
191
|
+
* Creates an unsupported-feature failure.
|
|
192
|
+
*
|
|
193
|
+
* @param details - Error context describing the missing feature or adapter.
|
|
194
|
+
*/
|
|
195
|
+
constructor(details) {
|
|
196
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_UNSUPPORTED_FEATURE"));
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var ConfigurationError = class extends ZeroTransferError {
|
|
200
|
+
/**
|
|
201
|
+
* Creates a configuration failure.
|
|
202
|
+
*
|
|
203
|
+
* @param details - Error context describing the invalid option or argument.
|
|
204
|
+
*/
|
|
205
|
+
constructor(details) {
|
|
206
|
+
super(withDefaultCode(details, "ZERO_TRANSFER_CONFIGURATION_ERROR"));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/logging/Logger.ts
|
|
211
|
+
var noopLogger = {
|
|
212
|
+
trace() {
|
|
213
|
+
},
|
|
214
|
+
debug() {
|
|
215
|
+
},
|
|
216
|
+
info() {
|
|
217
|
+
},
|
|
218
|
+
warn() {
|
|
219
|
+
},
|
|
220
|
+
error() {
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
function emitLog(logger, level, record) {
|
|
224
|
+
const method = logger[level];
|
|
225
|
+
if (method === void 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const logRecord = {
|
|
229
|
+
...record,
|
|
230
|
+
level
|
|
231
|
+
};
|
|
232
|
+
method(logRecord, logRecord.message);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/profiles/ProfileValidator.ts
|
|
236
|
+
import { Buffer } from "buffer";
|
|
237
|
+
|
|
238
|
+
// src/core/ProviderId.ts
|
|
239
|
+
var CLASSIC_PROVIDER_IDS = ["ftp", "ftps", "sftp"];
|
|
240
|
+
function isClassicProviderId(providerId) {
|
|
241
|
+
return typeof providerId === "string" && CLASSIC_PROVIDER_IDS.includes(providerId);
|
|
242
|
+
}
|
|
243
|
+
function resolveProviderId(selection) {
|
|
244
|
+
return selection.provider ?? selection.protocol;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/profiles/ProfileValidator.ts
|
|
248
|
+
var TLS_VERSIONS = /* @__PURE__ */ new Set(["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"]);
|
|
249
|
+
var SHA256_FINGERPRINT_HEX_LENGTH = 64;
|
|
250
|
+
var SHA256_DIGEST_BYTE_LENGTH = 32;
|
|
251
|
+
function validateConnectionProfile(profile) {
|
|
252
|
+
if (resolveProviderId(profile) === void 0) {
|
|
253
|
+
throw new ConfigurationError({
|
|
254
|
+
message: "Connection profiles must include a provider or protocol",
|
|
255
|
+
retryable: false
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (profile.host.trim().length === 0) {
|
|
259
|
+
throw new ConfigurationError({
|
|
260
|
+
message: "Connection profiles must include a non-empty host",
|
|
261
|
+
retryable: false
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (profile.port !== void 0 && !isValidPort(profile.port)) {
|
|
265
|
+
throw new ConfigurationError({
|
|
266
|
+
details: { port: profile.port },
|
|
267
|
+
message: "Connection profile port must be an integer between 1 and 65535",
|
|
268
|
+
retryable: false
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (profile.timeoutMs !== void 0 && !isPositiveFiniteNumber(profile.timeoutMs)) {
|
|
272
|
+
throw new ConfigurationError({
|
|
273
|
+
details: { timeoutMs: profile.timeoutMs },
|
|
274
|
+
message: "Connection profile timeoutMs must be a positive finite number",
|
|
275
|
+
retryable: false
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (profile.tls !== void 0) {
|
|
279
|
+
validateTlsProfile(profile.tls);
|
|
280
|
+
}
|
|
281
|
+
if (profile.ssh !== void 0) {
|
|
282
|
+
validateSshProfile(profile.ssh);
|
|
283
|
+
}
|
|
284
|
+
return profile;
|
|
285
|
+
}
|
|
286
|
+
function validateSshProfile(profile) {
|
|
287
|
+
validatePinnedHostKeySha256(profile.pinnedHostKeySha256);
|
|
288
|
+
if (profile.algorithms !== void 0) {
|
|
289
|
+
validateSshAlgorithms(profile.algorithms);
|
|
290
|
+
}
|
|
291
|
+
if (profile.agent !== void 0) {
|
|
292
|
+
validateSshAgentSource(profile.agent);
|
|
293
|
+
}
|
|
294
|
+
if (profile.keyboardInteractive !== void 0 && typeof profile.keyboardInteractive !== "function") {
|
|
295
|
+
throw new ConfigurationError({
|
|
296
|
+
details: { keyboardInteractive: typeof profile.keyboardInteractive },
|
|
297
|
+
message: "Connection profile ssh.keyboardInteractive must be a function when provided",
|
|
298
|
+
retryable: false
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (profile.socketFactory !== void 0 && typeof profile.socketFactory !== "function") {
|
|
302
|
+
throw new ConfigurationError({
|
|
303
|
+
details: { socketFactory: typeof profile.socketFactory },
|
|
304
|
+
message: "Connection profile ssh.socketFactory must be a function when provided",
|
|
305
|
+
retryable: false
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function validateSshAlgorithms(value) {
|
|
310
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
311
|
+
throw createSshAlgorithmsError(value);
|
|
312
|
+
}
|
|
313
|
+
const algorithms = value;
|
|
314
|
+
for (const [name, list] of Object.entries(algorithms)) {
|
|
315
|
+
if (list === void 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(list)) {
|
|
319
|
+
if (!isNonEmptyStringArray(list)) {
|
|
320
|
+
throw createSshAlgorithmsError({ [name]: list });
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (typeof list !== "object" || list === null || Array.isArray(list)) {
|
|
325
|
+
throw createSshAlgorithmsError({ [name]: list });
|
|
326
|
+
}
|
|
327
|
+
const operationLists = list;
|
|
328
|
+
for (const [operation, operationList] of Object.entries(operationLists)) {
|
|
329
|
+
if (!["append", "prepend", "remove"].includes(operation)) {
|
|
330
|
+
throw createSshAlgorithmsError({ [name]: list });
|
|
331
|
+
}
|
|
332
|
+
if (typeof operationList !== "string" && (!Array.isArray(operationList) || !isNonEmptyStringArray(operationList))) {
|
|
333
|
+
throw createSshAlgorithmsError({ [name]: list });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isNonEmptyStringArray(value) {
|
|
339
|
+
return value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
|
|
340
|
+
}
|
|
341
|
+
function validateSshAgentSource(value) {
|
|
342
|
+
if (typeof value === "string") {
|
|
343
|
+
if (value.trim().length > 0) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
} else if (typeof value === "object" && value !== null && typeof value.getIdentities === "function" && typeof value.sign === "function") {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
throw new ConfigurationError({
|
|
350
|
+
details: { agent: typeof value },
|
|
351
|
+
message: "Connection profile ssh.agent must be a non-empty socket path or agent object",
|
|
352
|
+
retryable: false
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function validateTlsProfile(profile) {
|
|
356
|
+
if (profile.servername !== void 0 && profile.servername.trim().length === 0) {
|
|
357
|
+
throw new ConfigurationError({
|
|
358
|
+
message: "Connection profile tls.servername must be non-empty when provided",
|
|
359
|
+
retryable: false
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (profile.rejectUnauthorized !== void 0 && typeof profile.rejectUnauthorized !== "boolean") {
|
|
363
|
+
throw new ConfigurationError({
|
|
364
|
+
details: { rejectUnauthorized: profile.rejectUnauthorized },
|
|
365
|
+
message: "Connection profile tls.rejectUnauthorized must be a boolean",
|
|
366
|
+
retryable: false
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
validateTlsVersion(profile.minVersion, "minVersion");
|
|
370
|
+
validateTlsVersion(profile.maxVersion, "maxVersion");
|
|
371
|
+
validatePinnedFingerprint256(profile.pinnedFingerprint256);
|
|
372
|
+
}
|
|
373
|
+
function validatePinnedFingerprint256(value) {
|
|
374
|
+
if (value === void 0) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const fingerprints = Array.isArray(value) ? value : [value];
|
|
378
|
+
if (fingerprints.length === 0) {
|
|
379
|
+
throw createPinnedFingerprintError(value);
|
|
380
|
+
}
|
|
381
|
+
for (const fingerprint of fingerprints) {
|
|
382
|
+
if (typeof fingerprint !== "string" || !isSha256Fingerprint(fingerprint)) {
|
|
383
|
+
throw createPinnedFingerprintError(value);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function validatePinnedHostKeySha256(value) {
|
|
388
|
+
if (value === void 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const fingerprints = Array.isArray(value) ? value : [value];
|
|
392
|
+
if (fingerprints.length === 0) {
|
|
393
|
+
throw createPinnedHostKeyError(value);
|
|
394
|
+
}
|
|
395
|
+
for (const fingerprint of fingerprints) {
|
|
396
|
+
if (typeof fingerprint !== "string" || !isSshHostKeySha256Fingerprint(fingerprint)) {
|
|
397
|
+
throw createPinnedHostKeyError(value);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function isSha256Fingerprint(value) {
|
|
402
|
+
const normalized = value.trim().replace(/:/g, "");
|
|
403
|
+
return normalized.length === SHA256_FINGERPRINT_HEX_LENGTH && /^[a-f0-9]+$/i.test(normalized);
|
|
404
|
+
}
|
|
405
|
+
function isSshHostKeySha256Fingerprint(value) {
|
|
406
|
+
const trimmed = value.trim();
|
|
407
|
+
if (isSha256Fingerprint(trimmed)) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
const bare = trimmed.startsWith("SHA256:") ? trimmed.slice("SHA256:".length) : trimmed;
|
|
411
|
+
const padded = padBase64(bare);
|
|
412
|
+
if (!/^[a-z0-9+/]+={0,2}$/i.test(padded)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
return Buffer.from(padded, "base64").byteLength === SHA256_DIGEST_BYTE_LENGTH;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function padBase64(value) {
|
|
422
|
+
const remainder = value.length % 4;
|
|
423
|
+
return remainder === 0 ? value : `${value}${"=".repeat(4 - remainder)}`;
|
|
424
|
+
}
|
|
425
|
+
function createPinnedFingerprintError(value) {
|
|
426
|
+
return new ConfigurationError({
|
|
427
|
+
details: { pinnedFingerprint256: value },
|
|
428
|
+
message: "Connection profile tls.pinnedFingerprint256 must be a SHA-256 hex fingerprint or non-empty array of fingerprints",
|
|
429
|
+
retryable: false
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function createPinnedHostKeyError(value) {
|
|
433
|
+
return new ConfigurationError({
|
|
434
|
+
details: { pinnedHostKeySha256: value },
|
|
435
|
+
message: "Connection profile ssh.pinnedHostKeySha256 must be an OpenSSH SHA256, base64, or hex fingerprint or non-empty array of fingerprints",
|
|
436
|
+
retryable: false
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function createSshAlgorithmsError(value) {
|
|
440
|
+
return new ConfigurationError({
|
|
441
|
+
details: { algorithms: value },
|
|
442
|
+
message: "Connection profile ssh.algorithms must use ssh2-compatible non-empty algorithm lists",
|
|
443
|
+
retryable: false
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function validateTlsVersion(value, field) {
|
|
447
|
+
if (value === void 0) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!TLS_VERSIONS.has(value)) {
|
|
451
|
+
throw new ConfigurationError({
|
|
452
|
+
details: { [field]: value },
|
|
453
|
+
message: `Connection profile tls.${field} must be a supported TLS version`,
|
|
454
|
+
retryable: false
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function isValidPort(value) {
|
|
459
|
+
return Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
460
|
+
}
|
|
461
|
+
function isPositiveFiniteNumber(value) {
|
|
462
|
+
return Number.isFinite(value) && value > 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/core/ProviderRegistry.ts
|
|
466
|
+
var ProviderRegistry = class {
|
|
467
|
+
factories = /* @__PURE__ */ new Map();
|
|
468
|
+
/**
|
|
469
|
+
* Creates a registry and optionally seeds it with provider factories.
|
|
470
|
+
*
|
|
471
|
+
* @param providers - Provider factories to register immediately.
|
|
472
|
+
*/
|
|
473
|
+
constructor(providers = []) {
|
|
474
|
+
for (const provider of providers) {
|
|
475
|
+
this.register(provider);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Registers a provider factory.
|
|
480
|
+
*
|
|
481
|
+
* @param provider - Provider factory to add.
|
|
482
|
+
* @returns This registry for fluent setup.
|
|
483
|
+
* @throws {@link ConfigurationError} When a provider id is registered twice.
|
|
484
|
+
*/
|
|
485
|
+
register(provider) {
|
|
486
|
+
if (this.factories.has(provider.id)) {
|
|
487
|
+
throw new ConfigurationError({
|
|
488
|
+
details: { provider: provider.id },
|
|
489
|
+
message: `Provider "${provider.id}" is already registered`,
|
|
490
|
+
retryable: false
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
this.factories.set(provider.id, provider);
|
|
494
|
+
return this;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Removes a provider factory from the registry.
|
|
498
|
+
*
|
|
499
|
+
* @param providerId - Provider id to remove.
|
|
500
|
+
* @returns `true` when a provider was removed.
|
|
501
|
+
*/
|
|
502
|
+
unregister(providerId) {
|
|
503
|
+
return this.factories.delete(providerId);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Checks whether a provider id is registered.
|
|
507
|
+
*
|
|
508
|
+
* @param providerId - Provider id to inspect.
|
|
509
|
+
* @returns `true` when a provider factory exists.
|
|
510
|
+
*/
|
|
511
|
+
has(providerId) {
|
|
512
|
+
return this.factories.has(providerId);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Gets a provider factory when registered.
|
|
516
|
+
*
|
|
517
|
+
* @param providerId - Provider id to retrieve.
|
|
518
|
+
* @returns The provider factory, or `undefined` when missing.
|
|
519
|
+
*/
|
|
520
|
+
get(providerId) {
|
|
521
|
+
return this.factories.get(providerId);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Gets a registered provider factory or throws a typed SDK error.
|
|
525
|
+
*
|
|
526
|
+
* @param providerId - Provider id to retrieve.
|
|
527
|
+
* @returns The registered provider factory.
|
|
528
|
+
* @throws {@link UnsupportedFeatureError} When no provider has been registered.
|
|
529
|
+
*/
|
|
530
|
+
require(providerId) {
|
|
531
|
+
const provider = this.get(providerId);
|
|
532
|
+
if (provider === void 0) {
|
|
533
|
+
throw new UnsupportedFeatureError({
|
|
534
|
+
details: { provider: providerId },
|
|
535
|
+
message: `Provider "${providerId}" is not registered`,
|
|
536
|
+
retryable: false
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return provider;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Gets a provider capability snapshot when registered.
|
|
543
|
+
*
|
|
544
|
+
* @param providerId - Provider id to inspect.
|
|
545
|
+
* @returns Capability snapshot, or `undefined` when missing.
|
|
546
|
+
*/
|
|
547
|
+
getCapabilities(providerId) {
|
|
548
|
+
return this.get(providerId)?.capabilities;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Gets a provider capability snapshot or throws a typed SDK error.
|
|
552
|
+
*
|
|
553
|
+
* @param providerId - Provider id to inspect.
|
|
554
|
+
* @returns Capability snapshot for the registered provider.
|
|
555
|
+
* @throws {@link UnsupportedFeatureError} When no provider has been registered.
|
|
556
|
+
*/
|
|
557
|
+
requireCapabilities(providerId) {
|
|
558
|
+
return this.require(providerId).capabilities;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Lists registered provider factories in insertion order.
|
|
562
|
+
*
|
|
563
|
+
* @returns Registered provider factories.
|
|
564
|
+
*/
|
|
565
|
+
list() {
|
|
566
|
+
return [...this.factories.values()];
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Lists registered provider capabilities in insertion order.
|
|
570
|
+
*
|
|
571
|
+
* @returns Capability snapshots for every registered provider.
|
|
572
|
+
*/
|
|
573
|
+
listCapabilities() {
|
|
574
|
+
return this.list().map((provider) => provider.capabilities);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/core/TransferClient.ts
|
|
579
|
+
var TransferClient = class {
|
|
580
|
+
/** Provider registry used by this client. */
|
|
581
|
+
registry;
|
|
582
|
+
logger;
|
|
583
|
+
/**
|
|
584
|
+
* Creates a transfer client without opening any provider connections.
|
|
585
|
+
*
|
|
586
|
+
* @param options - Optional registry, provider factories, and logger.
|
|
587
|
+
*/
|
|
588
|
+
constructor(options = {}) {
|
|
589
|
+
this.registry = options.registry ?? new ProviderRegistry();
|
|
590
|
+
this.logger = options.logger ?? noopLogger;
|
|
591
|
+
for (const provider of options.providers ?? []) {
|
|
592
|
+
this.registry.register(provider);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Registers a provider factory with this client's registry.
|
|
597
|
+
*
|
|
598
|
+
* @param provider - Provider factory to register.
|
|
599
|
+
* @returns This client for fluent setup.
|
|
600
|
+
*/
|
|
601
|
+
registerProvider(provider) {
|
|
602
|
+
this.registry.register(provider);
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Checks whether this client can create sessions for a provider id.
|
|
607
|
+
*
|
|
608
|
+
* @param providerId - Provider id to inspect.
|
|
609
|
+
* @returns `true` when a provider factory is registered.
|
|
610
|
+
*/
|
|
611
|
+
hasProvider(providerId) {
|
|
612
|
+
return this.registry.has(providerId);
|
|
613
|
+
}
|
|
614
|
+
getCapabilities(providerId) {
|
|
615
|
+
if (providerId === void 0) {
|
|
616
|
+
return this.registry.listCapabilities();
|
|
617
|
+
}
|
|
618
|
+
return this.registry.requireCapabilities(providerId);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Opens a provider session using `profile.provider`, with `profile.protocol` as compatibility fallback.
|
|
622
|
+
*
|
|
623
|
+
* @param profile - Connection profile containing a provider or legacy protocol field.
|
|
624
|
+
* @returns A connected provider session.
|
|
625
|
+
* @throws {@link ConfigurationError} When neither provider nor protocol is present.
|
|
626
|
+
*/
|
|
627
|
+
async connect(profile) {
|
|
628
|
+
const validProfile = validateConnectionProfile(profile);
|
|
629
|
+
const providerId = resolveProviderId(validProfile);
|
|
630
|
+
if (providerId === void 0) {
|
|
631
|
+
throw new ConfigurationError({
|
|
632
|
+
message: "Connection profiles must include a provider or protocol",
|
|
633
|
+
retryable: false
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const providerFactory = this.registry.require(providerId);
|
|
637
|
+
const provider = providerFactory.create();
|
|
638
|
+
const normalizedProfile = {
|
|
639
|
+
...validProfile,
|
|
640
|
+
provider: providerId
|
|
641
|
+
};
|
|
642
|
+
if (normalizedProfile.protocol === void 0 && isClassicProviderId(providerId)) {
|
|
643
|
+
normalizedProfile.protocol = providerId;
|
|
644
|
+
}
|
|
645
|
+
emitLog(this.logger, "info", createConnectLogRecord(normalizedProfile, providerId));
|
|
646
|
+
return provider.connect(normalizedProfile);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
function createConnectLogRecord(profile, providerId) {
|
|
650
|
+
const record = {
|
|
651
|
+
component: "core",
|
|
652
|
+
host: profile.host,
|
|
653
|
+
message: "Connecting through provider",
|
|
654
|
+
provider: providerId
|
|
655
|
+
};
|
|
656
|
+
if (isClassicProviderId(providerId)) {
|
|
657
|
+
record.protocol = providerId;
|
|
658
|
+
}
|
|
659
|
+
return record;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/core/createTransferClient.ts
|
|
663
|
+
function createTransferClient(options = {}) {
|
|
664
|
+
return new TransferClient(options);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/client/ZeroTransfer.ts
|
|
668
|
+
var ZeroTransfer = class _ZeroTransfer extends EventEmitter {
|
|
669
|
+
/** Creates a provider-neutral transfer client with the built-in provider registry. */
|
|
670
|
+
static createTransferClient = createTransferClient;
|
|
671
|
+
/** Protocol selected for this client instance. */
|
|
672
|
+
protocol;
|
|
673
|
+
logger;
|
|
674
|
+
adapter;
|
|
675
|
+
connected = false;
|
|
676
|
+
/**
|
|
677
|
+
* Creates a client facade without opening a network connection.
|
|
678
|
+
*
|
|
679
|
+
* @param options - Optional facade configuration, logger, and protocol adapter.
|
|
680
|
+
*/
|
|
681
|
+
constructor(options = {}) {
|
|
682
|
+
super();
|
|
683
|
+
this.protocol = options.protocol ?? "ftp";
|
|
684
|
+
this.logger = options.logger ?? noopLogger;
|
|
685
|
+
this.adapter = options.adapter;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Creates a new client facade using the provided options.
|
|
689
|
+
*
|
|
690
|
+
* @param options - Optional facade configuration, logger, and adapter.
|
|
691
|
+
* @returns A disconnected {@link ZeroTransfer} instance.
|
|
692
|
+
*/
|
|
693
|
+
static create(options = {}) {
|
|
694
|
+
return new _ZeroTransfer(options);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Creates a client and connects it in one step.
|
|
698
|
+
*
|
|
699
|
+
* @param profile - Remote host, authentication, and protocol connection settings.
|
|
700
|
+
* @param options - Optional facade settings that can be overridden by the profile.
|
|
701
|
+
* @returns A connected {@link ZeroTransfer} instance.
|
|
702
|
+
* @throws {@link UnsupportedFeatureError} When no adapter is available for the protocol.
|
|
703
|
+
*/
|
|
704
|
+
static async connect(profile, options = {}) {
|
|
705
|
+
const clientOptions = { ...options };
|
|
706
|
+
if (profile.logger !== void 0) {
|
|
707
|
+
clientOptions.logger = profile.logger;
|
|
708
|
+
}
|
|
709
|
+
if (profile.protocol !== void 0) {
|
|
710
|
+
clientOptions.protocol = profile.protocol;
|
|
711
|
+
} else if (isClassicProviderId(profile.provider)) {
|
|
712
|
+
clientOptions.protocol = profile.provider;
|
|
713
|
+
}
|
|
714
|
+
const client = new _ZeroTransfer(clientOptions);
|
|
715
|
+
await client.connect(profile);
|
|
716
|
+
return client;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Opens a remote connection through the configured protocol adapter.
|
|
720
|
+
*
|
|
721
|
+
* @param profile - Remote host, authentication, timeout, logger, and protocol settings.
|
|
722
|
+
* @returns A promise that resolves after the adapter reports a successful connection.
|
|
723
|
+
* @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
|
|
724
|
+
*/
|
|
725
|
+
async connect(profile) {
|
|
726
|
+
const adapter = this.requireAdapter();
|
|
727
|
+
const protocol = profile.protocol ?? (isClassicProviderId(profile.provider) ? profile.provider : this.protocol);
|
|
728
|
+
emitLog(this.logger, "info", {
|
|
729
|
+
component: "client",
|
|
730
|
+
host: profile.host,
|
|
731
|
+
message: "Connecting",
|
|
732
|
+
protocol
|
|
733
|
+
});
|
|
734
|
+
await adapter.connect({
|
|
735
|
+
...profile,
|
|
736
|
+
protocol
|
|
737
|
+
});
|
|
738
|
+
this.connected = true;
|
|
739
|
+
this.emit("connect", {
|
|
740
|
+
host: profile.host,
|
|
741
|
+
protocol
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Closes the active remote connection if one exists.
|
|
746
|
+
*
|
|
747
|
+
* @returns A promise that resolves after the adapter disconnects or immediately when idle.
|
|
748
|
+
*/
|
|
749
|
+
async disconnect() {
|
|
750
|
+
if (this.adapter !== void 0 && this.connected) {
|
|
751
|
+
await this.adapter.disconnect();
|
|
752
|
+
}
|
|
753
|
+
this.connected = false;
|
|
754
|
+
this.emit("disconnect");
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Checks whether the facade currently considers the adapter connected.
|
|
758
|
+
*
|
|
759
|
+
* @returns `true` after a successful connection and before disconnection.
|
|
760
|
+
*/
|
|
761
|
+
isConnected() {
|
|
762
|
+
return this.connected;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Describes protocol and adapter readiness for feature discovery.
|
|
766
|
+
*
|
|
767
|
+
* @returns A capability snapshot for diagnostics and UI state.
|
|
768
|
+
*/
|
|
769
|
+
getCapabilities() {
|
|
770
|
+
return {
|
|
771
|
+
adapterReady: this.adapter !== void 0,
|
|
772
|
+
protocol: this.protocol
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Lists remote entries for a path using the configured adapter.
|
|
777
|
+
*
|
|
778
|
+
* @param path - Remote directory path to inspect.
|
|
779
|
+
* @param options - Optional listing controls such as recursion and abort signal.
|
|
780
|
+
* @returns Normalized remote entries for the requested directory.
|
|
781
|
+
* @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
|
|
782
|
+
*/
|
|
783
|
+
async list(path2, options) {
|
|
784
|
+
return this.requireAdapter().list(path2, options);
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Reads metadata for a remote path using the configured adapter.
|
|
788
|
+
*
|
|
789
|
+
* @param path - Remote file, directory, or symbolic-link path to inspect.
|
|
790
|
+
* @param options - Optional stat controls such as abort signal.
|
|
791
|
+
* @returns Normalized metadata for an existing remote entry.
|
|
792
|
+
* @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
|
|
793
|
+
*/
|
|
794
|
+
async stat(path2, options) {
|
|
795
|
+
return this.requireAdapter().stat(path2, options);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Returns the configured adapter or raises the alpha unsupported-feature error.
|
|
799
|
+
*
|
|
800
|
+
* @returns A concrete remote file adapter ready to execute operations.
|
|
801
|
+
* @throws {@link UnsupportedFeatureError} When no adapter has been provided.
|
|
802
|
+
*/
|
|
803
|
+
requireAdapter() {
|
|
804
|
+
if (this.adapter === void 0) {
|
|
805
|
+
throw new UnsupportedFeatureError({
|
|
806
|
+
message: `The ${this.protocol.toUpperCase()} adapter is not implemented in this alpha foundation yet`,
|
|
807
|
+
protocol: this.protocol,
|
|
808
|
+
retryable: false
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return this.adapter;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/client/operations.ts
|
|
816
|
+
import { isAbsolute, resolve as resolvePath } from "path";
|
|
817
|
+
|
|
818
|
+
// src/transfers/BandwidthThrottle.ts
|
|
819
|
+
function createBandwidthThrottle(limit, options = {}) {
|
|
820
|
+
if (limit === void 0) return void 0;
|
|
821
|
+
const bytesPerSecond = normalizeRate(limit.bytesPerSecond);
|
|
822
|
+
const burstBytes = normalizeBurst(limit.burstBytes, bytesPerSecond);
|
|
823
|
+
const now = options.now ?? Date.now;
|
|
824
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
825
|
+
let tokens = burstBytes;
|
|
826
|
+
let lastRefillAt = now();
|
|
827
|
+
function refill() {
|
|
828
|
+
const current = now();
|
|
829
|
+
const elapsedMs = Math.max(0, current - lastRefillAt);
|
|
830
|
+
if (elapsedMs > 0) {
|
|
831
|
+
tokens = Math.min(burstBytes, tokens + elapsedMs / 1e3 * bytesPerSecond);
|
|
832
|
+
lastRefillAt = current;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function consume(bytes, signal) {
|
|
836
|
+
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
837
|
+
throw new ConfigurationError({
|
|
838
|
+
details: { bytes },
|
|
839
|
+
message: "Bandwidth throttle byte count must be a non-negative number",
|
|
840
|
+
retryable: false
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
if (bytes === 0) return;
|
|
844
|
+
let remaining = bytes;
|
|
845
|
+
while (remaining > 0) {
|
|
846
|
+
throwIfAborted(signal);
|
|
847
|
+
refill();
|
|
848
|
+
if (tokens >= remaining) {
|
|
849
|
+
tokens -= remaining;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (tokens >= burstBytes) {
|
|
853
|
+
const drained = tokens;
|
|
854
|
+
tokens = 0;
|
|
855
|
+
remaining -= drained;
|
|
856
|
+
const waitMs2 = Math.ceil(Math.min(remaining, burstBytes) / bytesPerSecond * 1e3);
|
|
857
|
+
await sleep(waitMs2, signal);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const deficit = Math.min(remaining, burstBytes) - tokens;
|
|
861
|
+
const waitMs = Math.max(1, Math.ceil(deficit / bytesPerSecond * 1e3));
|
|
862
|
+
await sleep(waitMs, signal);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return { burstBytes, bytesPerSecond, consume };
|
|
866
|
+
}
|
|
867
|
+
function throttleByteIterable(source, throttle, signal) {
|
|
868
|
+
if (throttle === void 0) return source;
|
|
869
|
+
return {
|
|
870
|
+
[Symbol.asyncIterator]: async function* () {
|
|
871
|
+
for await (const chunk of source) {
|
|
872
|
+
throwIfAborted(signal);
|
|
873
|
+
if (chunk.byteLength > 0) {
|
|
874
|
+
await throttle.consume(chunk.byteLength, signal);
|
|
875
|
+
}
|
|
876
|
+
yield chunk;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function normalizeRate(value) {
|
|
882
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
883
|
+
throw new ConfigurationError({
|
|
884
|
+
details: { bytesPerSecond: value },
|
|
885
|
+
message: "Bandwidth limit bytesPerSecond must be a positive number",
|
|
886
|
+
retryable: false
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return value;
|
|
890
|
+
}
|
|
891
|
+
function normalizeBurst(value, bytesPerSecond) {
|
|
892
|
+
if (value === void 0) return bytesPerSecond;
|
|
893
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
894
|
+
throw new ConfigurationError({
|
|
895
|
+
details: { burstBytes: value },
|
|
896
|
+
message: "Bandwidth limit burstBytes must be a positive number when provided",
|
|
897
|
+
retryable: false
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
function throwIfAborted(signal) {
|
|
903
|
+
if (signal?.aborted === true) {
|
|
904
|
+
throw new AbortError({
|
|
905
|
+
message: "Bandwidth throttle wait aborted",
|
|
906
|
+
retryable: false
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function defaultSleep(delayMs, signal) {
|
|
911
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
912
|
+
return new Promise((resolve, reject) => {
|
|
913
|
+
const timer = setTimeout(() => {
|
|
914
|
+
cleanup();
|
|
915
|
+
resolve();
|
|
916
|
+
}, delayMs);
|
|
917
|
+
const onAbort = () => {
|
|
918
|
+
cleanup();
|
|
919
|
+
reject(
|
|
920
|
+
new AbortError({
|
|
921
|
+
message: "Bandwidth throttle wait aborted",
|
|
922
|
+
retryable: false
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
};
|
|
926
|
+
function cleanup() {
|
|
927
|
+
clearTimeout(timer);
|
|
928
|
+
signal?.removeEventListener("abort", onAbort);
|
|
929
|
+
}
|
|
930
|
+
if (signal !== void 0) {
|
|
931
|
+
if (signal.aborted) {
|
|
932
|
+
onAbort();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/transfers/createProviderTransferExecutor.ts
|
|
941
|
+
function createProviderTransferExecutor(options) {
|
|
942
|
+
return async (context) => {
|
|
943
|
+
const { job } = context;
|
|
944
|
+
if (!isReadWriteOperation(job.operation)) {
|
|
945
|
+
throw new UnsupportedFeatureError({
|
|
946
|
+
details: { jobId: job.id, operation: job.operation },
|
|
947
|
+
message: `Provider read/write executor does not support transfer operation: ${job.operation}`,
|
|
948
|
+
retryable: false
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const source = requireEndpoint(job, "source");
|
|
952
|
+
const destination = requireEndpoint(job, "destination");
|
|
953
|
+
const sourceSession = options.resolveSession({ endpoint: source, job, role: "source" });
|
|
954
|
+
const destinationSession = options.resolveSession({
|
|
955
|
+
endpoint: destination,
|
|
956
|
+
job,
|
|
957
|
+
role: "destination"
|
|
958
|
+
});
|
|
959
|
+
const sourceTransfers = requireTransferOperations(sourceSession, source, "source", job);
|
|
960
|
+
const destinationTransfers = requireTransferOperations(
|
|
961
|
+
destinationSession,
|
|
962
|
+
destination,
|
|
963
|
+
"destination",
|
|
964
|
+
job
|
|
965
|
+
);
|
|
966
|
+
context.throwIfAborted();
|
|
967
|
+
const readResult = await sourceTransfers.read(createReadRequest(context, source));
|
|
968
|
+
context.throwIfAborted();
|
|
969
|
+
const throttledReadResult = applyBandwidthThrottle(readResult, context, options.throttle);
|
|
970
|
+
const writeResult = await destinationTransfers.write(
|
|
971
|
+
createWriteRequest(context, destination, throttledReadResult)
|
|
972
|
+
);
|
|
973
|
+
return mergeProviderTransferResult(readResult, writeResult, job);
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function applyBandwidthThrottle(readResult, context, options) {
|
|
977
|
+
const throttle = createBandwidthThrottle(context.bandwidthLimit, options);
|
|
978
|
+
if (throttle === void 0) return readResult;
|
|
979
|
+
return {
|
|
980
|
+
...readResult,
|
|
981
|
+
content: throttleByteIterable(readResult.content, throttle, context.signal)
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function isReadWriteOperation(operation) {
|
|
985
|
+
return operation === "copy" || operation === "download" || operation === "upload";
|
|
986
|
+
}
|
|
987
|
+
function requireEndpoint(job, role) {
|
|
988
|
+
const endpoint = role === "source" ? job.source : job.destination;
|
|
989
|
+
if (endpoint === void 0) {
|
|
990
|
+
throw new ConfigurationError({
|
|
991
|
+
details: { jobId: job.id, operation: job.operation, role },
|
|
992
|
+
message: `Transfer job requires a ${role} endpoint: ${job.id}`,
|
|
993
|
+
retryable: false
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
return endpoint;
|
|
997
|
+
}
|
|
998
|
+
function requireTransferOperations(session, endpoint, role, job) {
|
|
999
|
+
if (session === void 0) {
|
|
1000
|
+
throw new UnsupportedFeatureError({
|
|
1001
|
+
details: { endpoint: cloneEndpoint(endpoint), jobId: job.id, operation: job.operation, role },
|
|
1002
|
+
message: `No provider session resolved for ${role} endpoint: ${endpoint.path}`,
|
|
1003
|
+
retryable: false
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (session.transfers === void 0) {
|
|
1007
|
+
throw new UnsupportedFeatureError({
|
|
1008
|
+
details: {
|
|
1009
|
+
endpoint: cloneEndpoint(endpoint),
|
|
1010
|
+
jobId: job.id,
|
|
1011
|
+
operation: job.operation,
|
|
1012
|
+
provider: session.provider,
|
|
1013
|
+
role
|
|
1014
|
+
},
|
|
1015
|
+
message: `Provider session does not expose transfer operations: ${session.provider}`,
|
|
1016
|
+
retryable: false
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
return session.transfers;
|
|
1020
|
+
}
|
|
1021
|
+
function createReadRequest(context, endpoint) {
|
|
1022
|
+
const request = {
|
|
1023
|
+
attempt: context.attempt,
|
|
1024
|
+
endpoint: cloneEndpoint(endpoint),
|
|
1025
|
+
job: context.job,
|
|
1026
|
+
reportProgress: (bytesTransferred, totalBytes) => context.reportProgress(bytesTransferred, totalBytes),
|
|
1027
|
+
throwIfAborted: () => context.throwIfAborted()
|
|
1028
|
+
};
|
|
1029
|
+
if (context.signal !== void 0) request.signal = context.signal;
|
|
1030
|
+
if (context.bandwidthLimit !== void 0) {
|
|
1031
|
+
request.bandwidthLimit = { ...context.bandwidthLimit };
|
|
1032
|
+
}
|
|
1033
|
+
return request;
|
|
1034
|
+
}
|
|
1035
|
+
function createWriteRequest(context, endpoint, readResult) {
|
|
1036
|
+
const request = {
|
|
1037
|
+
attempt: context.attempt,
|
|
1038
|
+
content: readResult.content,
|
|
1039
|
+
endpoint: cloneEndpoint(endpoint),
|
|
1040
|
+
job: context.job,
|
|
1041
|
+
reportProgress: (bytesTransferred, totalBytes2) => context.reportProgress(bytesTransferred, totalBytes2),
|
|
1042
|
+
throwIfAborted: () => context.throwIfAborted()
|
|
1043
|
+
};
|
|
1044
|
+
const totalBytes = readResult.totalBytes ?? context.job.totalBytes;
|
|
1045
|
+
if (context.signal !== void 0) request.signal = context.signal;
|
|
1046
|
+
if (context.bandwidthLimit !== void 0) {
|
|
1047
|
+
request.bandwidthLimit = { ...context.bandwidthLimit };
|
|
1048
|
+
}
|
|
1049
|
+
if (totalBytes !== void 0) request.totalBytes = totalBytes;
|
|
1050
|
+
if (context.job.resumed === true) request.offset = readResult.bytesRead ?? 0;
|
|
1051
|
+
if (readResult.verification !== void 0) {
|
|
1052
|
+
request.verification = cloneVerification(readResult.verification);
|
|
1053
|
+
}
|
|
1054
|
+
return request;
|
|
1055
|
+
}
|
|
1056
|
+
function mergeProviderTransferResult(readResult, writeResult, job) {
|
|
1057
|
+
const result = {
|
|
1058
|
+
bytesTransferred: writeResult.bytesTransferred
|
|
1059
|
+
};
|
|
1060
|
+
const totalBytes = writeResult.totalBytes ?? readResult.totalBytes ?? job.totalBytes;
|
|
1061
|
+
const warnings = [...readResult.warnings ?? [], ...writeResult.warnings ?? []];
|
|
1062
|
+
if (totalBytes !== void 0) result.totalBytes = totalBytes;
|
|
1063
|
+
if (writeResult.resumed !== void 0) result.resumed = writeResult.resumed;
|
|
1064
|
+
if (writeResult.verified !== void 0) result.verified = writeResult.verified;
|
|
1065
|
+
if (writeResult.checksum !== void 0) result.checksum = writeResult.checksum;
|
|
1066
|
+
else if (readResult.checksum !== void 0) result.checksum = readResult.checksum;
|
|
1067
|
+
if (writeResult.verification !== void 0) {
|
|
1068
|
+
result.verification = cloneVerification(writeResult.verification);
|
|
1069
|
+
} else if (readResult.verification !== void 0) {
|
|
1070
|
+
result.verification = cloneVerification(readResult.verification);
|
|
1071
|
+
}
|
|
1072
|
+
if (warnings.length > 0) result.warnings = warnings;
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
function cloneEndpoint(endpoint) {
|
|
1076
|
+
const clone = { path: endpoint.path };
|
|
1077
|
+
if (endpoint.provider !== void 0) clone.provider = endpoint.provider;
|
|
1078
|
+
return clone;
|
|
1079
|
+
}
|
|
1080
|
+
function cloneVerification(verification) {
|
|
1081
|
+
const clone = { verified: verification.verified };
|
|
1082
|
+
if (verification.method !== void 0) clone.method = verification.method;
|
|
1083
|
+
if (verification.checksum !== void 0) clone.checksum = verification.checksum;
|
|
1084
|
+
if (verification.expectedChecksum !== void 0) {
|
|
1085
|
+
clone.expectedChecksum = verification.expectedChecksum;
|
|
1086
|
+
}
|
|
1087
|
+
if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
|
|
1088
|
+
if (verification.details !== void 0) clone.details = { ...verification.details };
|
|
1089
|
+
return clone;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/services/TransferService.ts
|
|
1093
|
+
function createTransferResult(input) {
|
|
1094
|
+
const durationMs = Math.max(0, input.completedAt.getTime() - input.startedAt.getTime());
|
|
1095
|
+
const result = {
|
|
1096
|
+
destinationPath: input.destinationPath,
|
|
1097
|
+
bytesTransferred: input.bytesTransferred,
|
|
1098
|
+
startedAt: input.startedAt,
|
|
1099
|
+
completedAt: input.completedAt,
|
|
1100
|
+
durationMs,
|
|
1101
|
+
averageBytesPerSecond: calculateBytesPerSecond(input.bytesTransferred, durationMs),
|
|
1102
|
+
resumed: input.resumed ?? false,
|
|
1103
|
+
verified: input.verified ?? false
|
|
1104
|
+
};
|
|
1105
|
+
if (input.sourcePath !== void 0) result.sourcePath = input.sourcePath;
|
|
1106
|
+
if (input.checksum !== void 0) result.checksum = input.checksum;
|
|
1107
|
+
return result;
|
|
1108
|
+
}
|
|
1109
|
+
function createProgressEvent(input) {
|
|
1110
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1111
|
+
const elapsedMs = Math.max(0, now.getTime() - input.startedAt.getTime());
|
|
1112
|
+
const event = {
|
|
1113
|
+
transferId: input.transferId,
|
|
1114
|
+
bytesTransferred: input.bytesTransferred,
|
|
1115
|
+
startedAt: input.startedAt,
|
|
1116
|
+
elapsedMs,
|
|
1117
|
+
bytesPerSecond: calculateBytesPerSecond(input.bytesTransferred, elapsedMs)
|
|
1118
|
+
};
|
|
1119
|
+
if (input.totalBytes !== void 0) {
|
|
1120
|
+
event.totalBytes = input.totalBytes;
|
|
1121
|
+
event.percent = input.totalBytes > 0 ? input.bytesTransferred / input.totalBytes * 100 : 0;
|
|
1122
|
+
}
|
|
1123
|
+
return event;
|
|
1124
|
+
}
|
|
1125
|
+
function calculateBytesPerSecond(bytes, durationMs) {
|
|
1126
|
+
if (durationMs <= 0) {
|
|
1127
|
+
return bytes;
|
|
1128
|
+
}
|
|
1129
|
+
return bytes / (durationMs / 1e3);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/transfers/TransferEngine.ts
|
|
1133
|
+
var TransferEngine = class {
|
|
1134
|
+
now;
|
|
1135
|
+
/**
|
|
1136
|
+
* Creates a transfer engine.
|
|
1137
|
+
*
|
|
1138
|
+
* @param options - Optional clock override for deterministic tests.
|
|
1139
|
+
*/
|
|
1140
|
+
constructor(options = {}) {
|
|
1141
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Executes a transfer job through a caller-supplied operation.
|
|
1145
|
+
*
|
|
1146
|
+
* @param job - Job metadata used for correlation and receipts.
|
|
1147
|
+
* @param executor - Concrete transfer operation implementation.
|
|
1148
|
+
* @param options - Optional abort, retry, and progress hooks.
|
|
1149
|
+
* @returns Receipt for the completed transfer.
|
|
1150
|
+
* @throws {@link AbortError} When execution is cancelled.
|
|
1151
|
+
* @throws {@link TransferError} When all attempts fail.
|
|
1152
|
+
*/
|
|
1153
|
+
async execute(job, executor, options = {}) {
|
|
1154
|
+
const maxAttempts = normalizeMaxAttempts(options.retry?.maxAttempts);
|
|
1155
|
+
const attempts = [];
|
|
1156
|
+
const startedAt = this.now();
|
|
1157
|
+
const abortScope = createAbortScope(options.signal, options.timeout, job);
|
|
1158
|
+
let latestBytesTransferred = 0;
|
|
1159
|
+
try {
|
|
1160
|
+
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
1161
|
+
this.throwIfAborted(abortScope.signal, job);
|
|
1162
|
+
const attemptStartedAt = this.now();
|
|
1163
|
+
const context = this.createExecutionContext(
|
|
1164
|
+
job,
|
|
1165
|
+
attemptNumber,
|
|
1166
|
+
attemptStartedAt,
|
|
1167
|
+
options,
|
|
1168
|
+
abortScope.signal,
|
|
1169
|
+
(bytesTransferred) => {
|
|
1170
|
+
latestBytesTransferred = bytesTransferred;
|
|
1171
|
+
}
|
|
1172
|
+
);
|
|
1173
|
+
try {
|
|
1174
|
+
const result = await runExecutor(executor, context, abortScope.signal, job);
|
|
1175
|
+
context.throwIfAborted();
|
|
1176
|
+
latestBytesTransferred = result.bytesTransferred;
|
|
1177
|
+
const completedAt = this.now();
|
|
1178
|
+
attempts.push(
|
|
1179
|
+
createAttempt(attemptNumber, attemptStartedAt, completedAt, result.bytesTransferred)
|
|
1180
|
+
);
|
|
1181
|
+
return createReceipt(job, result, attempts, startedAt, completedAt);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
const completedAt = this.now();
|
|
1184
|
+
const attempt = createAttempt(
|
|
1185
|
+
attemptNumber,
|
|
1186
|
+
attemptStartedAt,
|
|
1187
|
+
completedAt,
|
|
1188
|
+
latestBytesTransferred,
|
|
1189
|
+
summarizeError(error)
|
|
1190
|
+
);
|
|
1191
|
+
attempts.push(attempt);
|
|
1192
|
+
if (error instanceof AbortError || error instanceof TimeoutError) {
|
|
1193
|
+
throw error;
|
|
1194
|
+
}
|
|
1195
|
+
const retryInput = { attempt: attemptNumber, error, job };
|
|
1196
|
+
const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
|
|
1197
|
+
if (shouldRetry) {
|
|
1198
|
+
options.retry?.onRetry?.(retryInput);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
throw createTransferFailure(job, error, attempts);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
throw createTransferFailure(job, void 0, attempts);
|
|
1205
|
+
} finally {
|
|
1206
|
+
abortScope.dispose();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
|
|
1210
|
+
const context = {
|
|
1211
|
+
attempt,
|
|
1212
|
+
job,
|
|
1213
|
+
reportProgress: (bytesTransferred, totalBytes) => {
|
|
1214
|
+
this.throwIfAborted(signal, job);
|
|
1215
|
+
updateBytesTransferred(bytesTransferred);
|
|
1216
|
+
const progressInput = {
|
|
1217
|
+
bytesTransferred,
|
|
1218
|
+
now: this.now(),
|
|
1219
|
+
startedAt,
|
|
1220
|
+
transferId: job.id
|
|
1221
|
+
};
|
|
1222
|
+
const resolvedTotalBytes = totalBytes ?? job.totalBytes;
|
|
1223
|
+
const event = createProgressEvent(
|
|
1224
|
+
resolvedTotalBytes === void 0 ? progressInput : { ...progressInput, totalBytes: resolvedTotalBytes }
|
|
1225
|
+
);
|
|
1226
|
+
options.onProgress?.(event);
|
|
1227
|
+
return event;
|
|
1228
|
+
},
|
|
1229
|
+
throwIfAborted: () => this.throwIfAborted(signal, job)
|
|
1230
|
+
};
|
|
1231
|
+
if (signal !== void 0) {
|
|
1232
|
+
context.signal = signal;
|
|
1233
|
+
}
|
|
1234
|
+
if (options.bandwidthLimit !== void 0) {
|
|
1235
|
+
context.bandwidthLimit = { ...options.bandwidthLimit };
|
|
1236
|
+
}
|
|
1237
|
+
return context;
|
|
1238
|
+
}
|
|
1239
|
+
throwIfAborted(signal, job) {
|
|
1240
|
+
if (signal?.aborted === true) {
|
|
1241
|
+
if (signal.reason instanceof ZeroTransferError) {
|
|
1242
|
+
throw signal.reason;
|
|
1243
|
+
}
|
|
1244
|
+
throw new AbortError({
|
|
1245
|
+
details: { jobId: job.id, operation: job.operation },
|
|
1246
|
+
message: `Transfer job aborted: ${job.id}`,
|
|
1247
|
+
retryable: false
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
function createAbortScope(parentSignal, timeout, job) {
|
|
1253
|
+
const timeoutMs = normalizeTimeoutMs(timeout?.timeoutMs);
|
|
1254
|
+
if (parentSignal === void 0 && timeoutMs === void 0) {
|
|
1255
|
+
return { dispose: () => void 0 };
|
|
1256
|
+
}
|
|
1257
|
+
const controller = new AbortController();
|
|
1258
|
+
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
|
1259
|
+
const timeoutHandle = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
1260
|
+
controller.abort(
|
|
1261
|
+
new TimeoutError({
|
|
1262
|
+
details: { jobId: job.id, operation: job.operation, timeoutMs },
|
|
1263
|
+
message: `Transfer job timed out after ${timeoutMs}ms: ${job.id}`,
|
|
1264
|
+
retryable: timeout?.retryable ?? true
|
|
1265
|
+
})
|
|
1266
|
+
);
|
|
1267
|
+
}, timeoutMs);
|
|
1268
|
+
if (parentSignal?.aborted === true) {
|
|
1269
|
+
abortFromParent();
|
|
1270
|
+
} else {
|
|
1271
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
dispose: () => {
|
|
1275
|
+
if (timeoutHandle !== void 0) {
|
|
1276
|
+
clearTimeout(timeoutHandle);
|
|
1277
|
+
}
|
|
1278
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
1279
|
+
},
|
|
1280
|
+
signal: controller.signal
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function normalizeTimeoutMs(value) {
|
|
1284
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) {
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
return Math.floor(value);
|
|
1288
|
+
}
|
|
1289
|
+
async function runExecutor(executor, context, signal, job) {
|
|
1290
|
+
if (signal === void 0) {
|
|
1291
|
+
return executor(context);
|
|
1292
|
+
}
|
|
1293
|
+
return Promise.race([executor(context), rejectWhenAborted(signal, job)]);
|
|
1294
|
+
}
|
|
1295
|
+
function rejectWhenAborted(signal, job) {
|
|
1296
|
+
return new Promise((_, reject) => {
|
|
1297
|
+
const rejectAbort = () => {
|
|
1298
|
+
if (signal.reason instanceof ZeroTransferError) {
|
|
1299
|
+
reject(signal.reason);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
reject(
|
|
1303
|
+
new AbortError({
|
|
1304
|
+
details: { jobId: job.id, operation: job.operation },
|
|
1305
|
+
message: `Transfer job aborted: ${job.id}`,
|
|
1306
|
+
retryable: false
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1309
|
+
};
|
|
1310
|
+
if (signal.aborted) {
|
|
1311
|
+
rejectAbort();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
function normalizeMaxAttempts(value) {
|
|
1318
|
+
if (value === void 0) {
|
|
1319
|
+
return 1;
|
|
1320
|
+
}
|
|
1321
|
+
return Math.max(1, Math.floor(value));
|
|
1322
|
+
}
|
|
1323
|
+
function createAttempt(attempt, startedAt, completedAt, bytesTransferred, error) {
|
|
1324
|
+
const result = {
|
|
1325
|
+
attempt,
|
|
1326
|
+
bytesTransferred,
|
|
1327
|
+
completedAt,
|
|
1328
|
+
durationMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
|
|
1329
|
+
startedAt
|
|
1330
|
+
};
|
|
1331
|
+
if (error !== void 0) {
|
|
1332
|
+
result.error = error;
|
|
1333
|
+
}
|
|
1334
|
+
return result;
|
|
1335
|
+
}
|
|
1336
|
+
function createReceipt(job, result, attempts, startedAt, completedAt) {
|
|
1337
|
+
const durationMs = Math.max(0, completedAt.getTime() - startedAt.getTime());
|
|
1338
|
+
const verification = normalizeVerificationResult(result);
|
|
1339
|
+
const receipt = {
|
|
1340
|
+
attempts,
|
|
1341
|
+
averageBytesPerSecond: calculateBytesPerSecond2(result.bytesTransferred, durationMs),
|
|
1342
|
+
bytesTransferred: result.bytesTransferred,
|
|
1343
|
+
completedAt,
|
|
1344
|
+
durationMs,
|
|
1345
|
+
jobId: job.id,
|
|
1346
|
+
operation: job.operation,
|
|
1347
|
+
resumed: result.resumed ?? job.resumed ?? false,
|
|
1348
|
+
startedAt,
|
|
1349
|
+
transferId: job.id,
|
|
1350
|
+
verified: verification?.verified ?? result.verified ?? false,
|
|
1351
|
+
warnings: [...result.warnings ?? []]
|
|
1352
|
+
};
|
|
1353
|
+
if (job.source !== void 0) receipt.source = { ...job.source };
|
|
1354
|
+
if (job.destination !== void 0) receipt.destination = { ...job.destination };
|
|
1355
|
+
if (result.totalBytes !== void 0) receipt.totalBytes = result.totalBytes;
|
|
1356
|
+
else if (job.totalBytes !== void 0) receipt.totalBytes = job.totalBytes;
|
|
1357
|
+
if (result.checksum !== void 0) receipt.checksum = result.checksum;
|
|
1358
|
+
else if (verification?.checksum !== void 0) receipt.checksum = verification.checksum;
|
|
1359
|
+
if (verification !== void 0) receipt.verification = verification;
|
|
1360
|
+
if (job.metadata !== void 0) receipt.metadata = { ...job.metadata };
|
|
1361
|
+
return receipt;
|
|
1362
|
+
}
|
|
1363
|
+
function normalizeVerificationResult(result) {
|
|
1364
|
+
const verification = result.verification;
|
|
1365
|
+
if (verification !== void 0) {
|
|
1366
|
+
const normalized2 = { verified: verification.verified };
|
|
1367
|
+
if (verification.method !== void 0) normalized2.method = verification.method;
|
|
1368
|
+
if (verification.checksum !== void 0) normalized2.checksum = verification.checksum;
|
|
1369
|
+
if (verification.expectedChecksum !== void 0) {
|
|
1370
|
+
normalized2.expectedChecksum = verification.expectedChecksum;
|
|
1371
|
+
}
|
|
1372
|
+
if (verification.actualChecksum !== void 0)
|
|
1373
|
+
normalized2.actualChecksum = verification.actualChecksum;
|
|
1374
|
+
if (verification.details !== void 0) normalized2.details = { ...verification.details };
|
|
1375
|
+
return normalized2;
|
|
1376
|
+
}
|
|
1377
|
+
if (result.verified === void 0 && result.checksum === void 0) {
|
|
1378
|
+
return void 0;
|
|
1379
|
+
}
|
|
1380
|
+
const normalized = { verified: result.verified ?? false };
|
|
1381
|
+
if (result.checksum !== void 0) {
|
|
1382
|
+
normalized.checksum = result.checksum;
|
|
1383
|
+
}
|
|
1384
|
+
return normalized;
|
|
1385
|
+
}
|
|
1386
|
+
function createTransferFailure(job, error, attempts) {
|
|
1387
|
+
return new TransferError({
|
|
1388
|
+
cause: error,
|
|
1389
|
+
details: {
|
|
1390
|
+
attempts,
|
|
1391
|
+
jobId: job.id,
|
|
1392
|
+
operation: job.operation
|
|
1393
|
+
},
|
|
1394
|
+
message: `Transfer job failed: ${job.id}`,
|
|
1395
|
+
retryable: isRetryable(error)
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
function summarizeError(error) {
|
|
1399
|
+
if (error instanceof ZeroTransferError) {
|
|
1400
|
+
return {
|
|
1401
|
+
code: error.code,
|
|
1402
|
+
message: error.message,
|
|
1403
|
+
name: error.name,
|
|
1404
|
+
retryable: error.retryable
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (error instanceof Error) {
|
|
1408
|
+
return {
|
|
1409
|
+
message: error.message,
|
|
1410
|
+
name: error.name
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
message: String(error),
|
|
1415
|
+
name: "Error"
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function isRetryable(error) {
|
|
1419
|
+
return error instanceof ZeroTransferError && error.retryable;
|
|
1420
|
+
}
|
|
1421
|
+
function calculateBytesPerSecond2(bytes, durationMs) {
|
|
1422
|
+
if (durationMs <= 0) {
|
|
1423
|
+
return bytes;
|
|
1424
|
+
}
|
|
1425
|
+
return bytes / (durationMs / 1e3);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/mft/runRoute.ts
|
|
1429
|
+
async function runRoute(options) {
|
|
1430
|
+
const { client, route } = options;
|
|
1431
|
+
if (route.enabled === false) {
|
|
1432
|
+
throw new ConfigurationError({
|
|
1433
|
+
details: { routeId: route.id },
|
|
1434
|
+
message: `MFT route "${route.id}" is disabled`,
|
|
1435
|
+
retryable: false
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
const sourceSession = await client.connect(route.source.profile);
|
|
1439
|
+
let destinationSession;
|
|
1440
|
+
try {
|
|
1441
|
+
destinationSession = await client.connect(route.destination.profile);
|
|
1442
|
+
const engine = options.engine ?? new TransferEngine();
|
|
1443
|
+
const job = createRouteJob(route, sourceSession, destinationSession, options);
|
|
1444
|
+
const sessions = /* @__PURE__ */ new Map([
|
|
1445
|
+
["source", sourceSession],
|
|
1446
|
+
["destination", destinationSession]
|
|
1447
|
+
]);
|
|
1448
|
+
const executor = createProviderTransferExecutor({
|
|
1449
|
+
resolveSession: ({ role }) => sessions.get(role)
|
|
1450
|
+
});
|
|
1451
|
+
return await engine.execute(job, executor, buildExecuteOptions(options));
|
|
1452
|
+
} finally {
|
|
1453
|
+
if (destinationSession !== void 0) {
|
|
1454
|
+
await destinationSession.disconnect();
|
|
1455
|
+
}
|
|
1456
|
+
await sourceSession.disconnect();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function createRouteJob(route, sourceSession, destinationSession, options) {
|
|
1460
|
+
const operation = route.operation ?? "copy";
|
|
1461
|
+
const source = {
|
|
1462
|
+
path: route.source.path,
|
|
1463
|
+
provider: sourceSession.provider
|
|
1464
|
+
};
|
|
1465
|
+
const destination = {
|
|
1466
|
+
path: route.destination.path,
|
|
1467
|
+
provider: destinationSession.provider
|
|
1468
|
+
};
|
|
1469
|
+
const baseMetadata = { routeId: route.id };
|
|
1470
|
+
if (route.name !== void 0) baseMetadata["routeName"] = route.name;
|
|
1471
|
+
if (route.metadata !== void 0) Object.assign(baseMetadata, route.metadata);
|
|
1472
|
+
if (options.metadata !== void 0) Object.assign(baseMetadata, options.metadata);
|
|
1473
|
+
const job = {
|
|
1474
|
+
destination,
|
|
1475
|
+
id: options.jobId ?? defaultJobId(route, options.now),
|
|
1476
|
+
operation,
|
|
1477
|
+
source
|
|
1478
|
+
};
|
|
1479
|
+
if (Object.keys(baseMetadata).length > 0) {
|
|
1480
|
+
job.metadata = baseMetadata;
|
|
1481
|
+
}
|
|
1482
|
+
return job;
|
|
1483
|
+
}
|
|
1484
|
+
function defaultJobId(route, now) {
|
|
1485
|
+
const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1486
|
+
return `route:${route.id}:${timestamp.toString(36)}`;
|
|
1487
|
+
}
|
|
1488
|
+
function buildExecuteOptions(options) {
|
|
1489
|
+
const execute = {};
|
|
1490
|
+
if (options.signal !== void 0) execute.signal = options.signal;
|
|
1491
|
+
if (options.retry !== void 0) execute.retry = options.retry;
|
|
1492
|
+
if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
|
|
1493
|
+
if (options.timeout !== void 0) execute.timeout = options.timeout;
|
|
1494
|
+
if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
|
|
1495
|
+
return execute;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/client/operations.ts
|
|
1499
|
+
var LOCAL_PROFILE = { host: "local", provider: "local" };
|
|
1500
|
+
function uploadFile(options) {
|
|
1501
|
+
const { client, destination, localPath, routeId, routeName, ...rest } = options;
|
|
1502
|
+
const route = buildRoute({
|
|
1503
|
+
destination: { path: destination.path, profile: destination.profile },
|
|
1504
|
+
id: routeId ?? `upload:${defaultRouteSuffix(localPath, destination.path)}`,
|
|
1505
|
+
name: routeName,
|
|
1506
|
+
operation: "upload",
|
|
1507
|
+
source: { path: absolutePath(localPath), profile: LOCAL_PROFILE }
|
|
1508
|
+
});
|
|
1509
|
+
return runRoute({ client, route, ...rest });
|
|
1510
|
+
}
|
|
1511
|
+
function downloadFile(options) {
|
|
1512
|
+
const { client, localPath, routeId, routeName, source, ...rest } = options;
|
|
1513
|
+
const route = buildRoute({
|
|
1514
|
+
destination: { path: absolutePath(localPath), profile: LOCAL_PROFILE },
|
|
1515
|
+
id: routeId ?? `download:${defaultRouteSuffix(source.path, localPath)}`,
|
|
1516
|
+
name: routeName,
|
|
1517
|
+
operation: "download",
|
|
1518
|
+
source: { path: source.path, profile: source.profile }
|
|
1519
|
+
});
|
|
1520
|
+
return runRoute({ client, route, ...rest });
|
|
1521
|
+
}
|
|
1522
|
+
function copyBetween(options) {
|
|
1523
|
+
const { client, destination, routeId, routeName, source, ...rest } = options;
|
|
1524
|
+
const route = buildRoute({
|
|
1525
|
+
destination: { path: destination.path, profile: destination.profile },
|
|
1526
|
+
id: routeId ?? `copy:${defaultRouteSuffix(source.path, destination.path)}`,
|
|
1527
|
+
name: routeName,
|
|
1528
|
+
operation: "copy",
|
|
1529
|
+
source: { path: source.path, profile: source.profile }
|
|
1530
|
+
});
|
|
1531
|
+
return runRoute({ client, route, ...rest });
|
|
1532
|
+
}
|
|
1533
|
+
function buildRoute(input) {
|
|
1534
|
+
const route = {
|
|
1535
|
+
destination: input.destination,
|
|
1536
|
+
id: input.id,
|
|
1537
|
+
operation: input.operation,
|
|
1538
|
+
source: input.source
|
|
1539
|
+
};
|
|
1540
|
+
if (input.name !== void 0) route.name = input.name;
|
|
1541
|
+
return route;
|
|
1542
|
+
}
|
|
1543
|
+
function absolutePath(localPath) {
|
|
1544
|
+
return isAbsolute(localPath) ? localPath : resolvePath(localPath);
|
|
1545
|
+
}
|
|
1546
|
+
function defaultRouteSuffix(source, destination) {
|
|
1547
|
+
return `${source}->${destination}`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/logging/redaction.ts
|
|
1551
|
+
var REDACTED = "[REDACTED]";
|
|
1552
|
+
var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
|
|
1553
|
+
var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
|
|
1554
|
+
function isSensitiveKey(key) {
|
|
1555
|
+
return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
|
|
1556
|
+
}
|
|
1557
|
+
function redactCommand(command) {
|
|
1558
|
+
return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
|
|
1559
|
+
return `${commandName.toUpperCase()} ${REDACTED}`;
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
function redactValue(value) {
|
|
1563
|
+
if (typeof value === "string") {
|
|
1564
|
+
return redactCommand(value);
|
|
1565
|
+
}
|
|
1566
|
+
if (Array.isArray(value)) {
|
|
1567
|
+
return value.map((item) => redactValue(item));
|
|
1568
|
+
}
|
|
1569
|
+
if (value !== null && typeof value === "object") {
|
|
1570
|
+
return redactObject(value);
|
|
1571
|
+
}
|
|
1572
|
+
return value;
|
|
1573
|
+
}
|
|
1574
|
+
function redactObject(input) {
|
|
1575
|
+
return Object.fromEntries(
|
|
1576
|
+
Object.entries(input).map(([key, value]) => {
|
|
1577
|
+
if (isSensitiveKey(key)) {
|
|
1578
|
+
return [key, REDACTED];
|
|
1579
|
+
}
|
|
1580
|
+
return [key, redactValue(value)];
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/profiles/SecretSource.ts
|
|
1586
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
1587
|
+
import { readFile } from "fs/promises";
|
|
1588
|
+
async function resolveSecret(source, options = {}) {
|
|
1589
|
+
if (isSecretValue(source)) {
|
|
1590
|
+
return cloneSecretValue(source);
|
|
1591
|
+
}
|
|
1592
|
+
if (typeof source === "function") {
|
|
1593
|
+
return cloneSecretValue(await source());
|
|
1594
|
+
}
|
|
1595
|
+
if (isValueSecretSource(source)) {
|
|
1596
|
+
return cloneSecretValue(source.value);
|
|
1597
|
+
}
|
|
1598
|
+
if (isEnvSecretSource(source)) {
|
|
1599
|
+
const value = (options.env ?? process.env)[source.env];
|
|
1600
|
+
if (value === void 0) {
|
|
1601
|
+
throw createSecretConfigurationError(
|
|
1602
|
+
"Secret environment variable is not set",
|
|
1603
|
+
"env",
|
|
1604
|
+
source.env
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
return value;
|
|
1608
|
+
}
|
|
1609
|
+
if (isBase64EnvSecretSource(source)) {
|
|
1610
|
+
const value = (options.env ?? process.env)[source.base64Env];
|
|
1611
|
+
if (value === void 0) {
|
|
1612
|
+
throw createSecretConfigurationError(
|
|
1613
|
+
"Secret environment variable is not set",
|
|
1614
|
+
"base64Env",
|
|
1615
|
+
source.base64Env
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
return Buffer2.from(value, "base64");
|
|
1619
|
+
}
|
|
1620
|
+
if (isFileSecretSource(source)) {
|
|
1621
|
+
const fileReader = options.readFile ?? readFile;
|
|
1622
|
+
const value = await fileReader(source.path);
|
|
1623
|
+
if (source.encoding === "buffer") {
|
|
1624
|
+
return Buffer2.from(value);
|
|
1625
|
+
}
|
|
1626
|
+
return value.toString(source.encoding ?? "utf8");
|
|
1627
|
+
}
|
|
1628
|
+
throw createSecretConfigurationError("Unsupported secret source", "source", "unknown");
|
|
1629
|
+
}
|
|
1630
|
+
function redactSecretSource(source) {
|
|
1631
|
+
if (isSecretValue(source) || typeof source === "function") {
|
|
1632
|
+
return REDACTED;
|
|
1633
|
+
}
|
|
1634
|
+
if (isValueSecretSource(source)) return { value: REDACTED };
|
|
1635
|
+
if (isEnvSecretSource(source)) return { env: REDACTED };
|
|
1636
|
+
if (isBase64EnvSecretSource(source)) return { base64Env: REDACTED };
|
|
1637
|
+
if (isFileSecretSource(source)) return { encoding: source.encoding, path: REDACTED };
|
|
1638
|
+
return REDACTED;
|
|
1639
|
+
}
|
|
1640
|
+
function isSecretValue(value) {
|
|
1641
|
+
return typeof value === "string" || Buffer2.isBuffer(value);
|
|
1642
|
+
}
|
|
1643
|
+
function isValueSecretSource(value) {
|
|
1644
|
+
return isRecord(value) && "value" in value && isSecretValue(value.value);
|
|
1645
|
+
}
|
|
1646
|
+
function isEnvSecretSource(value) {
|
|
1647
|
+
return isRecord(value) && typeof value.env === "string";
|
|
1648
|
+
}
|
|
1649
|
+
function isBase64EnvSecretSource(value) {
|
|
1650
|
+
return isRecord(value) && typeof value.base64Env === "string";
|
|
1651
|
+
}
|
|
1652
|
+
function isFileSecretSource(value) {
|
|
1653
|
+
return isRecord(value) && typeof value.path === "string";
|
|
1654
|
+
}
|
|
1655
|
+
function isRecord(value) {
|
|
1656
|
+
return typeof value === "object" && value !== null;
|
|
1657
|
+
}
|
|
1658
|
+
function cloneSecretValue(value) {
|
|
1659
|
+
return Buffer2.isBuffer(value) ? Buffer2.from(value) : value;
|
|
1660
|
+
}
|
|
1661
|
+
function createSecretConfigurationError(message, sourceType, sourceName) {
|
|
1662
|
+
return new ConfigurationError({
|
|
1663
|
+
details: { sourceName, sourceType },
|
|
1664
|
+
message,
|
|
1665
|
+
retryable: false
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/profiles/ProfileRedactor.ts
|
|
1670
|
+
function redactConnectionProfile(profile) {
|
|
1671
|
+
const { logger, password, signal, ssh, tls, username, ...rest } = profile;
|
|
1672
|
+
const redacted = redactObject(rest);
|
|
1673
|
+
if (username !== void 0) redacted.username = redactSecretSource(username);
|
|
1674
|
+
if (password !== void 0) redacted.password = redactSecretSource(password);
|
|
1675
|
+
if (ssh !== void 0) redacted.ssh = redactSshProfile(ssh);
|
|
1676
|
+
if (tls !== void 0) redacted.tls = redactTlsProfile(tls);
|
|
1677
|
+
if (signal !== void 0) redacted.signal = "[AbortSignal]";
|
|
1678
|
+
if (logger !== void 0) redacted.logger = REDACTED;
|
|
1679
|
+
return redacted;
|
|
1680
|
+
}
|
|
1681
|
+
function redactSshProfile(profile) {
|
|
1682
|
+
const { agent, keyboardInteractive, knownHosts, passphrase, privateKey, socketFactory, ...rest } = profile;
|
|
1683
|
+
const redacted = redactObject(rest);
|
|
1684
|
+
if (agent !== void 0) redacted.agent = REDACTED;
|
|
1685
|
+
if (privateKey !== void 0) redacted.privateKey = redactSecretSource(privateKey);
|
|
1686
|
+
if (passphrase !== void 0) redacted.passphrase = redactSecretSource(passphrase);
|
|
1687
|
+
if (knownHosts !== void 0) redacted.knownHosts = redactSshKnownHostsSource(knownHosts);
|
|
1688
|
+
if (keyboardInteractive !== void 0) redacted.keyboardInteractive = REDACTED;
|
|
1689
|
+
if (socketFactory !== void 0) redacted.socketFactory = REDACTED;
|
|
1690
|
+
return redacted;
|
|
1691
|
+
}
|
|
1692
|
+
function redactSshKnownHostsSource(source) {
|
|
1693
|
+
if (Array.isArray(source)) {
|
|
1694
|
+
return source.map((item) => redactSecretSource(item));
|
|
1695
|
+
}
|
|
1696
|
+
return redactSecretSource(source);
|
|
1697
|
+
}
|
|
1698
|
+
function redactTlsProfile(profile) {
|
|
1699
|
+
const { ca, cert, checkServerIdentity, key, passphrase, pfx, ...rest } = profile;
|
|
1700
|
+
const redacted = redactObject(rest);
|
|
1701
|
+
if (ca !== void 0) redacted.ca = redactTlsSecretSource(ca);
|
|
1702
|
+
if (cert !== void 0) redacted.cert = redactSecretSource(cert);
|
|
1703
|
+
if (key !== void 0) redacted.key = redactSecretSource(key);
|
|
1704
|
+
if (passphrase !== void 0) redacted.passphrase = redactSecretSource(passphrase);
|
|
1705
|
+
if (pfx !== void 0) redacted.pfx = redactSecretSource(pfx);
|
|
1706
|
+
if (checkServerIdentity !== void 0) redacted.checkServerIdentity = REDACTED;
|
|
1707
|
+
return redacted;
|
|
1708
|
+
}
|
|
1709
|
+
function redactTlsSecretSource(source) {
|
|
1710
|
+
if (Array.isArray(source)) {
|
|
1711
|
+
return source.map((item) => redactSecretSource(item));
|
|
1712
|
+
}
|
|
1713
|
+
return redactSecretSource(source);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/diagnostics/index.ts
|
|
1717
|
+
function summarizeClientDiagnostics(client) {
|
|
1718
|
+
const capabilities = client.getCapabilities();
|
|
1719
|
+
return {
|
|
1720
|
+
providers: capabilities.map((entry) => ({ capabilities: entry, id: entry.provider }))
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
async function runConnectionDiagnostics(options) {
|
|
1724
|
+
const now = options.now ?? (() => performance.now());
|
|
1725
|
+
const probeList = options.probeList !== false;
|
|
1726
|
+
const listPath = options.listPath ?? "/";
|
|
1727
|
+
const sampleSize = Math.max(0, options.sampleSize ?? 5);
|
|
1728
|
+
const redactedProfile = redactConnectionProfile(options.profile);
|
|
1729
|
+
const result = {
|
|
1730
|
+
host: options.profile.host,
|
|
1731
|
+
ok: false,
|
|
1732
|
+
redactedProfile,
|
|
1733
|
+
timings: {}
|
|
1734
|
+
};
|
|
1735
|
+
const connectStart = now();
|
|
1736
|
+
try {
|
|
1737
|
+
const session = await options.client.connect(options.profile);
|
|
1738
|
+
result.timings.connectMs = now() - connectStart;
|
|
1739
|
+
result.provider = session.provider;
|
|
1740
|
+
result.capabilities = session.capabilities;
|
|
1741
|
+
try {
|
|
1742
|
+
if (probeList) {
|
|
1743
|
+
const listStart = now();
|
|
1744
|
+
const entries = await session.fs.list(listPath);
|
|
1745
|
+
result.timings.listMs = now() - listStart;
|
|
1746
|
+
result.sample = entries.slice(0, sampleSize);
|
|
1747
|
+
}
|
|
1748
|
+
result.ok = true;
|
|
1749
|
+
} finally {
|
|
1750
|
+
const disconnectStart = now();
|
|
1751
|
+
await session.disconnect();
|
|
1752
|
+
result.timings.disconnectMs = now() - disconnectStart;
|
|
1753
|
+
}
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
result.error = summarizeDiagnosticError(error);
|
|
1756
|
+
}
|
|
1757
|
+
return result;
|
|
1758
|
+
}
|
|
1759
|
+
function summarizeDiagnosticError(error) {
|
|
1760
|
+
if (error instanceof Error) {
|
|
1761
|
+
const summary = { message: error.message };
|
|
1762
|
+
if (error.name !== "Error") summary.name = error.name;
|
|
1763
|
+
const code = error.code;
|
|
1764
|
+
if (typeof code === "string") summary.code = code;
|
|
1765
|
+
return summary;
|
|
1766
|
+
}
|
|
1767
|
+
return { message: String(error) };
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/providers/local/LocalProvider.ts
|
|
1771
|
+
import { createReadStream } from "fs";
|
|
1772
|
+
import {
|
|
1773
|
+
lstat,
|
|
1774
|
+
mkdir,
|
|
1775
|
+
open,
|
|
1776
|
+
readdir,
|
|
1777
|
+
readlink,
|
|
1778
|
+
rename,
|
|
1779
|
+
rm,
|
|
1780
|
+
unlink,
|
|
1781
|
+
writeFile
|
|
1782
|
+
} from "fs/promises";
|
|
1783
|
+
import path from "path";
|
|
1784
|
+
|
|
1785
|
+
// src/utils/path.ts
|
|
1786
|
+
var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
|
|
1787
|
+
function assertSafeFtpArgument(value, label = "path") {
|
|
1788
|
+
if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
|
|
1789
|
+
throw new ConfigurationError({
|
|
1790
|
+
message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
|
|
1791
|
+
retryable: false,
|
|
1792
|
+
details: {
|
|
1793
|
+
label
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
return value;
|
|
1798
|
+
}
|
|
1799
|
+
function normalizeRemotePath(input) {
|
|
1800
|
+
assertSafeFtpArgument(input);
|
|
1801
|
+
if (input.length === 0) {
|
|
1802
|
+
return ".";
|
|
1803
|
+
}
|
|
1804
|
+
const isAbsolute2 = input.startsWith("/");
|
|
1805
|
+
const segments = [];
|
|
1806
|
+
for (const segment of input.split(/[\\/]+/)) {
|
|
1807
|
+
if (segment.length === 0 || segment === ".") {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (segment === "..") {
|
|
1811
|
+
if (segments.length > 0 && segments[segments.length - 1] !== "..") {
|
|
1812
|
+
segments.pop();
|
|
1813
|
+
} else if (!isAbsolute2) {
|
|
1814
|
+
segments.push(segment);
|
|
1815
|
+
}
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
segments.push(segment);
|
|
1819
|
+
}
|
|
1820
|
+
const normalized = segments.join("/");
|
|
1821
|
+
if (isAbsolute2) {
|
|
1822
|
+
return normalized.length > 0 ? `/${normalized}` : "/";
|
|
1823
|
+
}
|
|
1824
|
+
return normalized.length > 0 ? normalized : ".";
|
|
1825
|
+
}
|
|
1826
|
+
function joinRemotePath(...segments) {
|
|
1827
|
+
if (segments.length === 0) {
|
|
1828
|
+
return ".";
|
|
1829
|
+
}
|
|
1830
|
+
return normalizeRemotePath(segments.join("/"));
|
|
1831
|
+
}
|
|
1832
|
+
function basenameRemotePath(input) {
|
|
1833
|
+
const normalized = normalizeRemotePath(input);
|
|
1834
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
1835
|
+
return parts[parts.length - 1] ?? normalized;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/providers/local/LocalProvider.ts
|
|
1839
|
+
var LOCAL_PROVIDER_ID = "local";
|
|
1840
|
+
var LOCAL_PROVIDER_CAPABILITIES = {
|
|
1841
|
+
provider: LOCAL_PROVIDER_ID,
|
|
1842
|
+
authentication: ["anonymous"],
|
|
1843
|
+
list: true,
|
|
1844
|
+
stat: true,
|
|
1845
|
+
readStream: true,
|
|
1846
|
+
writeStream: true,
|
|
1847
|
+
serverSideCopy: false,
|
|
1848
|
+
serverSideMove: false,
|
|
1849
|
+
resumeDownload: true,
|
|
1850
|
+
resumeUpload: true,
|
|
1851
|
+
checksum: [],
|
|
1852
|
+
atomicRename: false,
|
|
1853
|
+
chmod: false,
|
|
1854
|
+
chown: false,
|
|
1855
|
+
symlink: true,
|
|
1856
|
+
metadata: ["accessedAt", "createdAt", "modifiedAt", "permissions", "symlinkTarget", "uniqueId"],
|
|
1857
|
+
maxConcurrency: 16,
|
|
1858
|
+
notes: ["Local filesystem provider for tests and local-only workflows"]
|
|
1859
|
+
};
|
|
1860
|
+
function createLocalProviderFactory(options = {}) {
|
|
1861
|
+
return {
|
|
1862
|
+
id: LOCAL_PROVIDER_ID,
|
|
1863
|
+
capabilities: LOCAL_PROVIDER_CAPABILITIES,
|
|
1864
|
+
create: () => new LocalProvider(options.rootPath)
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
var LocalProvider = class {
|
|
1868
|
+
constructor(configuredRootPath) {
|
|
1869
|
+
this.configuredRootPath = configuredRootPath;
|
|
1870
|
+
}
|
|
1871
|
+
configuredRootPath;
|
|
1872
|
+
id = LOCAL_PROVIDER_ID;
|
|
1873
|
+
capabilities = LOCAL_PROVIDER_CAPABILITIES;
|
|
1874
|
+
connect(profile) {
|
|
1875
|
+
return Promise.resolve().then(() => {
|
|
1876
|
+
const rootPath = path.resolve(this.configuredRootPath ?? profile.host);
|
|
1877
|
+
return new LocalTransferSession(rootPath);
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
var LocalTransferSession = class {
|
|
1882
|
+
provider = LOCAL_PROVIDER_ID;
|
|
1883
|
+
capabilities = LOCAL_PROVIDER_CAPABILITIES;
|
|
1884
|
+
fs;
|
|
1885
|
+
transfers;
|
|
1886
|
+
constructor(rootPath) {
|
|
1887
|
+
this.fs = new LocalFileSystem(rootPath);
|
|
1888
|
+
this.transfers = new LocalTransferOperations(rootPath);
|
|
1889
|
+
}
|
|
1890
|
+
disconnect() {
|
|
1891
|
+
return Promise.resolve();
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
var LocalTransferOperations = class {
|
|
1895
|
+
constructor(rootPath) {
|
|
1896
|
+
this.rootPath = rootPath;
|
|
1897
|
+
}
|
|
1898
|
+
rootPath;
|
|
1899
|
+
async read(request) {
|
|
1900
|
+
request.throwIfAborted();
|
|
1901
|
+
const remotePath = normalizeLocalProviderPath(request.endpoint.path);
|
|
1902
|
+
const entry = await readLocalEntry(this.rootPath, remotePath);
|
|
1903
|
+
if (entry.type !== "file") {
|
|
1904
|
+
throw createPathNotFoundError(remotePath, `Local provider path is not a file: ${remotePath}`);
|
|
1905
|
+
}
|
|
1906
|
+
const range = resolveReadRange(entry.size ?? 0, request.range);
|
|
1907
|
+
const result = {
|
|
1908
|
+
content: createLocalReadSource(resolveLocalPath(this.rootPath, remotePath), range),
|
|
1909
|
+
totalBytes: range.length
|
|
1910
|
+
};
|
|
1911
|
+
if (range.offset > 0) {
|
|
1912
|
+
result.bytesRead = range.offset;
|
|
1913
|
+
}
|
|
1914
|
+
return result;
|
|
1915
|
+
}
|
|
1916
|
+
async write(request) {
|
|
1917
|
+
request.throwIfAborted();
|
|
1918
|
+
const remotePath = normalizeLocalProviderPath(request.endpoint.path);
|
|
1919
|
+
const localPath = resolveLocalPath(this.rootPath, remotePath);
|
|
1920
|
+
const content = await collectTransferContent(request);
|
|
1921
|
+
const offset = normalizeOptionalByteCount(request.offset, "offset", remotePath);
|
|
1922
|
+
await ensureLocalParentDirectory(localPath, remotePath);
|
|
1923
|
+
await writeLocalContent(localPath, remotePath, content, offset);
|
|
1924
|
+
const stat = await readLocalEntry(this.rootPath, remotePath);
|
|
1925
|
+
const result = {
|
|
1926
|
+
bytesTransferred: content.byteLength,
|
|
1927
|
+
resumed: offset !== void 0 && offset > 0,
|
|
1928
|
+
verified: request.verification?.verified ?? false
|
|
1929
|
+
};
|
|
1930
|
+
if (stat.size !== void 0) {
|
|
1931
|
+
result.totalBytes = stat.size;
|
|
1932
|
+
}
|
|
1933
|
+
if (request.verification !== void 0) {
|
|
1934
|
+
result.verification = cloneVerification2(request.verification);
|
|
1935
|
+
}
|
|
1936
|
+
return result;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
var LocalFileSystem = class {
|
|
1940
|
+
constructor(rootPath) {
|
|
1941
|
+
this.rootPath = rootPath;
|
|
1942
|
+
}
|
|
1943
|
+
rootPath;
|
|
1944
|
+
async list(path2) {
|
|
1945
|
+
const remotePath = normalizeLocalProviderPath(path2);
|
|
1946
|
+
const directory = await this.stat(remotePath);
|
|
1947
|
+
if (directory.type !== "directory") {
|
|
1948
|
+
throw createPathNotFoundError(
|
|
1949
|
+
remotePath,
|
|
1950
|
+
`Local provider path is not a directory: ${remotePath}`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
const localPath = resolveLocalPath(this.rootPath, remotePath);
|
|
1954
|
+
const names = await readLocalDirectory(localPath, remotePath);
|
|
1955
|
+
const entries = await Promise.all(
|
|
1956
|
+
names.map((name) => readLocalEntry(this.rootPath, joinRemotePath(remotePath, name)))
|
|
1957
|
+
);
|
|
1958
|
+
return entries.sort(compareEntries);
|
|
1959
|
+
}
|
|
1960
|
+
async stat(path2) {
|
|
1961
|
+
return readLocalEntry(this.rootPath, normalizeLocalProviderPath(path2));
|
|
1962
|
+
}
|
|
1963
|
+
async remove(remote, options = {}) {
|
|
1964
|
+
const remotePath = normalizeLocalProviderPath(remote);
|
|
1965
|
+
const localPath = resolveLocalPath(this.rootPath, remotePath);
|
|
1966
|
+
try {
|
|
1967
|
+
await unlink(localPath);
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
if (options.ignoreMissing && isNodeErrno(error, "ENOENT")) return;
|
|
1970
|
+
if (isNodeErrno(error, "ENOENT")) {
|
|
1971
|
+
throw createPathNotFoundError(remotePath, `Local path not found: ${remotePath}`);
|
|
1972
|
+
}
|
|
1973
|
+
throw error;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
async rename(from, to) {
|
|
1977
|
+
const fromRemote = normalizeLocalProviderPath(from);
|
|
1978
|
+
const toRemote = normalizeLocalProviderPath(to);
|
|
1979
|
+
const fromLocal = resolveLocalPath(this.rootPath, fromRemote);
|
|
1980
|
+
const toLocal = resolveLocalPath(this.rootPath, toRemote);
|
|
1981
|
+
try {
|
|
1982
|
+
await rename(fromLocal, toLocal);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
if (isNodeErrno(error, "ENOENT")) {
|
|
1985
|
+
throw createPathNotFoundError(fromRemote, `Local path not found: ${fromRemote}`);
|
|
1986
|
+
}
|
|
1987
|
+
throw error;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async mkdir(remote, options = {}) {
|
|
1991
|
+
const remotePath = normalizeLocalProviderPath(remote);
|
|
1992
|
+
const localPath = resolveLocalPath(this.rootPath, remotePath);
|
|
1993
|
+
await mkdir(localPath, { recursive: options.recursive === true });
|
|
1994
|
+
}
|
|
1995
|
+
async rmdir(remote, options = {}) {
|
|
1996
|
+
const remotePath = normalizeLocalProviderPath(remote);
|
|
1997
|
+
const localPath = resolveLocalPath(this.rootPath, remotePath);
|
|
1998
|
+
try {
|
|
1999
|
+
await rm(localPath, { recursive: options.recursive === true, force: false });
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
if (isNodeErrno(error, "ENOENT")) {
|
|
2002
|
+
if (options.ignoreMissing) return;
|
|
2003
|
+
throw createPathNotFoundError(remotePath, `Local path not found: ${remotePath}`);
|
|
2004
|
+
}
|
|
2005
|
+
throw error;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
function isNodeErrno(error, code) {
|
|
2010
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2011
|
+
}
|
|
2012
|
+
function resolveReadRange(size, range) {
|
|
2013
|
+
if (range === void 0) {
|
|
2014
|
+
return { length: size, offset: 0 };
|
|
2015
|
+
}
|
|
2016
|
+
const requestedOffset = normalizeByteCount(range.offset, "offset", "/");
|
|
2017
|
+
const requestedLength = range.length === void 0 ? size - Math.min(requestedOffset, size) : normalizeByteCount(range.length, "length", "/");
|
|
2018
|
+
const offset = Math.min(requestedOffset, size);
|
|
2019
|
+
const length = Math.max(0, Math.min(requestedLength, size - offset));
|
|
2020
|
+
return { length, offset };
|
|
2021
|
+
}
|
|
2022
|
+
async function* createLocalReadSource(localPath, range) {
|
|
2023
|
+
if (range.length <= 0) {
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const stream = createReadStream(localPath, {
|
|
2027
|
+
end: range.offset + range.length - 1,
|
|
2028
|
+
start: range.offset
|
|
2029
|
+
});
|
|
2030
|
+
for await (const chunk of stream) {
|
|
2031
|
+
yield new Uint8Array(chunk);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
async function collectTransferContent(request) {
|
|
2035
|
+
const chunks = [];
|
|
2036
|
+
let byteLength = 0;
|
|
2037
|
+
for await (const chunk of request.content) {
|
|
2038
|
+
request.throwIfAborted();
|
|
2039
|
+
const clonedChunk = new Uint8Array(chunk);
|
|
2040
|
+
chunks.push(clonedChunk);
|
|
2041
|
+
byteLength += clonedChunk.byteLength;
|
|
2042
|
+
request.reportProgress(byteLength, request.totalBytes);
|
|
2043
|
+
}
|
|
2044
|
+
return concatChunks(chunks, byteLength);
|
|
2045
|
+
}
|
|
2046
|
+
function concatChunks(chunks, byteLength) {
|
|
2047
|
+
const content = new Uint8Array(byteLength);
|
|
2048
|
+
let offset = 0;
|
|
2049
|
+
for (const chunk of chunks) {
|
|
2050
|
+
content.set(chunk, offset);
|
|
2051
|
+
offset += chunk.byteLength;
|
|
2052
|
+
}
|
|
2053
|
+
return content;
|
|
2054
|
+
}
|
|
2055
|
+
async function ensureLocalParentDirectory(localPath, remotePath) {
|
|
2056
|
+
try {
|
|
2057
|
+
await mkdir(path.dirname(localPath), { recursive: true });
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
throw mapLocalFileSystemError(error, remotePath);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async function writeLocalContent(localPath, remotePath, content, offset) {
|
|
2063
|
+
try {
|
|
2064
|
+
if (offset === void 0) {
|
|
2065
|
+
await writeFile(localPath, content);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const handle = await openLocalFileForOffsetWrite(localPath);
|
|
2069
|
+
try {
|
|
2070
|
+
await handle.write(content, 0, content.byteLength, offset);
|
|
2071
|
+
} finally {
|
|
2072
|
+
await handle.close();
|
|
2073
|
+
}
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
throw mapLocalFileSystemError(error, remotePath);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
async function openLocalFileForOffsetWrite(localPath) {
|
|
2079
|
+
try {
|
|
2080
|
+
return await open(localPath, "r+");
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
2083
|
+
return open(localPath, "w+");
|
|
2084
|
+
}
|
|
2085
|
+
throw error;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function normalizeOptionalByteCount(value, field, remotePath) {
|
|
2089
|
+
return value === void 0 ? void 0 : normalizeByteCount(value, field, remotePath);
|
|
2090
|
+
}
|
|
2091
|
+
function normalizeByteCount(value, field, remotePath) {
|
|
2092
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2093
|
+
throw new ConfigurationError({
|
|
2094
|
+
details: { field, provider: LOCAL_PROVIDER_ID },
|
|
2095
|
+
message: `Local provider ${field} must be a non-negative number`,
|
|
2096
|
+
path: remotePath,
|
|
2097
|
+
retryable: false
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
return Math.floor(value);
|
|
2101
|
+
}
|
|
2102
|
+
function cloneVerification2(verification) {
|
|
2103
|
+
const clone = { verified: verification.verified };
|
|
2104
|
+
if (verification.method !== void 0) clone.method = verification.method;
|
|
2105
|
+
if (verification.checksum !== void 0) clone.checksum = verification.checksum;
|
|
2106
|
+
if (verification.expectedChecksum !== void 0) {
|
|
2107
|
+
clone.expectedChecksum = verification.expectedChecksum;
|
|
2108
|
+
}
|
|
2109
|
+
if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
|
|
2110
|
+
if (verification.details !== void 0) clone.details = { ...verification.details };
|
|
2111
|
+
return clone;
|
|
2112
|
+
}
|
|
2113
|
+
async function readLocalDirectory(localPath, remotePath) {
|
|
2114
|
+
try {
|
|
2115
|
+
return await readdir(localPath);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
throw mapLocalFileSystemError(error, remotePath);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
async function readLocalEntry(rootPath, remotePath) {
|
|
2121
|
+
const localPath = resolveLocalPath(rootPath, remotePath);
|
|
2122
|
+
let stats;
|
|
2123
|
+
try {
|
|
2124
|
+
stats = await lstat(localPath);
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
throw mapLocalFileSystemError(error, remotePath);
|
|
2127
|
+
}
|
|
2128
|
+
const entry = {
|
|
2129
|
+
accessedAt: cloneDate(stats.atime),
|
|
2130
|
+
createdAt: cloneDate(stats.birthtime),
|
|
2131
|
+
modifiedAt: cloneDate(stats.mtime),
|
|
2132
|
+
name: basenameRemotePath(remotePath),
|
|
2133
|
+
path: remotePath,
|
|
2134
|
+
permissions: { raw: formatMode(stats.mode) },
|
|
2135
|
+
size: stats.size,
|
|
2136
|
+
type: getLocalEntryType(stats),
|
|
2137
|
+
uniqueId: `${stats.dev}:${stats.ino}`
|
|
2138
|
+
};
|
|
2139
|
+
if (entry.type === "symlink") {
|
|
2140
|
+
const symlinkTarget = await readSymlinkTarget(localPath);
|
|
2141
|
+
if (symlinkTarget !== void 0) {
|
|
2142
|
+
entry.symlinkTarget = symlinkTarget;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return {
|
|
2146
|
+
...entry,
|
|
2147
|
+
exists: true
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
async function readSymlinkTarget(localPath) {
|
|
2151
|
+
try {
|
|
2152
|
+
return await readlink(localPath);
|
|
2153
|
+
} catch {
|
|
2154
|
+
return void 0;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function normalizeLocalProviderPath(input) {
|
|
2158
|
+
const normalized = normalizeRemotePath(input);
|
|
2159
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
2160
|
+
throw new ConfigurationError({
|
|
2161
|
+
details: { provider: LOCAL_PROVIDER_ID },
|
|
2162
|
+
message: `Local provider path escapes the configured root: ${normalized}`,
|
|
2163
|
+
path: normalized,
|
|
2164
|
+
retryable: false
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
if (normalized === "." || normalized === "/") {
|
|
2168
|
+
return "/";
|
|
2169
|
+
}
|
|
2170
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2171
|
+
}
|
|
2172
|
+
function resolveLocalPath(rootPath, remotePath) {
|
|
2173
|
+
const normalizedRemotePath = normalizeLocalProviderPath(remotePath);
|
|
2174
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
2175
|
+
const candidateAbsolute = path.resolve(normalizedRemotePath.split("/").join(path.sep));
|
|
2176
|
+
if (candidateAbsolute === resolvedRootPath || candidateAbsolute.startsWith(resolvedRootPath + path.sep)) {
|
|
2177
|
+
return candidateAbsolute;
|
|
2178
|
+
}
|
|
2179
|
+
const relativePath = normalizedRemotePath === "/" ? "." : normalizedRemotePath.slice(1);
|
|
2180
|
+
const resolvedPath = path.resolve(rootPath, relativePath.split("/").join(path.sep));
|
|
2181
|
+
const relativeToRoot = path.relative(rootPath, resolvedPath);
|
|
2182
|
+
if (relativeToRoot === "" || !relativeToRoot.startsWith("..") && !path.isAbsolute(relativeToRoot)) {
|
|
2183
|
+
return resolvedPath;
|
|
2184
|
+
}
|
|
2185
|
+
throw new ConfigurationError({
|
|
2186
|
+
details: { provider: LOCAL_PROVIDER_ID, rootPath },
|
|
2187
|
+
message: `Local provider path escapes the configured root: ${normalizedRemotePath}`,
|
|
2188
|
+
path: normalizedRemotePath,
|
|
2189
|
+
retryable: false
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
function getLocalEntryType(stats) {
|
|
2193
|
+
if (stats.isFile()) return "file";
|
|
2194
|
+
if (stats.isDirectory()) return "directory";
|
|
2195
|
+
if (stats.isSymbolicLink()) return "symlink";
|
|
2196
|
+
return "unknown";
|
|
2197
|
+
}
|
|
2198
|
+
function formatMode(mode) {
|
|
2199
|
+
return (mode & 511).toString(8).padStart(3, "0");
|
|
2200
|
+
}
|
|
2201
|
+
function cloneDate(value) {
|
|
2202
|
+
return new Date(value.getTime());
|
|
2203
|
+
}
|
|
2204
|
+
function compareEntries(left, right) {
|
|
2205
|
+
return left.path.localeCompare(right.path);
|
|
2206
|
+
}
|
|
2207
|
+
function mapLocalFileSystemError(error, remotePath) {
|
|
2208
|
+
const code = getErrorCode(error);
|
|
2209
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
2210
|
+
return createPathNotFoundError(remotePath, `Local provider path not found: ${remotePath}`);
|
|
2211
|
+
}
|
|
2212
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
2213
|
+
return new PermissionDeniedError({
|
|
2214
|
+
cause: error,
|
|
2215
|
+
details: { provider: LOCAL_PROVIDER_ID },
|
|
2216
|
+
message: `Local provider permission denied: ${remotePath}`,
|
|
2217
|
+
path: remotePath,
|
|
2218
|
+
retryable: false
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
return new ConfigurationError({
|
|
2222
|
+
cause: error,
|
|
2223
|
+
details: { code, provider: LOCAL_PROVIDER_ID },
|
|
2224
|
+
message: `Local provider filesystem operation failed: ${remotePath}`,
|
|
2225
|
+
path: remotePath,
|
|
2226
|
+
retryable: false
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
function createPathNotFoundError(path2, message) {
|
|
2230
|
+
return new PathNotFoundError({
|
|
2231
|
+
details: { provider: LOCAL_PROVIDER_ID },
|
|
2232
|
+
message,
|
|
2233
|
+
path: path2,
|
|
2234
|
+
retryable: false
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
function getErrorCode(error) {
|
|
2238
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/providers/memory/MemoryProvider.ts
|
|
2242
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
2243
|
+
var MEMORY_PROVIDER_ID = "memory";
|
|
2244
|
+
var MEMORY_PROVIDER_CAPABILITIES = {
|
|
2245
|
+
provider: MEMORY_PROVIDER_ID,
|
|
2246
|
+
authentication: ["anonymous"],
|
|
2247
|
+
list: true,
|
|
2248
|
+
stat: true,
|
|
2249
|
+
readStream: true,
|
|
2250
|
+
writeStream: true,
|
|
2251
|
+
serverSideCopy: false,
|
|
2252
|
+
serverSideMove: false,
|
|
2253
|
+
resumeDownload: true,
|
|
2254
|
+
resumeUpload: true,
|
|
2255
|
+
checksum: [],
|
|
2256
|
+
atomicRename: false,
|
|
2257
|
+
chmod: false,
|
|
2258
|
+
chown: false,
|
|
2259
|
+
symlink: true,
|
|
2260
|
+
metadata: [
|
|
2261
|
+
"accessedAt",
|
|
2262
|
+
"createdAt",
|
|
2263
|
+
"group",
|
|
2264
|
+
"modifiedAt",
|
|
2265
|
+
"owner",
|
|
2266
|
+
"permissions",
|
|
2267
|
+
"symlinkTarget",
|
|
2268
|
+
"uniqueId"
|
|
2269
|
+
],
|
|
2270
|
+
maxConcurrency: 32,
|
|
2271
|
+
notes: ["Deterministic in-memory provider for tests and provider contract validation"]
|
|
2272
|
+
};
|
|
2273
|
+
function createMemoryProviderFactory(options = {}) {
|
|
2274
|
+
const state = createMemoryState(options.entries ?? []);
|
|
2275
|
+
return {
|
|
2276
|
+
id: MEMORY_PROVIDER_ID,
|
|
2277
|
+
capabilities: MEMORY_PROVIDER_CAPABILITIES,
|
|
2278
|
+
create: () => new MemoryProvider(state)
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
var MemoryProvider = class {
|
|
2282
|
+
constructor(state) {
|
|
2283
|
+
this.state = state;
|
|
2284
|
+
}
|
|
2285
|
+
state;
|
|
2286
|
+
id = MEMORY_PROVIDER_ID;
|
|
2287
|
+
capabilities = MEMORY_PROVIDER_CAPABILITIES;
|
|
2288
|
+
connect() {
|
|
2289
|
+
return Promise.resolve(new MemoryTransferSession(this.state));
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
var MemoryTransferSession = class {
|
|
2293
|
+
provider = MEMORY_PROVIDER_ID;
|
|
2294
|
+
capabilities = MEMORY_PROVIDER_CAPABILITIES;
|
|
2295
|
+
fs;
|
|
2296
|
+
transfers;
|
|
2297
|
+
constructor(state) {
|
|
2298
|
+
this.fs = new MemoryFileSystem(state);
|
|
2299
|
+
this.transfers = new MemoryTransferOperations(state);
|
|
2300
|
+
}
|
|
2301
|
+
disconnect() {
|
|
2302
|
+
return Promise.resolve();
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
var MemoryFileSystem = class {
|
|
2306
|
+
constructor(state) {
|
|
2307
|
+
this.state = state;
|
|
2308
|
+
}
|
|
2309
|
+
state;
|
|
2310
|
+
list(path2) {
|
|
2311
|
+
return Promise.resolve().then(() => {
|
|
2312
|
+
const normalizedPath = normalizeMemoryPath(path2);
|
|
2313
|
+
const directory = this.requireEntry(normalizedPath);
|
|
2314
|
+
if (directory.type !== "directory") {
|
|
2315
|
+
throw createPathNotFoundError2(
|
|
2316
|
+
normalizedPath,
|
|
2317
|
+
`Memory path is not a directory: ${normalizedPath}`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
return [...this.state.entries.values()].filter(
|
|
2321
|
+
(entry) => entry.path !== normalizedPath && getParentPath(entry.path) === normalizedPath
|
|
2322
|
+
).map(cloneRemoteEntry).sort(compareEntries2);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
stat(path2) {
|
|
2326
|
+
return Promise.resolve().then(
|
|
2327
|
+
() => cloneRemoteStat(this.requireEntry(normalizeMemoryPath(path2)))
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
remove(path2, options = {}) {
|
|
2331
|
+
return Promise.resolve().then(() => {
|
|
2332
|
+
const normalized = normalizeMemoryPath(path2);
|
|
2333
|
+
const entry = this.state.entries.get(normalized);
|
|
2334
|
+
if (entry === void 0) {
|
|
2335
|
+
if (options.ignoreMissing) return;
|
|
2336
|
+
throw createPathNotFoundError2(normalized, `Memory path not found: ${normalized}`);
|
|
2337
|
+
}
|
|
2338
|
+
if (entry.type === "directory") {
|
|
2339
|
+
throw createPathNotFoundError2(
|
|
2340
|
+
normalized,
|
|
2341
|
+
`Memory path is a directory; use rmdir: ${normalized}`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
this.state.entries.delete(normalized);
|
|
2345
|
+
this.state.content.delete(normalized);
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
rename(from, to) {
|
|
2349
|
+
return Promise.resolve().then(() => {
|
|
2350
|
+
const fromPath = normalizeMemoryPath(from);
|
|
2351
|
+
const toPath = normalizeMemoryPath(to);
|
|
2352
|
+
const entry = this.state.entries.get(fromPath);
|
|
2353
|
+
if (entry === void 0) {
|
|
2354
|
+
throw createPathNotFoundError2(fromPath, `Memory path not found: ${fromPath}`);
|
|
2355
|
+
}
|
|
2356
|
+
ensureParentDirectories(this.state.entries, toPath);
|
|
2357
|
+
const moved = { ...entry, path: toPath, name: basenameRemotePath(toPath) };
|
|
2358
|
+
this.state.entries.delete(fromPath);
|
|
2359
|
+
this.state.entries.set(toPath, moved);
|
|
2360
|
+
const content = this.state.content.get(fromPath);
|
|
2361
|
+
if (content !== void 0) {
|
|
2362
|
+
this.state.content.delete(fromPath);
|
|
2363
|
+
this.state.content.set(toPath, content);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
mkdir(path2, options = {}) {
|
|
2368
|
+
return Promise.resolve().then(() => {
|
|
2369
|
+
const normalized = normalizeMemoryPath(path2);
|
|
2370
|
+
const existing = this.state.entries.get(normalized);
|
|
2371
|
+
if (existing !== void 0) {
|
|
2372
|
+
if (existing.type === "directory" && options.recursive) return;
|
|
2373
|
+
throw createInvalidFixtureError(normalized, `Memory path already exists: ${normalized}`);
|
|
2374
|
+
}
|
|
2375
|
+
if (options.recursive) {
|
|
2376
|
+
ensureParentDirectories(this.state.entries, normalized);
|
|
2377
|
+
} else {
|
|
2378
|
+
const parent = getParentPath(normalized);
|
|
2379
|
+
if (parent !== void 0 && !this.state.entries.has(parent)) {
|
|
2380
|
+
throw createPathNotFoundError2(parent, `Memory parent not found: ${parent}`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
this.state.entries.set(normalized, createDirectoryEntry(normalized));
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
rmdir(path2, options = {}) {
|
|
2387
|
+
return Promise.resolve().then(() => {
|
|
2388
|
+
const normalized = normalizeMemoryPath(path2);
|
|
2389
|
+
const entry = this.state.entries.get(normalized);
|
|
2390
|
+
if (entry === void 0) {
|
|
2391
|
+
if (options.ignoreMissing) return;
|
|
2392
|
+
throw createPathNotFoundError2(normalized, `Memory path not found: ${normalized}`);
|
|
2393
|
+
}
|
|
2394
|
+
if (entry.type !== "directory") {
|
|
2395
|
+
throw createPathNotFoundError2(normalized, `Memory path is not a directory: ${normalized}`);
|
|
2396
|
+
}
|
|
2397
|
+
const children = [...this.state.entries.values()].filter(
|
|
2398
|
+
(child) => child.path !== normalized && getParentPath(child.path) === normalized
|
|
2399
|
+
);
|
|
2400
|
+
if (children.length > 0 && !options.recursive) {
|
|
2401
|
+
throw createInvalidFixtureError(normalized, `Memory directory not empty: ${normalized}`);
|
|
2402
|
+
}
|
|
2403
|
+
const stack = [...children];
|
|
2404
|
+
while (stack.length > 0) {
|
|
2405
|
+
const next = stack.pop();
|
|
2406
|
+
if (!next) continue;
|
|
2407
|
+
if (next.type === "directory") {
|
|
2408
|
+
for (const grand of this.state.entries.values()) {
|
|
2409
|
+
if (grand.path !== next.path && getParentPath(grand.path) === next.path) {
|
|
2410
|
+
stack.push(grand);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
this.state.entries.delete(next.path);
|
|
2415
|
+
this.state.content.delete(next.path);
|
|
2416
|
+
}
|
|
2417
|
+
this.state.entries.delete(normalized);
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
requireEntry(path2) {
|
|
2421
|
+
const entry = this.state.entries.get(path2);
|
|
2422
|
+
if (entry === void 0) {
|
|
2423
|
+
throw createPathNotFoundError2(path2, `Memory path not found: ${path2}`);
|
|
2424
|
+
}
|
|
2425
|
+
return entry;
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
var MemoryTransferOperations = class {
|
|
2429
|
+
constructor(state) {
|
|
2430
|
+
this.state = state;
|
|
2431
|
+
}
|
|
2432
|
+
state;
|
|
2433
|
+
read(request) {
|
|
2434
|
+
return Promise.resolve().then(() => {
|
|
2435
|
+
request.throwIfAborted();
|
|
2436
|
+
const path2 = normalizeMemoryPath(request.endpoint.path);
|
|
2437
|
+
const entry = requireFileEntry(this.state, path2);
|
|
2438
|
+
const content = this.state.content.get(path2) ?? new Uint8Array(entry.size ?? 0);
|
|
2439
|
+
const range = resolveByteRange(content.byteLength, request.range);
|
|
2440
|
+
const chunk = content.slice(range.offset, range.offset + range.length);
|
|
2441
|
+
const result = {
|
|
2442
|
+
content: createMemoryContentSource(chunk),
|
|
2443
|
+
totalBytes: chunk.byteLength
|
|
2444
|
+
};
|
|
2445
|
+
if (range.offset > 0) {
|
|
2446
|
+
result.bytesRead = range.offset;
|
|
2447
|
+
}
|
|
2448
|
+
return result;
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
async write(request) {
|
|
2452
|
+
request.throwIfAborted();
|
|
2453
|
+
const path2 = normalizeMemoryPath(request.endpoint.path);
|
|
2454
|
+
const existing = this.state.entries.get(path2);
|
|
2455
|
+
if (existing?.type === "directory") {
|
|
2456
|
+
throw createInvalidFixtureError(path2, `Memory path is a directory: ${path2}`);
|
|
2457
|
+
}
|
|
2458
|
+
const writtenContent = await collectTransferContent2(request);
|
|
2459
|
+
const offset = normalizeOptionalByteCount2(request.offset, "offset");
|
|
2460
|
+
const previousContent = this.state.content.get(path2) ?? new Uint8Array(0);
|
|
2461
|
+
const content = offset === void 0 ? writtenContent : mergeContentAtOffset(previousContent, writtenContent, offset);
|
|
2462
|
+
ensureParentDirectories(this.state.entries, path2);
|
|
2463
|
+
this.state.entries.set(path2, createWrittenFileEntry(path2, content.byteLength));
|
|
2464
|
+
this.state.content.set(path2, content);
|
|
2465
|
+
const result = {
|
|
2466
|
+
bytesTransferred: writtenContent.byteLength,
|
|
2467
|
+
resumed: offset !== void 0 && offset > 0,
|
|
2468
|
+
totalBytes: content.byteLength,
|
|
2469
|
+
verified: request.verification?.verified ?? false
|
|
2470
|
+
};
|
|
2471
|
+
if (request.verification !== void 0) {
|
|
2472
|
+
result.verification = cloneVerification3(request.verification);
|
|
2473
|
+
}
|
|
2474
|
+
return result;
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
function createMemoryState(entries) {
|
|
2478
|
+
const state = {
|
|
2479
|
+
content: /* @__PURE__ */ new Map(),
|
|
2480
|
+
entries: /* @__PURE__ */ new Map([["/", createDirectoryEntry("/")]])
|
|
2481
|
+
};
|
|
2482
|
+
for (const input of entries) {
|
|
2483
|
+
const entry = createMemoryEntry(input);
|
|
2484
|
+
const content = createMemoryContent(input, entry);
|
|
2485
|
+
if (entry.path === "/" && entry.type !== "directory") {
|
|
2486
|
+
throw createInvalidFixtureError(entry.path, "Memory provider root must be a directory");
|
|
2487
|
+
}
|
|
2488
|
+
ensureParentDirectories(state.entries, entry.path);
|
|
2489
|
+
state.entries.set(entry.path, entry);
|
|
2490
|
+
if (content !== void 0) {
|
|
2491
|
+
state.content.set(entry.path, content);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return state;
|
|
2495
|
+
}
|
|
2496
|
+
function createMemoryEntry(input) {
|
|
2497
|
+
const path2 = normalizeMemoryPath(input.path);
|
|
2498
|
+
const entry = {
|
|
2499
|
+
name: input.name ?? basenameRemotePath(path2),
|
|
2500
|
+
path: path2,
|
|
2501
|
+
type: input.type
|
|
2502
|
+
};
|
|
2503
|
+
copyOptionalEntryFields(entry, input);
|
|
2504
|
+
const content = normalizeMemoryContent(input.content);
|
|
2505
|
+
if (content !== void 0) {
|
|
2506
|
+
entry.size = content.byteLength;
|
|
2507
|
+
}
|
|
2508
|
+
return {
|
|
2509
|
+
...entry,
|
|
2510
|
+
exists: true
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
function createMemoryContent(input, entry) {
|
|
2514
|
+
const content = normalizeMemoryContent(input.content);
|
|
2515
|
+
if (content !== void 0) {
|
|
2516
|
+
if (entry.type !== "file") {
|
|
2517
|
+
throw createInvalidFixtureError(
|
|
2518
|
+
entry.path,
|
|
2519
|
+
`Memory fixture content requires a file: ${entry.path}`
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
return content;
|
|
2523
|
+
}
|
|
2524
|
+
if (entry.type === "file") {
|
|
2525
|
+
return new Uint8Array(entry.size ?? 0);
|
|
2526
|
+
}
|
|
2527
|
+
return void 0;
|
|
2528
|
+
}
|
|
2529
|
+
function normalizeMemoryContent(content) {
|
|
2530
|
+
if (content === void 0) {
|
|
2531
|
+
return void 0;
|
|
2532
|
+
}
|
|
2533
|
+
return typeof content === "string" ? Buffer3.from(content) : new Uint8Array(content);
|
|
2534
|
+
}
|
|
2535
|
+
function createWrittenFileEntry(path2, size) {
|
|
2536
|
+
return {
|
|
2537
|
+
exists: true,
|
|
2538
|
+
modifiedAt: /* @__PURE__ */ new Date(),
|
|
2539
|
+
name: basenameRemotePath(path2),
|
|
2540
|
+
path: path2,
|
|
2541
|
+
size,
|
|
2542
|
+
type: "file"
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
function createDirectoryEntry(path2) {
|
|
2546
|
+
return {
|
|
2547
|
+
exists: true,
|
|
2548
|
+
name: basenameRemotePath(path2),
|
|
2549
|
+
path: path2,
|
|
2550
|
+
type: "directory"
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function ensureParentDirectories(state, path2) {
|
|
2554
|
+
for (const parentPath of getAncestorPaths(path2)) {
|
|
2555
|
+
const parent = state.get(parentPath);
|
|
2556
|
+
if (parent !== void 0 && parent.type !== "directory") {
|
|
2557
|
+
throw createInvalidFixtureError(
|
|
2558
|
+
parentPath,
|
|
2559
|
+
`Memory fixture parent is not a directory: ${parentPath}`
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
if (parent === void 0) {
|
|
2563
|
+
state.set(parentPath, createDirectoryEntry(parentPath));
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function normalizeMemoryPath(path2) {
|
|
2568
|
+
const normalized = normalizeRemotePath(path2);
|
|
2569
|
+
if (normalized === "." || normalized === "/") {
|
|
2570
|
+
return "/";
|
|
2571
|
+
}
|
|
2572
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2573
|
+
}
|
|
2574
|
+
function getAncestorPaths(path2) {
|
|
2575
|
+
const ancestors = [];
|
|
2576
|
+
let parentPath = getParentPath(path2);
|
|
2577
|
+
while (parentPath !== void 0 && parentPath !== "/") {
|
|
2578
|
+
ancestors.unshift(parentPath);
|
|
2579
|
+
parentPath = getParentPath(parentPath);
|
|
2580
|
+
}
|
|
2581
|
+
return ancestors;
|
|
2582
|
+
}
|
|
2583
|
+
function getParentPath(path2) {
|
|
2584
|
+
if (path2 === "/") {
|
|
2585
|
+
return void 0;
|
|
2586
|
+
}
|
|
2587
|
+
const parentEnd = path2.lastIndexOf("/");
|
|
2588
|
+
return parentEnd <= 0 ? "/" : path2.slice(0, parentEnd);
|
|
2589
|
+
}
|
|
2590
|
+
function requireFileEntry(state, path2) {
|
|
2591
|
+
const entry = state.entries.get(path2);
|
|
2592
|
+
if (entry === void 0) {
|
|
2593
|
+
throw createPathNotFoundError2(path2, `Memory path not found: ${path2}`);
|
|
2594
|
+
}
|
|
2595
|
+
if (entry.type !== "file") {
|
|
2596
|
+
throw createPathNotFoundError2(path2, `Memory path is not a file: ${path2}`);
|
|
2597
|
+
}
|
|
2598
|
+
return entry;
|
|
2599
|
+
}
|
|
2600
|
+
function resolveByteRange(size, range) {
|
|
2601
|
+
if (range === void 0) {
|
|
2602
|
+
return { length: size, offset: 0 };
|
|
2603
|
+
}
|
|
2604
|
+
const requestedOffset = normalizeByteCount2(range.offset, "offset");
|
|
2605
|
+
const requestedLength = range.length === void 0 ? size - Math.min(requestedOffset, size) : normalizeByteCount2(range.length, "length");
|
|
2606
|
+
const offset = Math.min(requestedOffset, size);
|
|
2607
|
+
const length = Math.max(0, Math.min(requestedLength, size - offset));
|
|
2608
|
+
return { length, offset };
|
|
2609
|
+
}
|
|
2610
|
+
async function collectTransferContent2(request) {
|
|
2611
|
+
const chunks = [];
|
|
2612
|
+
let byteLength = 0;
|
|
2613
|
+
for await (const chunk of request.content) {
|
|
2614
|
+
request.throwIfAborted();
|
|
2615
|
+
const clonedChunk = new Uint8Array(chunk);
|
|
2616
|
+
chunks.push(clonedChunk);
|
|
2617
|
+
byteLength += clonedChunk.byteLength;
|
|
2618
|
+
request.reportProgress(byteLength, request.totalBytes);
|
|
2619
|
+
}
|
|
2620
|
+
return concatChunks2(chunks, byteLength);
|
|
2621
|
+
}
|
|
2622
|
+
function concatChunks2(chunks, byteLength) {
|
|
2623
|
+
const content = new Uint8Array(byteLength);
|
|
2624
|
+
let offset = 0;
|
|
2625
|
+
for (const chunk of chunks) {
|
|
2626
|
+
content.set(chunk, offset);
|
|
2627
|
+
offset += chunk.byteLength;
|
|
2628
|
+
}
|
|
2629
|
+
return content;
|
|
2630
|
+
}
|
|
2631
|
+
function mergeContentAtOffset(previousContent, writtenContent, offset) {
|
|
2632
|
+
const content = new Uint8Array(
|
|
2633
|
+
Math.max(previousContent.byteLength, offset + writtenContent.byteLength)
|
|
2634
|
+
);
|
|
2635
|
+
content.set(previousContent);
|
|
2636
|
+
content.set(writtenContent, offset);
|
|
2637
|
+
return content;
|
|
2638
|
+
}
|
|
2639
|
+
async function* createMemoryContentSource(content) {
|
|
2640
|
+
await Promise.resolve();
|
|
2641
|
+
yield new Uint8Array(content);
|
|
2642
|
+
}
|
|
2643
|
+
function normalizeOptionalByteCount2(value, field) {
|
|
2644
|
+
return value === void 0 ? void 0 : normalizeByteCount2(value, field);
|
|
2645
|
+
}
|
|
2646
|
+
function normalizeByteCount2(value, field) {
|
|
2647
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2648
|
+
throw createInvalidFixtureError("/", `Memory provider ${field} must be a non-negative number`);
|
|
2649
|
+
}
|
|
2650
|
+
return Math.floor(value);
|
|
2651
|
+
}
|
|
2652
|
+
function cloneVerification3(verification) {
|
|
2653
|
+
const clone = { verified: verification.verified };
|
|
2654
|
+
if (verification.method !== void 0) clone.method = verification.method;
|
|
2655
|
+
if (verification.checksum !== void 0) clone.checksum = verification.checksum;
|
|
2656
|
+
if (verification.expectedChecksum !== void 0) {
|
|
2657
|
+
clone.expectedChecksum = verification.expectedChecksum;
|
|
2658
|
+
}
|
|
2659
|
+
if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
|
|
2660
|
+
if (verification.details !== void 0) clone.details = { ...verification.details };
|
|
2661
|
+
return clone;
|
|
2662
|
+
}
|
|
2663
|
+
function cloneRemoteEntry(entry) {
|
|
2664
|
+
const clone = {
|
|
2665
|
+
name: entry.name,
|
|
2666
|
+
path: entry.path,
|
|
2667
|
+
type: entry.type
|
|
2668
|
+
};
|
|
2669
|
+
copyOptionalEntryFields(clone, entry);
|
|
2670
|
+
return clone;
|
|
2671
|
+
}
|
|
2672
|
+
function cloneRemoteStat(entry) {
|
|
2673
|
+
return {
|
|
2674
|
+
...cloneRemoteEntry(entry),
|
|
2675
|
+
exists: true
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
function copyOptionalEntryFields(target, source) {
|
|
2679
|
+
if (source.size !== void 0) target.size = source.size;
|
|
2680
|
+
if (source.modifiedAt !== void 0) target.modifiedAt = cloneDate2(source.modifiedAt);
|
|
2681
|
+
if (source.createdAt !== void 0) target.createdAt = cloneDate2(source.createdAt);
|
|
2682
|
+
if (source.accessedAt !== void 0) target.accessedAt = cloneDate2(source.accessedAt);
|
|
2683
|
+
if (source.permissions !== void 0) target.permissions = clonePermissions(source.permissions);
|
|
2684
|
+
if (source.owner !== void 0) target.owner = source.owner;
|
|
2685
|
+
if (source.group !== void 0) target.group = source.group;
|
|
2686
|
+
if (source.symlinkTarget !== void 0) target.symlinkTarget = source.symlinkTarget;
|
|
2687
|
+
if (source.uniqueId !== void 0) target.uniqueId = source.uniqueId;
|
|
2688
|
+
if (source.raw !== void 0) target.raw = source.raw;
|
|
2689
|
+
}
|
|
2690
|
+
function cloneDate2(value) {
|
|
2691
|
+
return new Date(value.getTime());
|
|
2692
|
+
}
|
|
2693
|
+
function clonePermissions(permissions) {
|
|
2694
|
+
return { ...permissions };
|
|
2695
|
+
}
|
|
2696
|
+
function compareEntries2(left, right) {
|
|
2697
|
+
return left.path.localeCompare(right.path);
|
|
2698
|
+
}
|
|
2699
|
+
function createPathNotFoundError2(path2, message) {
|
|
2700
|
+
return new PathNotFoundError({
|
|
2701
|
+
details: { provider: MEMORY_PROVIDER_ID },
|
|
2702
|
+
message,
|
|
2703
|
+
path: path2,
|
|
2704
|
+
retryable: false
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
function createInvalidFixtureError(path2, message) {
|
|
2708
|
+
return new ConfigurationError({
|
|
2709
|
+
details: { provider: MEMORY_PROVIDER_ID },
|
|
2710
|
+
message,
|
|
2711
|
+
path: path2,
|
|
2712
|
+
retryable: false
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/profiles/resolveConnectionProfileSecrets.ts
|
|
2717
|
+
async function resolveConnectionProfileSecrets(profile, options = {}) {
|
|
2718
|
+
const { password, ssh, tls, username, ...rest } = profile;
|
|
2719
|
+
const resolved = { ...rest };
|
|
2720
|
+
if (username !== void 0) {
|
|
2721
|
+
resolved.username = await resolveSecret(username, options);
|
|
2722
|
+
}
|
|
2723
|
+
if (password !== void 0) {
|
|
2724
|
+
resolved.password = await resolveSecret(password, options);
|
|
2725
|
+
}
|
|
2726
|
+
if (tls !== void 0) {
|
|
2727
|
+
resolved.tls = await resolveTlsProfile(tls, options);
|
|
2728
|
+
}
|
|
2729
|
+
if (ssh !== void 0) {
|
|
2730
|
+
resolved.ssh = await resolveSshProfile(ssh, options);
|
|
2731
|
+
}
|
|
2732
|
+
return resolved;
|
|
2733
|
+
}
|
|
2734
|
+
async function resolveSshProfile(profile, options) {
|
|
2735
|
+
const { knownHosts, passphrase, privateKey, ...rest } = profile;
|
|
2736
|
+
const resolved = { ...rest };
|
|
2737
|
+
if (privateKey !== void 0) resolved.privateKey = await resolveSecret(privateKey, options);
|
|
2738
|
+
if (passphrase !== void 0) resolved.passphrase = await resolveSecret(passphrase, options);
|
|
2739
|
+
if (knownHosts !== void 0)
|
|
2740
|
+
resolved.knownHosts = await resolveKnownHostsSource(knownHosts, options);
|
|
2741
|
+
return resolved;
|
|
2742
|
+
}
|
|
2743
|
+
async function resolveKnownHostsSource(source, options) {
|
|
2744
|
+
if (Array.isArray(source)) {
|
|
2745
|
+
return Promise.all(source.map((item) => resolveSecret(item, options)));
|
|
2746
|
+
}
|
|
2747
|
+
return resolveSecret(source, options);
|
|
2748
|
+
}
|
|
2749
|
+
async function resolveTlsProfile(profile, options) {
|
|
2750
|
+
const { ca, cert, key, passphrase, pfx, ...rest } = profile;
|
|
2751
|
+
const resolved = { ...rest };
|
|
2752
|
+
if (ca !== void 0) resolved.ca = await resolveTlsSecretSource(ca, options);
|
|
2753
|
+
if (cert !== void 0) resolved.cert = await resolveSecret(cert, options);
|
|
2754
|
+
if (key !== void 0) resolved.key = await resolveSecret(key, options);
|
|
2755
|
+
if (passphrase !== void 0) resolved.passphrase = await resolveSecret(passphrase, options);
|
|
2756
|
+
if (pfx !== void 0) resolved.pfx = await resolveSecret(pfx, options);
|
|
2757
|
+
return resolved;
|
|
2758
|
+
}
|
|
2759
|
+
async function resolveTlsSecretSource(source, options) {
|
|
2760
|
+
if (Array.isArray(source)) {
|
|
2761
|
+
return Promise.all(source.map((item) => resolveSecret(item, options)));
|
|
2762
|
+
}
|
|
2763
|
+
return resolveSecret(source, options);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// src/profiles/OAuthTokenSource.ts
|
|
2767
|
+
function createOAuthTokenSecretSource(refresh, options = {}) {
|
|
2768
|
+
if (typeof refresh !== "function") {
|
|
2769
|
+
throw new ConfigurationError({
|
|
2770
|
+
message: "createOAuthTokenSecretSource requires a refresh callback",
|
|
2771
|
+
retryable: false
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
const skewMs = options.skewMs ?? 6e4;
|
|
2775
|
+
const now = options.now ?? (() => Date.now());
|
|
2776
|
+
if (skewMs < 0) {
|
|
2777
|
+
throw new ConfigurationError({
|
|
2778
|
+
message: "OAuthTokenSecretSourceOptions.skewMs must be non-negative",
|
|
2779
|
+
retryable: false
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
let cache;
|
|
2783
|
+
let pending;
|
|
2784
|
+
const renew = async () => {
|
|
2785
|
+
const result = await refresh();
|
|
2786
|
+
if (typeof result.accessToken !== "string" || result.accessToken === "") {
|
|
2787
|
+
throw new ConfigurationError({
|
|
2788
|
+
message: "OAuth refresh callback returned an empty access token",
|
|
2789
|
+
retryable: false
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
let expiresAtMs;
|
|
2793
|
+
if (result.expiresAt !== void 0) {
|
|
2794
|
+
const ts = result.expiresAt.getTime();
|
|
2795
|
+
if (Number.isFinite(ts)) expiresAtMs = ts;
|
|
2796
|
+
} else if (typeof result.expiresInSeconds === "number") {
|
|
2797
|
+
if (!Number.isFinite(result.expiresInSeconds) || result.expiresInSeconds <= 0) {
|
|
2798
|
+
throw new ConfigurationError({
|
|
2799
|
+
message: "OAuth refresh callback returned a non-positive expiresInSeconds",
|
|
2800
|
+
retryable: false
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
expiresAtMs = now() + result.expiresInSeconds * 1e3;
|
|
2804
|
+
}
|
|
2805
|
+
const cached = { accessToken: result.accessToken, expiresAtMs };
|
|
2806
|
+
cache = cached;
|
|
2807
|
+
return cached;
|
|
2808
|
+
};
|
|
2809
|
+
return async () => {
|
|
2810
|
+
const current = cache;
|
|
2811
|
+
if (current !== void 0 && isFresh(current, skewMs, now)) {
|
|
2812
|
+
return current.accessToken;
|
|
2813
|
+
}
|
|
2814
|
+
if (pending === void 0) {
|
|
2815
|
+
pending = renew().finally(() => {
|
|
2816
|
+
pending = void 0;
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
const refreshed = await pending;
|
|
2820
|
+
return refreshed.accessToken;
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function isFresh(token, skewMs, now) {
|
|
2824
|
+
if (token.expiresAtMs === void 0) return true;
|
|
2825
|
+
return token.expiresAtMs - skewMs > now();
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// src/profiles/importers/KnownHostsParser.ts
|
|
2829
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
2830
|
+
import { createHmac } from "crypto";
|
|
2831
|
+
function parseKnownHosts(text) {
|
|
2832
|
+
const entries = [];
|
|
2833
|
+
const lines = text.split(/\r?\n/);
|
|
2834
|
+
for (const line of lines) {
|
|
2835
|
+
const trimmed = line.trim();
|
|
2836
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2837
|
+
const entry = parseKnownHostsLine(line);
|
|
2838
|
+
if (entry !== void 0) entries.push(entry);
|
|
2839
|
+
}
|
|
2840
|
+
return entries;
|
|
2841
|
+
}
|
|
2842
|
+
function parseKnownHostsLine(line) {
|
|
2843
|
+
const tokens = line.trim().split(/\s+/);
|
|
2844
|
+
if (tokens.length < 3) return void 0;
|
|
2845
|
+
let index = 0;
|
|
2846
|
+
let marker;
|
|
2847
|
+
const first2 = tokens[index];
|
|
2848
|
+
if (first2 === "@cert-authority" || first2 === "@revoked") {
|
|
2849
|
+
marker = first2 === "@cert-authority" ? "cert-authority" : "revoked";
|
|
2850
|
+
index += 1;
|
|
2851
|
+
}
|
|
2852
|
+
const hostField = tokens[index];
|
|
2853
|
+
const keyType = tokens[index + 1];
|
|
2854
|
+
const keyBase64 = tokens[index + 2];
|
|
2855
|
+
if (hostField === void 0 || keyType === void 0 || keyBase64 === void 0) return void 0;
|
|
2856
|
+
const commentTokens = tokens.slice(index + 3);
|
|
2857
|
+
const comment = commentTokens.length > 0 ? commentTokens.join(" ") : void 0;
|
|
2858
|
+
let hostPatterns = [];
|
|
2859
|
+
let hashedSalt;
|
|
2860
|
+
let hashedHash;
|
|
2861
|
+
if (hostField.startsWith("|1|")) {
|
|
2862
|
+
const parts = hostField.split("|");
|
|
2863
|
+
if (parts.length < 4) return void 0;
|
|
2864
|
+
hashedSalt = parts[2];
|
|
2865
|
+
hashedHash = parts[3];
|
|
2866
|
+
} else {
|
|
2867
|
+
hostPatterns = hostField.split(",").filter((token) => token !== "");
|
|
2868
|
+
}
|
|
2869
|
+
const entry = {
|
|
2870
|
+
hostPatterns,
|
|
2871
|
+
keyBase64,
|
|
2872
|
+
keyType,
|
|
2873
|
+
raw: line
|
|
2874
|
+
};
|
|
2875
|
+
if (marker !== void 0) entry.marker = marker;
|
|
2876
|
+
if (comment !== void 0) entry.comment = comment;
|
|
2877
|
+
if (hashedSalt !== void 0) entry.hashedSalt = hashedSalt;
|
|
2878
|
+
if (hashedHash !== void 0) entry.hashedHash = hashedHash;
|
|
2879
|
+
return entry;
|
|
2880
|
+
}
|
|
2881
|
+
var DEFAULT_SSH_PORT = 22;
|
|
2882
|
+
function matchKnownHostsEntry(entry, host, port = DEFAULT_SSH_PORT) {
|
|
2883
|
+
if (entry.hashedSalt !== void 0 && entry.hashedHash !== void 0) {
|
|
2884
|
+
return matchesHashedEntry(entry.hashedSalt, entry.hashedHash, host, port);
|
|
2885
|
+
}
|
|
2886
|
+
let matched = false;
|
|
2887
|
+
for (const pattern of entry.hostPatterns) {
|
|
2888
|
+
if (pattern.startsWith("!")) {
|
|
2889
|
+
const negated = pattern.slice(1);
|
|
2890
|
+
if (matchesPlainPattern(negated, host, port)) return false;
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
if (matchesPlainPattern(pattern, host, port)) matched = true;
|
|
2894
|
+
}
|
|
2895
|
+
return matched;
|
|
2896
|
+
}
|
|
2897
|
+
function matchKnownHosts(entries, host, port = DEFAULT_SSH_PORT) {
|
|
2898
|
+
return entries.filter((entry) => matchKnownHostsEntry(entry, host, port));
|
|
2899
|
+
}
|
|
2900
|
+
function matchesPlainPattern(pattern, host, port) {
|
|
2901
|
+
const portMatch = pattern.match(/^\[(.+)\]:(\d+)$/);
|
|
2902
|
+
if (portMatch) {
|
|
2903
|
+
const [, hostPattern, portText] = portMatch;
|
|
2904
|
+
if (hostPattern === void 0 || portText === void 0) return false;
|
|
2905
|
+
const expectedPort = Number.parseInt(portText, 10);
|
|
2906
|
+
if (Number.isNaN(expectedPort) || expectedPort !== port) return false;
|
|
2907
|
+
return globMatch(hostPattern, host);
|
|
2908
|
+
}
|
|
2909
|
+
return port === DEFAULT_SSH_PORT && globMatch(pattern, host);
|
|
2910
|
+
}
|
|
2911
|
+
function globMatch(pattern, value) {
|
|
2912
|
+
const regex = new RegExp(
|
|
2913
|
+
`^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
|
|
2914
|
+
"i"
|
|
2915
|
+
);
|
|
2916
|
+
return regex.test(value);
|
|
2917
|
+
}
|
|
2918
|
+
function matchesHashedEntry(salt, hash, host, port) {
|
|
2919
|
+
const saltBuffer = Buffer4.from(salt, "base64");
|
|
2920
|
+
if (saltBuffer.length === 0) return false;
|
|
2921
|
+
const candidates = port === DEFAULT_SSH_PORT ? [host] : [`[${host}]:${String(port)}`, host];
|
|
2922
|
+
for (const candidate of candidates) {
|
|
2923
|
+
const expected = createHmac("sha1", saltBuffer).update(candidate).digest("base64");
|
|
2924
|
+
if (expected === hash) return true;
|
|
2925
|
+
}
|
|
2926
|
+
return false;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// src/profiles/importers/OpenSshConfigImporter.ts
|
|
2930
|
+
function parseOpenSshConfig(text) {
|
|
2931
|
+
const entries = [];
|
|
2932
|
+
let current;
|
|
2933
|
+
let skipping = false;
|
|
2934
|
+
const lines = text.split(/\r?\n/);
|
|
2935
|
+
for (const rawLine of lines) {
|
|
2936
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
2937
|
+
if (line === "") continue;
|
|
2938
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=?\s*(.*)$/);
|
|
2939
|
+
if (!match) continue;
|
|
2940
|
+
const [, keywordRaw, valueRaw] = match;
|
|
2941
|
+
if (keywordRaw === void 0 || valueRaw === void 0) continue;
|
|
2942
|
+
const keyword = keywordRaw.toLowerCase();
|
|
2943
|
+
const value = valueRaw.trim();
|
|
2944
|
+
if (keyword === "host") {
|
|
2945
|
+
if (current !== void 0) entries.push(current);
|
|
2946
|
+
current = { options: {}, patterns: tokenizeValues(value) };
|
|
2947
|
+
skipping = false;
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
if (keyword === "match") {
|
|
2951
|
+
if (current !== void 0) entries.push(current);
|
|
2952
|
+
current = void 0;
|
|
2953
|
+
skipping = true;
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
if (skipping || current === void 0) continue;
|
|
2957
|
+
const values = tokenizeValues(value);
|
|
2958
|
+
const existing = current.options[keyword];
|
|
2959
|
+
if (existing === void 0) {
|
|
2960
|
+
current.options[keyword] = [...values];
|
|
2961
|
+
} else {
|
|
2962
|
+
existing.push(...values);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (current !== void 0) entries.push(current);
|
|
2966
|
+
return entries;
|
|
2967
|
+
}
|
|
2968
|
+
function tokenizeValues(value) {
|
|
2969
|
+
if (value === "") return [];
|
|
2970
|
+
const tokens = [];
|
|
2971
|
+
const regex = /"([^"]*)"|(\S+)/g;
|
|
2972
|
+
let match;
|
|
2973
|
+
while ((match = regex.exec(value)) !== null) {
|
|
2974
|
+
tokens.push(match[1] ?? match[2] ?? "");
|
|
2975
|
+
}
|
|
2976
|
+
return tokens;
|
|
2977
|
+
}
|
|
2978
|
+
function resolveOpenSshHost(entries, alias) {
|
|
2979
|
+
const merged = {};
|
|
2980
|
+
const matched = [];
|
|
2981
|
+
for (const entry of entries) {
|
|
2982
|
+
if (!entryMatchesAlias(entry, alias)) continue;
|
|
2983
|
+
matched.push(entry);
|
|
2984
|
+
for (const [key, values] of Object.entries(entry.options)) {
|
|
2985
|
+
if (merged[key] === void 0) merged[key] = [...values];
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return { alias, matched, options: merged };
|
|
2989
|
+
}
|
|
2990
|
+
function entryMatchesAlias(entry, alias) {
|
|
2991
|
+
let matched = false;
|
|
2992
|
+
for (const pattern of entry.patterns) {
|
|
2993
|
+
if (pattern.startsWith("!")) {
|
|
2994
|
+
if (globMatch2(pattern.slice(1), alias)) return false;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
if (globMatch2(pattern, alias)) matched = true;
|
|
2998
|
+
}
|
|
2999
|
+
return matched;
|
|
3000
|
+
}
|
|
3001
|
+
function globMatch2(pattern, value) {
|
|
3002
|
+
const regex = new RegExp(
|
|
3003
|
+
`^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
|
|
3004
|
+
"i"
|
|
3005
|
+
);
|
|
3006
|
+
return regex.test(value);
|
|
3007
|
+
}
|
|
3008
|
+
function importOpenSshConfig(options) {
|
|
3009
|
+
const { alias } = options;
|
|
3010
|
+
const entries = options.entries ?? (options.text !== void 0 ? parseOpenSshConfig(options.text) : void 0);
|
|
3011
|
+
if (entries === void 0) {
|
|
3012
|
+
throw new ConfigurationError({
|
|
3013
|
+
code: "openssh_config_input_missing",
|
|
3014
|
+
message: "importOpenSshConfig requires either text or pre-parsed entries.",
|
|
3015
|
+
retryable: false
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
const resolved = resolveOpenSshHost(entries, alias);
|
|
3019
|
+
const optionsMap = resolved.options;
|
|
3020
|
+
const host = first(optionsMap, "hostname") ?? alias;
|
|
3021
|
+
const portText = first(optionsMap, "port");
|
|
3022
|
+
const port = portText !== void 0 ? safeInt(portText) : void 0;
|
|
3023
|
+
const user = first(optionsMap, "user");
|
|
3024
|
+
const identityFiles = optionsMap["identityfile"] ?? [];
|
|
3025
|
+
const knownHostsFiles = optionsMap["userknownhostsfile"] ?? [];
|
|
3026
|
+
const connectTimeoutText = first(optionsMap, "connecttimeout");
|
|
3027
|
+
const proxyJump = first(optionsMap, "proxyjump");
|
|
3028
|
+
const kex = optionsMap["kexalgorithms"] ?? [];
|
|
3029
|
+
const ciphers = optionsMap["ciphers"] ?? [];
|
|
3030
|
+
const macs = optionsMap["macs"] ?? [];
|
|
3031
|
+
const serverHostKey = optionsMap["hostkeyalgorithms"] ?? [];
|
|
3032
|
+
const profile = { host, provider: "sftp" };
|
|
3033
|
+
if (port !== void 0) profile.port = port;
|
|
3034
|
+
if (user !== void 0) profile.username = { value: user };
|
|
3035
|
+
if (connectTimeoutText !== void 0) {
|
|
3036
|
+
const seconds = safeInt(connectTimeoutText);
|
|
3037
|
+
if (seconds !== void 0) profile.timeoutMs = seconds * 1e3;
|
|
3038
|
+
}
|
|
3039
|
+
const ssh = {};
|
|
3040
|
+
if (identityFiles.length > 0) {
|
|
3041
|
+
const firstKey = identityFiles[0];
|
|
3042
|
+
if (firstKey !== void 0) ssh.privateKey = { path: expandHome(firstKey) };
|
|
3043
|
+
}
|
|
3044
|
+
if (knownHostsFiles.length > 0) {
|
|
3045
|
+
ssh.knownHosts = knownHostsFiles.map((path2) => ({ path: expandHome(path2) }));
|
|
3046
|
+
}
|
|
3047
|
+
const algorithms = {};
|
|
3048
|
+
if (kex.length > 0) algorithms["kex"] = expandAlgorithms(kex);
|
|
3049
|
+
if (ciphers.length > 0) algorithms["cipher"] = expandAlgorithms(ciphers);
|
|
3050
|
+
if (macs.length > 0) algorithms["hmac"] = expandAlgorithms(macs);
|
|
3051
|
+
if (serverHostKey.length > 0) algorithms["serverHostKey"] = expandAlgorithms(serverHostKey);
|
|
3052
|
+
if (Object.keys(algorithms).length > 0) {
|
|
3053
|
+
ssh.algorithms = algorithms;
|
|
3054
|
+
}
|
|
3055
|
+
if (Object.keys(ssh).length > 0) profile.ssh = ssh;
|
|
3056
|
+
const result = {
|
|
3057
|
+
identityFiles: identityFiles.map(expandHome),
|
|
3058
|
+
profile,
|
|
3059
|
+
resolved
|
|
3060
|
+
};
|
|
3061
|
+
if (proxyJump !== void 0) result.proxyJump = proxyJump;
|
|
3062
|
+
return result;
|
|
3063
|
+
}
|
|
3064
|
+
function first(options, key) {
|
|
3065
|
+
const values = options[key];
|
|
3066
|
+
return values !== void 0 && values.length > 0 ? values[0] : void 0;
|
|
3067
|
+
}
|
|
3068
|
+
function safeInt(text) {
|
|
3069
|
+
const value = Number.parseInt(text, 10);
|
|
3070
|
+
return Number.isFinite(value) ? value : void 0;
|
|
3071
|
+
}
|
|
3072
|
+
function expandHome(path2) {
|
|
3073
|
+
if (!path2.startsWith("~")) return path2;
|
|
3074
|
+
const home = process.env["HOME"] ?? process.env["USERPROFILE"];
|
|
3075
|
+
if (home === void 0) return path2;
|
|
3076
|
+
if (path2 === "~") return home;
|
|
3077
|
+
if (path2.startsWith("~/") || path2.startsWith("~\\")) return `${home}${path2.slice(1)}`;
|
|
3078
|
+
return path2;
|
|
3079
|
+
}
|
|
3080
|
+
function expandAlgorithms(values) {
|
|
3081
|
+
const out = [];
|
|
3082
|
+
for (const value of values) {
|
|
3083
|
+
for (const part of value.split(",")) {
|
|
3084
|
+
const trimmed = part.trim();
|
|
3085
|
+
if (trimmed !== "") out.push(trimmed);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
return out;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// src/profiles/importers/FileZillaImporter.ts
|
|
3092
|
+
import { Buffer as Buffer5 } from "buffer";
|
|
3093
|
+
function importFileZillaSites(xml) {
|
|
3094
|
+
const events = tokenizeXml(xml);
|
|
3095
|
+
if (events.length === 0) {
|
|
3096
|
+
throw new ConfigurationError({
|
|
3097
|
+
code: "filezilla_xml_empty",
|
|
3098
|
+
message: "FileZilla sitemanager XML is empty.",
|
|
3099
|
+
retryable: false
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
const sites = [];
|
|
3103
|
+
const skipped = [];
|
|
3104
|
+
const folderStack = [];
|
|
3105
|
+
const folderNamePending = [];
|
|
3106
|
+
let inServer = false;
|
|
3107
|
+
let serverFields = {};
|
|
3108
|
+
let serverPasswordEncoding;
|
|
3109
|
+
let activeTag;
|
|
3110
|
+
let captureFolderName = false;
|
|
3111
|
+
for (const event of events) {
|
|
3112
|
+
if (event.kind === "open") {
|
|
3113
|
+
if (event.name === "Folder") {
|
|
3114
|
+
folderStack.push("");
|
|
3115
|
+
folderNamePending.push(true);
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
if (event.name === "Server") {
|
|
3119
|
+
inServer = true;
|
|
3120
|
+
serverFields = {};
|
|
3121
|
+
serverPasswordEncoding = void 0;
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
activeTag = event.name;
|
|
3125
|
+
if (event.name === "Pass" && inServer) {
|
|
3126
|
+
serverPasswordEncoding = event.attributes["encoding"];
|
|
3127
|
+
}
|
|
3128
|
+
if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
|
|
3129
|
+
captureFolderName = true;
|
|
3130
|
+
}
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3133
|
+
if (event.kind === "text") {
|
|
3134
|
+
if (captureFolderName) {
|
|
3135
|
+
const top = folderStack.length - 1;
|
|
3136
|
+
if (top >= 0) folderStack[top] = event.text.trim();
|
|
3137
|
+
captureFolderName = false;
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
if (inServer && activeTag !== void 0) {
|
|
3141
|
+
serverFields[activeTag] = (serverFields[activeTag] ?? "") + event.text;
|
|
3142
|
+
}
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (event.kind === "close") {
|
|
3146
|
+
if (event.name === "Folder") {
|
|
3147
|
+
folderStack.pop();
|
|
3148
|
+
folderNamePending.pop();
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
if (event.name === "Server") {
|
|
3152
|
+
const folder = folderStack.filter((segment) => segment !== "");
|
|
3153
|
+
const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
|
|
3154
|
+
if (result.kind === "site") {
|
|
3155
|
+
sites.push({ ...result.site, folder });
|
|
3156
|
+
} else {
|
|
3157
|
+
skipped.push({
|
|
3158
|
+
folder,
|
|
3159
|
+
name: result.name,
|
|
3160
|
+
...result.protocol !== void 0 ? { protocol: result.protocol } : {}
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
inServer = false;
|
|
3164
|
+
serverFields = {};
|
|
3165
|
+
serverPasswordEncoding = void 0;
|
|
3166
|
+
activeTag = void 0;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
if (activeTag === event.name) activeTag = void 0;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
return { sites, skipped };
|
|
3173
|
+
}
|
|
3174
|
+
function buildSiteFromFields(fields, passwordEncoding) {
|
|
3175
|
+
const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
|
|
3176
|
+
const host = (fields["Host"] ?? "").trim();
|
|
3177
|
+
if (host === "") return { kind: "skipped", name };
|
|
3178
|
+
const protocolText = fields["Protocol"];
|
|
3179
|
+
const protocol = protocolText !== void 0 ? Number.parseInt(protocolText.trim(), 10) : 0;
|
|
3180
|
+
const mapped = mapFileZillaProtocol(protocol);
|
|
3181
|
+
if (mapped === void 0) {
|
|
3182
|
+
return Number.isFinite(protocol) ? { kind: "skipped", name, protocol } : { kind: "skipped", name };
|
|
3183
|
+
}
|
|
3184
|
+
const profile = { host, provider: mapped.provider };
|
|
3185
|
+
if (mapped.secure !== void 0) profile.secure = mapped.secure;
|
|
3186
|
+
const portText = fields["Port"];
|
|
3187
|
+
if (portText !== void 0) {
|
|
3188
|
+
const port = Number.parseInt(portText.trim(), 10);
|
|
3189
|
+
if (Number.isFinite(port)) profile.port = port;
|
|
3190
|
+
}
|
|
3191
|
+
const user = fields["User"]?.trim();
|
|
3192
|
+
if (user !== void 0 && user !== "") profile.username = { value: user };
|
|
3193
|
+
let password;
|
|
3194
|
+
const rawPass = fields["Pass"];
|
|
3195
|
+
if (rawPass !== void 0 && rawPass !== "") {
|
|
3196
|
+
if (passwordEncoding === "base64") {
|
|
3197
|
+
password = Buffer5.from(rawPass, "base64").toString("utf8");
|
|
3198
|
+
} else {
|
|
3199
|
+
password = rawPass;
|
|
3200
|
+
}
|
|
3201
|
+
if (password !== void 0 && password !== "") profile.password = { value: password };
|
|
3202
|
+
}
|
|
3203
|
+
const site = { name, profile };
|
|
3204
|
+
if (password !== void 0) site.password = password;
|
|
3205
|
+
const logonText = fields["Logontype"];
|
|
3206
|
+
if (logonText !== void 0) {
|
|
3207
|
+
const logonType = Number.parseInt(logonText.trim(), 10);
|
|
3208
|
+
if (Number.isFinite(logonType)) site.logonType = logonType;
|
|
3209
|
+
}
|
|
3210
|
+
return { kind: "site", site };
|
|
3211
|
+
}
|
|
3212
|
+
function mapFileZillaProtocol(code) {
|
|
3213
|
+
switch (code) {
|
|
3214
|
+
case 0:
|
|
3215
|
+
return { provider: "ftp" };
|
|
3216
|
+
case 1:
|
|
3217
|
+
return { provider: "sftp" };
|
|
3218
|
+
case 4:
|
|
3219
|
+
return { provider: "ftps", secure: true };
|
|
3220
|
+
case 5:
|
|
3221
|
+
return { provider: "ftps", secure: true };
|
|
3222
|
+
case 6:
|
|
3223
|
+
return { provider: "ftp", secure: false };
|
|
3224
|
+
default:
|
|
3225
|
+
return void 0;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
function tokenizeXml(xml) {
|
|
3229
|
+
const events = [];
|
|
3230
|
+
let index = 0;
|
|
3231
|
+
const length = xml.length;
|
|
3232
|
+
while (index < length) {
|
|
3233
|
+
const lt = xml.indexOf("<", index);
|
|
3234
|
+
if (lt === -1) {
|
|
3235
|
+
const text = xml.slice(index);
|
|
3236
|
+
if (text.trim() !== "") events.push({ kind: "text", text: decodeEntities(text) });
|
|
3237
|
+
break;
|
|
3238
|
+
}
|
|
3239
|
+
if (lt > index) {
|
|
3240
|
+
const text = xml.slice(index, lt);
|
|
3241
|
+
if (text.trim() !== "") events.push({ kind: "text", text: decodeEntities(text) });
|
|
3242
|
+
}
|
|
3243
|
+
if (xml.startsWith("<!--", lt)) {
|
|
3244
|
+
const end = xml.indexOf("-->", lt + 4);
|
|
3245
|
+
index = end === -1 ? length : end + 3;
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
if (xml.startsWith("<![CDATA[", lt)) {
|
|
3249
|
+
const end = xml.indexOf("]]>", lt + 9);
|
|
3250
|
+
const cdataEnd = end === -1 ? length : end;
|
|
3251
|
+
events.push({ kind: "text", text: xml.slice(lt + 9, cdataEnd) });
|
|
3252
|
+
index = end === -1 ? length : end + 3;
|
|
3253
|
+
continue;
|
|
3254
|
+
}
|
|
3255
|
+
if (xml[lt + 1] === "?" || xml[lt + 1] === "!") {
|
|
3256
|
+
const gt2 = xml.indexOf(">", lt + 1);
|
|
3257
|
+
index = gt2 === -1 ? length : gt2 + 1;
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
const gt = xml.indexOf(">", lt + 1);
|
|
3261
|
+
if (gt === -1) break;
|
|
3262
|
+
const tagBody = xml.slice(lt + 1, gt);
|
|
3263
|
+
if (tagBody.startsWith("/")) {
|
|
3264
|
+
events.push({ kind: "close", name: tagBody.slice(1).trim() });
|
|
3265
|
+
} else {
|
|
3266
|
+
const selfClosing = tagBody.endsWith("/");
|
|
3267
|
+
const body = selfClosing ? tagBody.slice(0, -1) : tagBody;
|
|
3268
|
+
const { name, attributes } = parseTagBody(body.trim());
|
|
3269
|
+
events.push({ attributes, kind: "open", name, selfClosing });
|
|
3270
|
+
if (selfClosing) events.push({ kind: "close", name });
|
|
3271
|
+
}
|
|
3272
|
+
index = gt + 1;
|
|
3273
|
+
}
|
|
3274
|
+
return events;
|
|
3275
|
+
}
|
|
3276
|
+
function parseTagBody(body) {
|
|
3277
|
+
const match = body.match(/^([A-Za-z_:][\w:.-]*)\s*(.*)$/);
|
|
3278
|
+
if (!match) return { attributes: {}, name: body };
|
|
3279
|
+
const name = match[1] ?? "";
|
|
3280
|
+
const rest = match[2] ?? "";
|
|
3281
|
+
const attributes = {};
|
|
3282
|
+
const attrRegex = /([A-Za-z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
3283
|
+
let attrMatch;
|
|
3284
|
+
while ((attrMatch = attrRegex.exec(rest)) !== null) {
|
|
3285
|
+
const key = attrMatch[1];
|
|
3286
|
+
const value = attrMatch[3] ?? attrMatch[4] ?? "";
|
|
3287
|
+
if (key !== void 0) attributes[key] = decodeEntities(value);
|
|
3288
|
+
}
|
|
3289
|
+
return { attributes, name };
|
|
3290
|
+
}
|
|
3291
|
+
function decodeEntities(text) {
|
|
3292
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// src/profiles/importers/WinScpImporter.ts
|
|
3296
|
+
function importWinScpSessions(ini) {
|
|
3297
|
+
const sections = parseIni(ini);
|
|
3298
|
+
const sessionSections = sections.filter((section) => section.name.startsWith("Sessions\\"));
|
|
3299
|
+
if (sessionSections.length === 0) {
|
|
3300
|
+
throw new ConfigurationError({
|
|
3301
|
+
code: "winscp_ini_no_sessions",
|
|
3302
|
+
message: "WinSCP INI does not contain any [Sessions\\...] sections.",
|
|
3303
|
+
retryable: false
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
const sessions = [];
|
|
3307
|
+
const skipped = [];
|
|
3308
|
+
for (const section of sessionSections) {
|
|
3309
|
+
const decodedPath = decodeSessionPath(section.name.slice("Sessions\\".length));
|
|
3310
|
+
const segments = decodedPath.split("/").filter((segment) => segment !== "");
|
|
3311
|
+
const name = segments[segments.length - 1] ?? decodedPath;
|
|
3312
|
+
const folder = segments.slice(0, -1);
|
|
3313
|
+
const built = buildSessionProfile(name, section.values);
|
|
3314
|
+
if (built.kind === "session") {
|
|
3315
|
+
sessions.push({ ...built.session, folder });
|
|
3316
|
+
} else {
|
|
3317
|
+
skipped.push({
|
|
3318
|
+
folder,
|
|
3319
|
+
name,
|
|
3320
|
+
...built.fsProtocol !== void 0 ? { fsProtocol: built.fsProtocol } : {}
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return { sessions, skipped };
|
|
3325
|
+
}
|
|
3326
|
+
function buildSessionProfile(name, values) {
|
|
3327
|
+
const host = values["HostName"]?.trim();
|
|
3328
|
+
if (host === void 0 || host === "") return { kind: "skipped" };
|
|
3329
|
+
const fsProtocolText = values["FSProtocol"];
|
|
3330
|
+
const fsProtocol = fsProtocolText !== void 0 ? Number.parseInt(fsProtocolText, 10) : 1;
|
|
3331
|
+
const ftpsText = values["Ftps"];
|
|
3332
|
+
const ftps = ftpsText !== void 0 ? Number.parseInt(ftpsText, 10) : 0;
|
|
3333
|
+
const mapped = mapWinScpProtocol(fsProtocol, ftps);
|
|
3334
|
+
if (mapped === void 0) {
|
|
3335
|
+
return Number.isFinite(fsProtocol) ? { fsProtocol, kind: "skipped" } : { kind: "skipped" };
|
|
3336
|
+
}
|
|
3337
|
+
const profile = { host, provider: mapped.provider };
|
|
3338
|
+
if (mapped.secure !== void 0) profile.secure = mapped.secure;
|
|
3339
|
+
const portText = values["PortNumber"];
|
|
3340
|
+
if (portText !== void 0) {
|
|
3341
|
+
const port = Number.parseInt(portText, 10);
|
|
3342
|
+
if (Number.isFinite(port)) profile.port = port;
|
|
3343
|
+
}
|
|
3344
|
+
const user = values["UserName"]?.trim();
|
|
3345
|
+
if (user !== void 0 && user !== "") profile.username = { value: user };
|
|
3346
|
+
if (mapped.provider === "sftp") {
|
|
3347
|
+
const ssh = {};
|
|
3348
|
+
const keyPath = values["PublicKeyFile"]?.trim();
|
|
3349
|
+
if (keyPath !== void 0 && keyPath !== "") ssh.privateKey = { path: keyPath };
|
|
3350
|
+
if (Object.keys(ssh).length > 0) profile.ssh = ssh;
|
|
3351
|
+
}
|
|
3352
|
+
const session = { name, profile };
|
|
3353
|
+
if (Number.isFinite(fsProtocol)) session.fsProtocol = fsProtocol;
|
|
3354
|
+
if (Number.isFinite(ftps) && ftps !== 0) session.ftps = ftps;
|
|
3355
|
+
return { kind: "session", session };
|
|
3356
|
+
}
|
|
3357
|
+
function mapWinScpProtocol(fsProtocol, ftps) {
|
|
3358
|
+
switch (fsProtocol) {
|
|
3359
|
+
case 0:
|
|
3360
|
+
case 1:
|
|
3361
|
+
case 2:
|
|
3362
|
+
return { provider: "sftp" };
|
|
3363
|
+
case 5:
|
|
3364
|
+
return ftps === 0 ? { provider: "ftp" } : { provider: "ftps", secure: ftps === 1 };
|
|
3365
|
+
default:
|
|
3366
|
+
return void 0;
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function parseIni(text) {
|
|
3370
|
+
const sections = [];
|
|
3371
|
+
let current;
|
|
3372
|
+
const lines = text.split(/\r?\n/);
|
|
3373
|
+
for (const rawLine of lines) {
|
|
3374
|
+
const line = rawLine.replace(/^\s*[#;].*$/, "").trim();
|
|
3375
|
+
if (line === "") continue;
|
|
3376
|
+
const sectionMatch = line.match(/^\[(.+)\]$/);
|
|
3377
|
+
if (sectionMatch && sectionMatch[1] !== void 0) {
|
|
3378
|
+
current = { name: sectionMatch[1], values: {} };
|
|
3379
|
+
sections.push(current);
|
|
3380
|
+
continue;
|
|
3381
|
+
}
|
|
3382
|
+
if (current === void 0) continue;
|
|
3383
|
+
const eq = line.indexOf("=");
|
|
3384
|
+
if (eq === -1) continue;
|
|
3385
|
+
const key = line.slice(0, eq).trim();
|
|
3386
|
+
const value = line.slice(eq + 1).trim();
|
|
3387
|
+
if (key !== "") current.values[key] = value;
|
|
3388
|
+
}
|
|
3389
|
+
return sections;
|
|
3390
|
+
}
|
|
3391
|
+
function decodeSessionPath(name) {
|
|
3392
|
+
try {
|
|
3393
|
+
return decodeURIComponent(name);
|
|
3394
|
+
} catch {
|
|
3395
|
+
return name;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/errors/errorFactory.ts
|
|
3400
|
+
function errorFromFtpReply(input) {
|
|
3401
|
+
const details = {
|
|
3402
|
+
ftpCode: input.ftpCode,
|
|
3403
|
+
message: input.message,
|
|
3404
|
+
protocol: input.protocol ?? "ftp",
|
|
3405
|
+
retryable: false
|
|
3406
|
+
};
|
|
3407
|
+
if (input.command !== void 0) details.command = input.command;
|
|
3408
|
+
if (input.path !== void 0) details.path = input.path;
|
|
3409
|
+
if (input.cause !== void 0) details.cause = input.cause;
|
|
3410
|
+
if (input.ftpCode === 530) {
|
|
3411
|
+
return new AuthenticationError(details);
|
|
3412
|
+
}
|
|
3413
|
+
if (input.ftpCode === 421) {
|
|
3414
|
+
return new ConnectionError({
|
|
3415
|
+
...details,
|
|
3416
|
+
retryable: true
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
if (input.ftpCode === 550) {
|
|
3420
|
+
return mapFtp550(details);
|
|
3421
|
+
}
|
|
3422
|
+
if ([450, 451, 452].includes(input.ftpCode)) {
|
|
3423
|
+
return new TransferError({
|
|
3424
|
+
...details,
|
|
3425
|
+
retryable: true
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
if (input.ftpCode >= 400 && input.ftpCode < 500) {
|
|
3429
|
+
return new ConnectionError({
|
|
3430
|
+
...details,
|
|
3431
|
+
retryable: true
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
return new ProtocolError(details);
|
|
3435
|
+
}
|
|
3436
|
+
function mapFtp550(details) {
|
|
3437
|
+
const lowerMessage = details.message.toLowerCase();
|
|
3438
|
+
if (lowerMessage.includes("already") || lowerMessage.includes("exists")) {
|
|
3439
|
+
return new PathAlreadyExistsError(details);
|
|
3440
|
+
}
|
|
3441
|
+
if (lowerMessage.includes("not found") || lowerMessage.includes("no such") || lowerMessage.includes("unavailable")) {
|
|
3442
|
+
return new PathNotFoundError(details);
|
|
3443
|
+
}
|
|
3444
|
+
return new PermissionDeniedError(details);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// src/transfers/TransferPlan.ts
|
|
3448
|
+
function createTransferPlan(input) {
|
|
3449
|
+
const plan = {
|
|
3450
|
+
createdAt: input.now?.() ?? /* @__PURE__ */ new Date(),
|
|
3451
|
+
dryRun: input.dryRun ?? true,
|
|
3452
|
+
id: input.id,
|
|
3453
|
+
steps: input.steps.map(clonePlanStep),
|
|
3454
|
+
warnings: [...input.warnings ?? []]
|
|
3455
|
+
};
|
|
3456
|
+
if (input.metadata !== void 0) {
|
|
3457
|
+
plan.metadata = { ...input.metadata };
|
|
3458
|
+
}
|
|
3459
|
+
return plan;
|
|
3460
|
+
}
|
|
3461
|
+
function summarizeTransferPlan(plan) {
|
|
3462
|
+
const actions = {};
|
|
3463
|
+
let destructiveSteps = 0;
|
|
3464
|
+
let executableSteps = 0;
|
|
3465
|
+
let skippedSteps = 0;
|
|
3466
|
+
let totalExpectedBytes = 0;
|
|
3467
|
+
for (const step of plan.steps) {
|
|
3468
|
+
actions[step.action] = (actions[step.action] ?? 0) + 1;
|
|
3469
|
+
destructiveSteps += step.destructive === true ? 1 : 0;
|
|
3470
|
+
skippedSteps += step.action === "skip" ? 1 : 0;
|
|
3471
|
+
executableSteps += step.action === "skip" ? 0 : 1;
|
|
3472
|
+
totalExpectedBytes += step.expectedBytes ?? 0;
|
|
3473
|
+
}
|
|
3474
|
+
return {
|
|
3475
|
+
actions,
|
|
3476
|
+
destructiveSteps,
|
|
3477
|
+
executableSteps,
|
|
3478
|
+
skippedSteps,
|
|
3479
|
+
totalExpectedBytes,
|
|
3480
|
+
totalSteps: plan.steps.length
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function createTransferJobsFromPlan(plan) {
|
|
3484
|
+
return plan.steps.flatMap((step) => {
|
|
3485
|
+
if (step.action === "skip") {
|
|
3486
|
+
return [];
|
|
3487
|
+
}
|
|
3488
|
+
const job = {
|
|
3489
|
+
id: `${plan.id}:${step.id}`,
|
|
3490
|
+
operation: step.action
|
|
3491
|
+
};
|
|
3492
|
+
if (step.source !== void 0) job.source = cloneEndpoint2(step.source);
|
|
3493
|
+
if (step.destination !== void 0) job.destination = cloneEndpoint2(step.destination);
|
|
3494
|
+
if (step.expectedBytes !== void 0) job.totalBytes = step.expectedBytes;
|
|
3495
|
+
if (step.metadata !== void 0) job.metadata = { ...step.metadata };
|
|
3496
|
+
return [job];
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
function clonePlanStep(step) {
|
|
3500
|
+
const clone = {
|
|
3501
|
+
action: step.action,
|
|
3502
|
+
id: step.id
|
|
3503
|
+
};
|
|
3504
|
+
if (step.source !== void 0) clone.source = cloneEndpoint2(step.source);
|
|
3505
|
+
if (step.destination !== void 0) clone.destination = cloneEndpoint2(step.destination);
|
|
3506
|
+
if (step.expectedBytes !== void 0) clone.expectedBytes = step.expectedBytes;
|
|
3507
|
+
if (step.destructive !== void 0) clone.destructive = step.destructive;
|
|
3508
|
+
if (step.reason !== void 0) clone.reason = step.reason;
|
|
3509
|
+
if (step.metadata !== void 0) clone.metadata = { ...step.metadata };
|
|
3510
|
+
return clone;
|
|
3511
|
+
}
|
|
3512
|
+
function cloneEndpoint2(endpoint) {
|
|
3513
|
+
const clone = { path: endpoint.path };
|
|
3514
|
+
if (endpoint.provider !== void 0) {
|
|
3515
|
+
clone.provider = endpoint.provider;
|
|
3516
|
+
}
|
|
3517
|
+
return clone;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
// src/transfers/TransferQueue.ts
|
|
3521
|
+
var TransferQueue = class {
|
|
3522
|
+
engine;
|
|
3523
|
+
items = [];
|
|
3524
|
+
defaultExecutor;
|
|
3525
|
+
resolveExecutor;
|
|
3526
|
+
retry;
|
|
3527
|
+
timeout;
|
|
3528
|
+
bandwidthLimit;
|
|
3529
|
+
onProgress;
|
|
3530
|
+
onReceipt;
|
|
3531
|
+
onError;
|
|
3532
|
+
concurrency;
|
|
3533
|
+
paused = false;
|
|
3534
|
+
/**
|
|
3535
|
+
* Creates a transfer queue.
|
|
3536
|
+
*
|
|
3537
|
+
* @param options - Queue engine, concurrency, executor, and observer options.
|
|
3538
|
+
*/
|
|
3539
|
+
constructor(options = {}) {
|
|
3540
|
+
this.engine = options.engine ?? new TransferEngine();
|
|
3541
|
+
this.concurrency = normalizeConcurrency(options.concurrency);
|
|
3542
|
+
this.defaultExecutor = options.executor;
|
|
3543
|
+
this.resolveExecutor = options.resolveExecutor;
|
|
3544
|
+
this.retry = options.retry;
|
|
3545
|
+
this.timeout = options.timeout;
|
|
3546
|
+
this.bandwidthLimit = options.bandwidthLimit;
|
|
3547
|
+
this.onProgress = options.onProgress;
|
|
3548
|
+
this.onReceipt = options.onReceipt;
|
|
3549
|
+
this.onError = options.onError;
|
|
3550
|
+
}
|
|
3551
|
+
/** Adds a transfer job to the queue. */
|
|
3552
|
+
add(job, executor) {
|
|
3553
|
+
if (this.items.some((item2) => item2.id === job.id)) {
|
|
3554
|
+
throw new ConfigurationError({
|
|
3555
|
+
details: { jobId: job.id },
|
|
3556
|
+
message: `Transfer queue already contains job: ${job.id}`,
|
|
3557
|
+
retryable: false
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
const item = {
|
|
3561
|
+
controller: new AbortController(),
|
|
3562
|
+
id: job.id,
|
|
3563
|
+
job: cloneTransferJob(job),
|
|
3564
|
+
status: "queued"
|
|
3565
|
+
};
|
|
3566
|
+
if (executor !== void 0) {
|
|
3567
|
+
item.executor = executor;
|
|
3568
|
+
}
|
|
3569
|
+
this.items.push(item);
|
|
3570
|
+
return toPublicItem(item);
|
|
3571
|
+
}
|
|
3572
|
+
/** Pauses dispatch of new queued jobs. Running jobs are allowed to finish. */
|
|
3573
|
+
pause() {
|
|
3574
|
+
this.paused = true;
|
|
3575
|
+
}
|
|
3576
|
+
/** Resumes dispatch of queued jobs on the next `run()` call. */
|
|
3577
|
+
resume() {
|
|
3578
|
+
this.paused = false;
|
|
3579
|
+
}
|
|
3580
|
+
/** Updates queue concurrency for subsequent drains. */
|
|
3581
|
+
setConcurrency(concurrency) {
|
|
3582
|
+
this.concurrency = normalizeConcurrency(concurrency);
|
|
3583
|
+
}
|
|
3584
|
+
/** Cancels a queued or running job. */
|
|
3585
|
+
cancel(jobId) {
|
|
3586
|
+
const item = this.items.find((candidate) => candidate.id === jobId);
|
|
3587
|
+
if (item === void 0 || item.status === "completed" || item.status === "failed" || item.status === "canceled") {
|
|
3588
|
+
return false;
|
|
3589
|
+
}
|
|
3590
|
+
item.controller.abort();
|
|
3591
|
+
if (item.status === "queued") {
|
|
3592
|
+
item.status = "canceled";
|
|
3593
|
+
}
|
|
3594
|
+
return true;
|
|
3595
|
+
}
|
|
3596
|
+
/** Returns a queued item snapshot by id. */
|
|
3597
|
+
get(jobId) {
|
|
3598
|
+
const item = this.items.find((candidate) => candidate.id === jobId);
|
|
3599
|
+
return item === void 0 ? void 0 : toPublicItem(item);
|
|
3600
|
+
}
|
|
3601
|
+
/** Lists queue item snapshots in insertion order. */
|
|
3602
|
+
list() {
|
|
3603
|
+
return this.items.map(toPublicItem);
|
|
3604
|
+
}
|
|
3605
|
+
/** Drains currently queued jobs until complete, failed, canceled, or paused. */
|
|
3606
|
+
async run(options = {}) {
|
|
3607
|
+
const workerCount = Math.max(1, Math.min(this.concurrency, this.countDispatchableItems()));
|
|
3608
|
+
const workers = Array.from({ length: workerCount }, () => this.runWorker(options));
|
|
3609
|
+
await Promise.all(workers);
|
|
3610
|
+
return this.summarize();
|
|
3611
|
+
}
|
|
3612
|
+
/** Returns a queue summary without executing more work. */
|
|
3613
|
+
summarize() {
|
|
3614
|
+
const publicItems = this.items.map(toPublicItem);
|
|
3615
|
+
return {
|
|
3616
|
+
canceled: publicItems.filter((item) => item.status === "canceled").length,
|
|
3617
|
+
completed: publicItems.filter((item) => item.status === "completed").length,
|
|
3618
|
+
failed: publicItems.filter((item) => item.status === "failed").length,
|
|
3619
|
+
failures: publicItems.filter((item) => item.status === "failed"),
|
|
3620
|
+
queued: publicItems.filter((item) => item.status === "queued").length,
|
|
3621
|
+
receipts: publicItems.filter(
|
|
3622
|
+
(item) => item.receipt !== void 0
|
|
3623
|
+
).map((item) => item.receipt),
|
|
3624
|
+
running: publicItems.filter((item) => item.status === "running").length,
|
|
3625
|
+
total: publicItems.length
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
async runWorker(options) {
|
|
3629
|
+
for (; ; ) {
|
|
3630
|
+
const item = this.nextQueuedItem();
|
|
3631
|
+
if (item === void 0) {
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
await this.runItem(item, options);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
nextQueuedItem() {
|
|
3638
|
+
if (this.paused) {
|
|
3639
|
+
return void 0;
|
|
3640
|
+
}
|
|
3641
|
+
const item = this.items.find((candidate) => candidate.status === "queued");
|
|
3642
|
+
if (item !== void 0) {
|
|
3643
|
+
item.status = item.controller.signal.aborted ? "canceled" : "running";
|
|
3644
|
+
}
|
|
3645
|
+
return item?.status === "running" ? item : void 0;
|
|
3646
|
+
}
|
|
3647
|
+
async runItem(item, options) {
|
|
3648
|
+
const abortListener = createAbortForwarder(options.signal, item.controller);
|
|
3649
|
+
try {
|
|
3650
|
+
const executeOptions = {
|
|
3651
|
+
signal: item.controller.signal
|
|
3652
|
+
};
|
|
3653
|
+
const onProgress = options.onProgress ?? this.onProgress;
|
|
3654
|
+
const retry = options.retry ?? this.retry;
|
|
3655
|
+
const timeout = options.timeout ?? this.timeout;
|
|
3656
|
+
const bandwidthLimit = options.bandwidthLimit ?? this.bandwidthLimit;
|
|
3657
|
+
if (onProgress !== void 0) {
|
|
3658
|
+
executeOptions.onProgress = onProgress;
|
|
3659
|
+
}
|
|
3660
|
+
if (retry !== void 0) {
|
|
3661
|
+
executeOptions.retry = retry;
|
|
3662
|
+
}
|
|
3663
|
+
if (timeout !== void 0) {
|
|
3664
|
+
executeOptions.timeout = timeout;
|
|
3665
|
+
}
|
|
3666
|
+
if (bandwidthLimit !== void 0) {
|
|
3667
|
+
executeOptions.bandwidthLimit = bandwidthLimit;
|
|
3668
|
+
}
|
|
3669
|
+
const receipt = await this.engine.execute(
|
|
3670
|
+
item.job,
|
|
3671
|
+
this.requireExecutor(item),
|
|
3672
|
+
executeOptions
|
|
3673
|
+
);
|
|
3674
|
+
item.receipt = receipt;
|
|
3675
|
+
item.status = "completed";
|
|
3676
|
+
this.onReceipt?.(receipt);
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
item.error = error;
|
|
3679
|
+
item.status = item.controller.signal.aborted ? "canceled" : "failed";
|
|
3680
|
+
if (item.status === "failed") {
|
|
3681
|
+
this.onError?.(toPublicItem(item), error);
|
|
3682
|
+
}
|
|
3683
|
+
} finally {
|
|
3684
|
+
abortListener.dispose();
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
requireExecutor(item) {
|
|
3688
|
+
const executor = item.executor ?? this.defaultExecutor ?? this.resolveExecutor?.(item.job);
|
|
3689
|
+
if (executor === void 0) {
|
|
3690
|
+
throw new ConfigurationError({
|
|
3691
|
+
details: { jobId: item.job.id },
|
|
3692
|
+
message: `Transfer queue job has no executor: ${item.job.id}`,
|
|
3693
|
+
retryable: false
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
return executor;
|
|
3697
|
+
}
|
|
3698
|
+
countDispatchableItems() {
|
|
3699
|
+
return this.items.filter((item) => item.status === "queued" && !item.controller.signal.aborted).length;
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
function normalizeConcurrency(value) {
|
|
3703
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
3704
|
+
return 1;
|
|
3705
|
+
}
|
|
3706
|
+
return Math.max(1, Math.floor(value));
|
|
3707
|
+
}
|
|
3708
|
+
function createAbortForwarder(source, target) {
|
|
3709
|
+
if (source === void 0) {
|
|
3710
|
+
return { dispose: () => void 0 };
|
|
3711
|
+
}
|
|
3712
|
+
const abort = () => target.abort();
|
|
3713
|
+
if (source.aborted) {
|
|
3714
|
+
abort();
|
|
3715
|
+
return { dispose: () => void 0 };
|
|
3716
|
+
}
|
|
3717
|
+
source.addEventListener("abort", abort, { once: true });
|
|
3718
|
+
return {
|
|
3719
|
+
dispose: () => source.removeEventListener("abort", abort)
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
function toPublicItem(item) {
|
|
3723
|
+
const snapshot = {
|
|
3724
|
+
id: item.id,
|
|
3725
|
+
job: cloneTransferJob(item.job),
|
|
3726
|
+
status: item.status
|
|
3727
|
+
};
|
|
3728
|
+
if (item.receipt !== void 0) snapshot.receipt = item.receipt;
|
|
3729
|
+
if (item.error !== void 0) snapshot.error = item.error;
|
|
3730
|
+
return snapshot;
|
|
3731
|
+
}
|
|
3732
|
+
function cloneTransferJob(job) {
|
|
3733
|
+
const clone = {
|
|
3734
|
+
id: job.id,
|
|
3735
|
+
operation: job.operation
|
|
3736
|
+
};
|
|
3737
|
+
if (job.source !== void 0) clone.source = { ...job.source };
|
|
3738
|
+
if (job.destination !== void 0) clone.destination = { ...job.destination };
|
|
3739
|
+
if (job.totalBytes !== void 0) clone.totalBytes = job.totalBytes;
|
|
3740
|
+
if (job.resumed !== void 0) clone.resumed = job.resumed;
|
|
3741
|
+
if (job.metadata !== void 0) clone.metadata = { ...job.metadata };
|
|
3742
|
+
return clone;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// src/sync/createRemoteBrowser.ts
|
|
3746
|
+
function parentRemotePath(input) {
|
|
3747
|
+
const normalized = normalizeRemotePath(input);
|
|
3748
|
+
if (normalized === "/") return "/";
|
|
3749
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
3750
|
+
parts.pop();
|
|
3751
|
+
if (parts.length === 0) return "/";
|
|
3752
|
+
return `/${parts.join("/")}`;
|
|
3753
|
+
}
|
|
3754
|
+
function buildRemoteBreadcrumbs(input) {
|
|
3755
|
+
const normalized = normalizeRemotePath(input);
|
|
3756
|
+
const crumbs = [{ name: "/", path: "/" }];
|
|
3757
|
+
if (normalized === "/") return crumbs;
|
|
3758
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
3759
|
+
let cursor = "";
|
|
3760
|
+
for (const part of parts) {
|
|
3761
|
+
cursor += `/${part}`;
|
|
3762
|
+
crumbs.push({ name: part, path: cursor });
|
|
3763
|
+
}
|
|
3764
|
+
return crumbs;
|
|
3765
|
+
}
|
|
3766
|
+
function sortRemoteEntries(entries, key = "name", order = "asc") {
|
|
3767
|
+
const direction = order === "asc" ? 1 : -1;
|
|
3768
|
+
return [...entries].sort((left, right) => {
|
|
3769
|
+
if (key !== "type") {
|
|
3770
|
+
const leftIsDir = left.type === "directory";
|
|
3771
|
+
const rightIsDir = right.type === "directory";
|
|
3772
|
+
if (leftIsDir !== rightIsDir) return leftIsDir ? -1 : 1;
|
|
3773
|
+
}
|
|
3774
|
+
const compared = compareEntriesByKey(left, right, key);
|
|
3775
|
+
if (compared !== 0) return compared * direction;
|
|
3776
|
+
return compareNames(left, right);
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
function filterRemoteEntries(entries, options = {}) {
|
|
3780
|
+
const showHidden = options.showHidden ?? true;
|
|
3781
|
+
const filter = options.filter;
|
|
3782
|
+
return entries.filter((entry) => {
|
|
3783
|
+
if (!showHidden && entry.name.startsWith(".")) return false;
|
|
3784
|
+
if (filter !== void 0 && !filter(entry)) return false;
|
|
3785
|
+
return true;
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
function createRemoteBrowser(options) {
|
|
3789
|
+
const { fs } = options;
|
|
3790
|
+
let currentPath = normalizeRemotePath(options.initialPath ?? "/");
|
|
3791
|
+
let cachedEntries = [];
|
|
3792
|
+
let sortKey = options.sortKey ?? "name";
|
|
3793
|
+
let sortOrder = options.sortOrder ?? "asc";
|
|
3794
|
+
let showHidden = options.showHidden ?? true;
|
|
3795
|
+
const filter = options.filter;
|
|
3796
|
+
async function loadCurrent() {
|
|
3797
|
+
const raw = await fs.list(currentPath);
|
|
3798
|
+
const projected = projectEntries(raw);
|
|
3799
|
+
cachedEntries = projected;
|
|
3800
|
+
return snapshot();
|
|
3801
|
+
}
|
|
3802
|
+
function projectEntries(raw) {
|
|
3803
|
+
const filterOptions = { showHidden };
|
|
3804
|
+
if (filter !== void 0) filterOptions.filter = filter;
|
|
3805
|
+
const filtered = filterRemoteEntries(raw, filterOptions);
|
|
3806
|
+
return sortRemoteEntries(filtered, sortKey, sortOrder);
|
|
3807
|
+
}
|
|
3808
|
+
function snapshot() {
|
|
3809
|
+
return {
|
|
3810
|
+
breadcrumbs: buildRemoteBreadcrumbs(currentPath),
|
|
3811
|
+
entries: [...cachedEntries],
|
|
3812
|
+
path: currentPath
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
async function navigate(target) {
|
|
3816
|
+
currentPath = resolveTarget(currentPath, target);
|
|
3817
|
+
return loadCurrent();
|
|
3818
|
+
}
|
|
3819
|
+
async function open2(entry) {
|
|
3820
|
+
if (entry.type !== "directory") {
|
|
3821
|
+
throw new TypeError(`Cannot open non-directory entry "${entry.path}" (type: ${entry.type})`);
|
|
3822
|
+
}
|
|
3823
|
+
return navigate(entry.path);
|
|
3824
|
+
}
|
|
3825
|
+
return {
|
|
3826
|
+
breadcrumbs: () => buildRemoteBreadcrumbs(currentPath),
|
|
3827
|
+
get entries() {
|
|
3828
|
+
return cachedEntries;
|
|
3829
|
+
},
|
|
3830
|
+
navigate,
|
|
3831
|
+
open: open2,
|
|
3832
|
+
get path() {
|
|
3833
|
+
return currentPath;
|
|
3834
|
+
},
|
|
3835
|
+
refresh: loadCurrent,
|
|
3836
|
+
setShowHidden(value) {
|
|
3837
|
+
showHidden = value;
|
|
3838
|
+
},
|
|
3839
|
+
setSort(key, order = sortOrder) {
|
|
3840
|
+
sortKey = key;
|
|
3841
|
+
sortOrder = order;
|
|
3842
|
+
},
|
|
3843
|
+
up: () => navigate(parentRemotePath(currentPath))
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
function resolveTarget(currentPath, target) {
|
|
3847
|
+
if (target.startsWith("/")) return normalizeRemotePath(target);
|
|
3848
|
+
if (target === "" || target === ".") return currentPath;
|
|
3849
|
+
if (target === "..") return parentRemotePath(currentPath);
|
|
3850
|
+
const base = currentPath === "/" ? "" : currentPath;
|
|
3851
|
+
return normalizeRemotePath(`${base}/${target}`);
|
|
3852
|
+
}
|
|
3853
|
+
function compareEntriesByKey(left, right, key) {
|
|
3854
|
+
switch (key) {
|
|
3855
|
+
case "size":
|
|
3856
|
+
return (left.size ?? 0) - (right.size ?? 0);
|
|
3857
|
+
case "modifiedAt": {
|
|
3858
|
+
const leftTime = left.modifiedAt?.getTime() ?? 0;
|
|
3859
|
+
const rightTime = right.modifiedAt?.getTime() ?? 0;
|
|
3860
|
+
return leftTime - rightTime;
|
|
3861
|
+
}
|
|
3862
|
+
case "type":
|
|
3863
|
+
return left.type.localeCompare(right.type);
|
|
3864
|
+
case "name":
|
|
3865
|
+
default:
|
|
3866
|
+
return compareNames(left, right);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
function compareNames(left, right) {
|
|
3870
|
+
return left.name.localeCompare(right.name, void 0, { numeric: true, sensitivity: "base" });
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// src/sync/createSyncPlan.ts
|
|
3874
|
+
function createSyncPlan(options) {
|
|
3875
|
+
const direction = options.direction ?? "source-to-destination";
|
|
3876
|
+
const deletePolicy = options.deletePolicy ?? "never";
|
|
3877
|
+
const conflictPolicy = options.conflictPolicy ?? "overwrite";
|
|
3878
|
+
const includeDirectoryActions = options.includeDirectoryActions ?? false;
|
|
3879
|
+
const sourceRoot = normalizeRemotePath(options.source.rootPath);
|
|
3880
|
+
const destinationRoot = normalizeRemotePath(options.destination.rootPath);
|
|
3881
|
+
const warnings = [];
|
|
3882
|
+
const steps = [];
|
|
3883
|
+
for (const entry of options.diff.entries) {
|
|
3884
|
+
const context = {
|
|
3885
|
+
conflictPolicy,
|
|
3886
|
+
deletePolicy,
|
|
3887
|
+
destinationRoot,
|
|
3888
|
+
direction,
|
|
3889
|
+
entry,
|
|
3890
|
+
includeDirectoryActions,
|
|
3891
|
+
sourceRoot,
|
|
3892
|
+
warnings
|
|
3893
|
+
};
|
|
3894
|
+
if (options.source.provider !== void 0) context.sourceProvider = options.source.provider;
|
|
3895
|
+
if (options.destination.provider !== void 0) {
|
|
3896
|
+
context.destinationProvider = options.destination.provider;
|
|
3897
|
+
}
|
|
3898
|
+
const step = planEntry(context);
|
|
3899
|
+
if (step !== void 0) steps.push(step);
|
|
3900
|
+
}
|
|
3901
|
+
const planInput = {
|
|
3902
|
+
id: options.id,
|
|
3903
|
+
steps,
|
|
3904
|
+
warnings
|
|
3905
|
+
};
|
|
3906
|
+
if (options.dryRun !== void 0) planInput.dryRun = options.dryRun;
|
|
3907
|
+
if (options.now !== void 0) planInput.now = options.now;
|
|
3908
|
+
if (options.metadata !== void 0) planInput.metadata = options.metadata;
|
|
3909
|
+
return createTransferPlan(planInput);
|
|
3910
|
+
}
|
|
3911
|
+
function planEntry(context) {
|
|
3912
|
+
const { entry } = context;
|
|
3913
|
+
const isDirectory = isDirectoryEntry(entry);
|
|
3914
|
+
if (isDirectory && !context.includeDirectoryActions) {
|
|
3915
|
+
return void 0;
|
|
3916
|
+
}
|
|
3917
|
+
switch (entry.status) {
|
|
3918
|
+
case "added":
|
|
3919
|
+
return planAdded(context);
|
|
3920
|
+
case "removed":
|
|
3921
|
+
return planRemoved(context);
|
|
3922
|
+
case "modified":
|
|
3923
|
+
return planModified(context);
|
|
3924
|
+
case "unchanged":
|
|
3925
|
+
return planUnchanged(context);
|
|
3926
|
+
default:
|
|
3927
|
+
return void 0;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
function planAdded(context) {
|
|
3931
|
+
if (context.direction === "source-to-destination") {
|
|
3932
|
+
return createCopyStep(context, "source", "destination", expectedBytesFor(context.entry));
|
|
3933
|
+
}
|
|
3934
|
+
if (context.deletePolicy === "never") {
|
|
3935
|
+
return createSkipStep(context, "Source-only entry preserved by delete policy");
|
|
3936
|
+
}
|
|
3937
|
+
return createDeleteStep(context, "source");
|
|
3938
|
+
}
|
|
3939
|
+
function planRemoved(context) {
|
|
3940
|
+
if (context.direction === "destination-to-source") {
|
|
3941
|
+
return createCopyStep(context, "destination", "source", expectedBytesFor(context.entry));
|
|
3942
|
+
}
|
|
3943
|
+
if (context.deletePolicy === "never") {
|
|
3944
|
+
return createSkipStep(context, "Destination-only entry preserved by delete policy");
|
|
3945
|
+
}
|
|
3946
|
+
if (context.deletePolicy === "replace-only") {
|
|
3947
|
+
return createSkipStep(
|
|
3948
|
+
context,
|
|
3949
|
+
"Destination-only entry preserved (no source replacement available)"
|
|
3950
|
+
);
|
|
3951
|
+
}
|
|
3952
|
+
return createDeleteStep(context, "destination");
|
|
3953
|
+
}
|
|
3954
|
+
function planModified(context) {
|
|
3955
|
+
switch (context.conflictPolicy) {
|
|
3956
|
+
case "overwrite":
|
|
3957
|
+
return createCopyStep(context, "source", "destination", expectedBytesFor(context.entry), {
|
|
3958
|
+
destructive: true
|
|
3959
|
+
});
|
|
3960
|
+
case "prefer-destination":
|
|
3961
|
+
return createCopyStep(context, "destination", "source", expectedBytesFor(context.entry), {
|
|
3962
|
+
destructive: true
|
|
3963
|
+
});
|
|
3964
|
+
case "skip":
|
|
3965
|
+
return createSkipStep(context, `Conflict skipped: ${context.entry.reasons.join(",")}`);
|
|
3966
|
+
case "error":
|
|
3967
|
+
throw new ConfigurationError({
|
|
3968
|
+
details: {
|
|
3969
|
+
path: context.entry.path,
|
|
3970
|
+
reasons: context.entry.reasons
|
|
3971
|
+
},
|
|
3972
|
+
message: `Sync plan conflict at ${context.entry.path} with reasons: ${context.entry.reasons.join(", ")}`,
|
|
3973
|
+
retryable: false
|
|
3974
|
+
});
|
|
3975
|
+
default:
|
|
3976
|
+
return createSkipStep(context, "Conflict skipped");
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
function planUnchanged(context) {
|
|
3980
|
+
return createSkipStep(context, "Entry already in sync");
|
|
3981
|
+
}
|
|
3982
|
+
function createCopyStep(context, fromSide, toSide, expectedBytes, overrides = {}) {
|
|
3983
|
+
const step = {
|
|
3984
|
+
action: "copy",
|
|
3985
|
+
id: makeStepId(context.entry, `copy-${fromSide}-to-${toSide}`),
|
|
3986
|
+
reason: describeReasons(context.entry, `Copy ${fromSide} to ${toSide}`)
|
|
3987
|
+
};
|
|
3988
|
+
step.source = endpointFor(context, fromSide);
|
|
3989
|
+
step.destination = endpointFor(context, toSide);
|
|
3990
|
+
if (expectedBytes !== void 0) step.expectedBytes = expectedBytes;
|
|
3991
|
+
if (overrides.destructive === true) step.destructive = true;
|
|
3992
|
+
if (overrides.metadata !== void 0) step.metadata = { ...overrides.metadata };
|
|
3993
|
+
return step;
|
|
3994
|
+
}
|
|
3995
|
+
function createDeleteStep(context, side) {
|
|
3996
|
+
return {
|
|
3997
|
+
action: "delete",
|
|
3998
|
+
destination: endpointFor(context, side),
|
|
3999
|
+
destructive: true,
|
|
4000
|
+
id: makeStepId(context.entry, `delete-${side}`),
|
|
4001
|
+
reason: `Delete ${side} entry not present on the other side`
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function createSkipStep(context, reason) {
|
|
4005
|
+
return {
|
|
4006
|
+
action: "skip",
|
|
4007
|
+
id: makeStepId(context.entry, "skip"),
|
|
4008
|
+
reason,
|
|
4009
|
+
source: endpointFor(context, "source"),
|
|
4010
|
+
destination: endpointFor(context, "destination")
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
function endpointFor(context, side) {
|
|
4014
|
+
const root = side === "source" ? context.sourceRoot : context.destinationRoot;
|
|
4015
|
+
const provider = side === "source" ? context.sourceProvider : context.destinationProvider;
|
|
4016
|
+
const endpoint = {
|
|
4017
|
+
path: joinRootAndRelative(root, context.entry.path)
|
|
4018
|
+
};
|
|
4019
|
+
if (provider !== void 0) endpoint.provider = provider;
|
|
4020
|
+
return endpoint;
|
|
4021
|
+
}
|
|
4022
|
+
function joinRootAndRelative(rootPath, relativePath) {
|
|
4023
|
+
if (rootPath === "/") return relativePath;
|
|
4024
|
+
if (relativePath === "/") return rootPath;
|
|
4025
|
+
return joinRemotePath(rootPath, relativePath);
|
|
4026
|
+
}
|
|
4027
|
+
function makeStepId(entry, suffix) {
|
|
4028
|
+
return `${entry.path}#${suffix}`;
|
|
4029
|
+
}
|
|
4030
|
+
function describeReasons(entry, prefix) {
|
|
4031
|
+
if (entry.reasons.length === 0) return prefix;
|
|
4032
|
+
return `${prefix} (${entry.reasons.join(",")})`;
|
|
4033
|
+
}
|
|
4034
|
+
function expectedBytesFor(entry) {
|
|
4035
|
+
return entry.source?.size ?? entry.destination?.size;
|
|
4036
|
+
}
|
|
4037
|
+
function isDirectoryEntry(entry) {
|
|
4038
|
+
return entry.source?.type === "directory" || entry.destination?.type === "directory";
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
// src/sync/createAtomicDeployPlan.ts
|
|
4042
|
+
var DEFAULT_RELEASES_DIRECTORY = ".releases";
|
|
4043
|
+
var DEFAULT_RETAIN = 3;
|
|
4044
|
+
function createAtomicDeployPlan(options) {
|
|
4045
|
+
const retain = options.retain ?? DEFAULT_RETAIN;
|
|
4046
|
+
if (retain < 1) {
|
|
4047
|
+
throw new ConfigurationError({
|
|
4048
|
+
details: { retain },
|
|
4049
|
+
message: "Atomic deploy retain count must be at least 1",
|
|
4050
|
+
retryable: false
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
const livePath = normalizeRemotePath(options.destination.rootPath);
|
|
4054
|
+
if (livePath === "/") {
|
|
4055
|
+
throw new ConfigurationError({
|
|
4056
|
+
message: "Atomic deploy destination rootPath must not be the filesystem root",
|
|
4057
|
+
retryable: false
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
const strategy = options.strategy ?? "rename";
|
|
4061
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
4062
|
+
const releaseId = options.releaseId ?? defaultReleaseId(now);
|
|
4063
|
+
const releasesRoot = joinRemotePath(
|
|
4064
|
+
livePath,
|
|
4065
|
+
options.releasesDirectory ?? DEFAULT_RELEASES_DIRECTORY
|
|
4066
|
+
);
|
|
4067
|
+
const stagingPath = joinRemotePath(releasesRoot, releaseId);
|
|
4068
|
+
const backupPath = strategy === "rename" ? joinRemotePath(releasesRoot, `${releaseId}.previous`) : void 0;
|
|
4069
|
+
const provider = options.destination.provider ?? options.source.provider;
|
|
4070
|
+
const warnings = [];
|
|
4071
|
+
const uploadPlan = createSyncPlan({
|
|
4072
|
+
conflictPolicy: "overwrite",
|
|
4073
|
+
deletePolicy: "never",
|
|
4074
|
+
destination: {
|
|
4075
|
+
...options.destination.provider !== void 0 ? { provider: options.destination.provider } : {},
|
|
4076
|
+
rootPath: stagingPath
|
|
4077
|
+
},
|
|
4078
|
+
diff: options.diff,
|
|
4079
|
+
direction: "source-to-destination",
|
|
4080
|
+
dryRun: options.dryRun ?? true,
|
|
4081
|
+
id: `${options.id}/upload`,
|
|
4082
|
+
includeDirectoryActions: false,
|
|
4083
|
+
...options.now !== void 0 ? { now: options.now } : {},
|
|
4084
|
+
source: options.source
|
|
4085
|
+
});
|
|
4086
|
+
const activate = buildActivateSteps({
|
|
4087
|
+
backupPath,
|
|
4088
|
+
livePath,
|
|
4089
|
+
planId: options.id,
|
|
4090
|
+
provider,
|
|
4091
|
+
stagingPath,
|
|
4092
|
+
strategy
|
|
4093
|
+
});
|
|
4094
|
+
const prune = buildPruneSteps({
|
|
4095
|
+
existingReleases: options.existingReleases ?? [],
|
|
4096
|
+
planId: options.id,
|
|
4097
|
+
provider,
|
|
4098
|
+
releaseId,
|
|
4099
|
+
releasesRoot,
|
|
4100
|
+
retain
|
|
4101
|
+
});
|
|
4102
|
+
const plan = {
|
|
4103
|
+
activate,
|
|
4104
|
+
createdAt: now,
|
|
4105
|
+
id: options.id,
|
|
4106
|
+
livePath,
|
|
4107
|
+
prune,
|
|
4108
|
+
releaseId,
|
|
4109
|
+
releasesRoot,
|
|
4110
|
+
retain,
|
|
4111
|
+
stagingPath,
|
|
4112
|
+
strategy,
|
|
4113
|
+
uploadPlan,
|
|
4114
|
+
warnings
|
|
4115
|
+
};
|
|
4116
|
+
if (provider !== void 0) plan.provider = provider;
|
|
4117
|
+
if (backupPath !== void 0) plan.backupPath = backupPath;
|
|
4118
|
+
if (options.metadata !== void 0) plan.metadata = { ...options.metadata };
|
|
4119
|
+
return plan;
|
|
4120
|
+
}
|
|
4121
|
+
function buildActivateSteps(context) {
|
|
4122
|
+
if (context.strategy === "symlink") {
|
|
4123
|
+
const step = {
|
|
4124
|
+
destructive: true,
|
|
4125
|
+
fromPath: context.stagingPath,
|
|
4126
|
+
id: `${context.planId}/activate/symlink`,
|
|
4127
|
+
operation: "symlink",
|
|
4128
|
+
reason: "Update live symlink to point at the new release",
|
|
4129
|
+
toPath: context.livePath
|
|
4130
|
+
};
|
|
4131
|
+
if (context.provider !== void 0) step.provider = context.provider;
|
|
4132
|
+
return [step];
|
|
4133
|
+
}
|
|
4134
|
+
const steps = [];
|
|
4135
|
+
if (context.backupPath !== void 0) {
|
|
4136
|
+
const backup = {
|
|
4137
|
+
destructive: true,
|
|
4138
|
+
fromPath: context.livePath,
|
|
4139
|
+
id: `${context.planId}/activate/backup`,
|
|
4140
|
+
operation: "rename",
|
|
4141
|
+
reason: "Rename current live path aside as a release backup",
|
|
4142
|
+
toPath: context.backupPath
|
|
4143
|
+
};
|
|
4144
|
+
if (context.provider !== void 0) backup.provider = context.provider;
|
|
4145
|
+
steps.push(backup);
|
|
4146
|
+
}
|
|
4147
|
+
const promote = {
|
|
4148
|
+
destructive: true,
|
|
4149
|
+
fromPath: context.stagingPath,
|
|
4150
|
+
id: `${context.planId}/activate/promote`,
|
|
4151
|
+
operation: "rename",
|
|
4152
|
+
reason: "Promote the staged release to the live path",
|
|
4153
|
+
toPath: context.livePath
|
|
4154
|
+
};
|
|
4155
|
+
if (context.provider !== void 0) promote.provider = context.provider;
|
|
4156
|
+
steps.push(promote);
|
|
4157
|
+
return steps;
|
|
4158
|
+
}
|
|
4159
|
+
function buildPruneSteps(context) {
|
|
4160
|
+
if (context.existingReleases.length === 0) return [];
|
|
4161
|
+
const normalizedRoot = normalizeRemotePath(context.releasesRoot);
|
|
4162
|
+
const newReleasePath = joinRemotePath(normalizedRoot, context.releaseId);
|
|
4163
|
+
const candidates = [...new Set(context.existingReleases.map((path2) => normalizeRemotePath(path2)))].filter((path2) => path2 !== newReleasePath).sort();
|
|
4164
|
+
const releasesToRetain = Math.max(0, context.retain - 1);
|
|
4165
|
+
if (candidates.length <= releasesToRetain) return [];
|
|
4166
|
+
const toPrune = candidates.slice(0, candidates.length - releasesToRetain);
|
|
4167
|
+
return toPrune.map((path2, index) => {
|
|
4168
|
+
const step = {
|
|
4169
|
+
id: `${context.planId}/prune/${index}`,
|
|
4170
|
+
path: path2,
|
|
4171
|
+
reason: "Older release exceeds retain window"
|
|
4172
|
+
};
|
|
4173
|
+
if (context.provider !== void 0) step.provider = context.provider;
|
|
4174
|
+
return step;
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
function defaultReleaseId(now) {
|
|
4178
|
+
return now.toISOString().replace(/[:.]/g, "-");
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
// src/sync/walkRemoteTree.ts
|
|
4182
|
+
async function* walkRemoteTree(fs, rootPath, options = {}) {
|
|
4183
|
+
const recursive = options.recursive ?? true;
|
|
4184
|
+
const includeDirectories = options.includeDirectories ?? true;
|
|
4185
|
+
const includeFiles = options.includeFiles ?? true;
|
|
4186
|
+
const followSymlinks = options.followSymlinks ?? false;
|
|
4187
|
+
const root = normalizeRemotePath(rootPath);
|
|
4188
|
+
const normalized = {
|
|
4189
|
+
followSymlinks,
|
|
4190
|
+
includeDirectories,
|
|
4191
|
+
includeFiles,
|
|
4192
|
+
recursive
|
|
4193
|
+
};
|
|
4194
|
+
if (options.maxDepth !== void 0) normalized.maxDepth = options.maxDepth;
|
|
4195
|
+
if (options.filter !== void 0) normalized.filter = options.filter;
|
|
4196
|
+
if (options.signal !== void 0) normalized.signal = options.signal;
|
|
4197
|
+
yield* walkDirectory(fs, root, 0, normalized);
|
|
4198
|
+
}
|
|
4199
|
+
async function* walkDirectory(fs, path2, depth, options) {
|
|
4200
|
+
throwIfAborted2(options.signal);
|
|
4201
|
+
const entries = await fs.list(path2);
|
|
4202
|
+
const sorted = [...entries].sort(compareEntries3);
|
|
4203
|
+
for (const entry of sorted) {
|
|
4204
|
+
if (options.filter !== void 0 && !options.filter(entry)) continue;
|
|
4205
|
+
if (matchesEntryKind(entry, options.includeDirectories, options.includeFiles)) {
|
|
4206
|
+
yield { depth, entry, parentPath: path2 };
|
|
4207
|
+
}
|
|
4208
|
+
if (options.recursive && canDescendInto(entry, options.followSymlinks) && (options.maxDepth === void 0 || depth < options.maxDepth)) {
|
|
4209
|
+
yield* walkDirectory(fs, ensureDescendPath(entry, path2), depth + 1, options);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
function matchesEntryKind(entry, includeDirectories, includeFiles) {
|
|
4214
|
+
if (entry.type === "directory") return includeDirectories;
|
|
4215
|
+
if (entry.type === "file") return includeFiles;
|
|
4216
|
+
return true;
|
|
4217
|
+
}
|
|
4218
|
+
function canDescendInto(entry, followSymlinks) {
|
|
4219
|
+
if (entry.type === "directory") return true;
|
|
4220
|
+
return followSymlinks && entry.type === "symlink";
|
|
4221
|
+
}
|
|
4222
|
+
function ensureDescendPath(entry, parentPath) {
|
|
4223
|
+
if (entry.path !== "" && entry.path !== entry.name) {
|
|
4224
|
+
return normalizeRemotePath(entry.path);
|
|
4225
|
+
}
|
|
4226
|
+
return joinRemotePath(parentPath, entry.name);
|
|
4227
|
+
}
|
|
4228
|
+
function compareEntries3(left, right) {
|
|
4229
|
+
if (left.path < right.path) return -1;
|
|
4230
|
+
if (left.path > right.path) return 1;
|
|
4231
|
+
return 0;
|
|
4232
|
+
}
|
|
4233
|
+
function throwIfAborted2(signal) {
|
|
4234
|
+
if (signal?.aborted === true) {
|
|
4235
|
+
throw new AbortError({
|
|
4236
|
+
message: "Remote tree walk aborted",
|
|
4237
|
+
retryable: false
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
// src/sync/diffRemoteTrees.ts
|
|
4243
|
+
async function diffRemoteTrees(source, sourcePath, destination, destinationPath, options = {}) {
|
|
4244
|
+
const includeUnchanged = options.includeUnchanged ?? false;
|
|
4245
|
+
const sourceRoot = normalizeRemotePath(sourcePath);
|
|
4246
|
+
const destinationRoot = normalizeRemotePath(destinationPath);
|
|
4247
|
+
const sourceWalk = createWalkOptions(options, options.sourceFilter);
|
|
4248
|
+
const destinationWalk = createWalkOptions(options, options.destinationFilter);
|
|
4249
|
+
const [sourceEntries, destinationEntries] = await Promise.all([
|
|
4250
|
+
collectEntries(source, sourceRoot, sourceWalk),
|
|
4251
|
+
collectEntries(destination, destinationRoot, destinationWalk)
|
|
4252
|
+
]);
|
|
4253
|
+
const aligned = alignEntries(sourceEntries, destinationEntries);
|
|
4254
|
+
const entries = [];
|
|
4255
|
+
const summary = {
|
|
4256
|
+
added: 0,
|
|
4257
|
+
modified: 0,
|
|
4258
|
+
removed: 0,
|
|
4259
|
+
total: 0,
|
|
4260
|
+
unchanged: 0
|
|
4261
|
+
};
|
|
4262
|
+
for (const { path: path2, source: sourceEntry, destination: destinationEntry } of aligned) {
|
|
4263
|
+
summary.total += 1;
|
|
4264
|
+
const reasons = [];
|
|
4265
|
+
let status;
|
|
4266
|
+
if (sourceEntry !== void 0 && destinationEntry === void 0) {
|
|
4267
|
+
status = "added";
|
|
4268
|
+
summary.added += 1;
|
|
4269
|
+
} else if (sourceEntry === void 0 && destinationEntry !== void 0) {
|
|
4270
|
+
status = "removed";
|
|
4271
|
+
summary.removed += 1;
|
|
4272
|
+
} else if (sourceEntry !== void 0 && destinationEntry !== void 0) {
|
|
4273
|
+
const computedReasons = compareEntries4(sourceEntry, destinationEntry, options);
|
|
4274
|
+
if (computedReasons.length === 0) {
|
|
4275
|
+
status = "unchanged";
|
|
4276
|
+
summary.unchanged += 1;
|
|
4277
|
+
} else {
|
|
4278
|
+
status = "modified";
|
|
4279
|
+
reasons.push(...computedReasons);
|
|
4280
|
+
summary.modified += 1;
|
|
4281
|
+
}
|
|
4282
|
+
} else {
|
|
4283
|
+
continue;
|
|
4284
|
+
}
|
|
4285
|
+
if (status === "unchanged" && !includeUnchanged) continue;
|
|
4286
|
+
const record = { path: path2, reasons, status };
|
|
4287
|
+
if (sourceEntry !== void 0) record.source = sourceEntry;
|
|
4288
|
+
if (destinationEntry !== void 0) record.destination = destinationEntry;
|
|
4289
|
+
entries.push(record);
|
|
4290
|
+
}
|
|
4291
|
+
entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
4292
|
+
return { entries, summary };
|
|
4293
|
+
}
|
|
4294
|
+
function createWalkOptions(options, filter) {
|
|
4295
|
+
const walk = options.walk ?? {};
|
|
4296
|
+
const merged = {};
|
|
4297
|
+
if (walk.recursive !== void 0) merged.recursive = walk.recursive;
|
|
4298
|
+
if (walk.maxDepth !== void 0) merged.maxDepth = walk.maxDepth;
|
|
4299
|
+
if (walk.includeDirectories !== void 0) merged.includeDirectories = walk.includeDirectories;
|
|
4300
|
+
if (walk.includeFiles !== void 0) merged.includeFiles = walk.includeFiles;
|
|
4301
|
+
if (walk.followSymlinks !== void 0) merged.followSymlinks = walk.followSymlinks;
|
|
4302
|
+
const resolvedFilter = filter ?? walk.filter;
|
|
4303
|
+
if (resolvedFilter !== void 0) merged.filter = resolvedFilter;
|
|
4304
|
+
if (options.signal !== void 0) merged.signal = options.signal;
|
|
4305
|
+
return merged;
|
|
4306
|
+
}
|
|
4307
|
+
async function collectEntries(fs, rootPath, walkOptions) {
|
|
4308
|
+
const map = /* @__PURE__ */ new Map();
|
|
4309
|
+
for await (const record of walkRemoteTree(fs, rootPath, walkOptions)) {
|
|
4310
|
+
const collected = toCollectedEntry(record.entry, rootPath);
|
|
4311
|
+
if (collected !== void 0) map.set(collected.relativePath, collected.entry);
|
|
4312
|
+
}
|
|
4313
|
+
return map;
|
|
4314
|
+
}
|
|
4315
|
+
function toCollectedEntry(entry, rootPath) {
|
|
4316
|
+
const root = normalizeRemotePath(rootPath);
|
|
4317
|
+
const path2 = normalizeRemotePath(entry.path);
|
|
4318
|
+
if (path2 === root) return void 0;
|
|
4319
|
+
if (root === "/") return { entry, relativePath: path2 };
|
|
4320
|
+
if (path2.startsWith(`${root}/`)) {
|
|
4321
|
+
return { entry, relativePath: path2.slice(root.length) };
|
|
4322
|
+
}
|
|
4323
|
+
return void 0;
|
|
4324
|
+
}
|
|
4325
|
+
function alignEntries(sourceEntries, destinationEntries) {
|
|
4326
|
+
const paths = /* @__PURE__ */ new Set([...sourceEntries.keys(), ...destinationEntries.keys()]);
|
|
4327
|
+
const aligned = [];
|
|
4328
|
+
for (const path2 of paths) {
|
|
4329
|
+
const pair = { path: path2 };
|
|
4330
|
+
const source = sourceEntries.get(path2);
|
|
4331
|
+
const destination = destinationEntries.get(path2);
|
|
4332
|
+
if (source !== void 0) pair.source = source;
|
|
4333
|
+
if (destination !== void 0) pair.destination = destination;
|
|
4334
|
+
aligned.push(pair);
|
|
4335
|
+
}
|
|
4336
|
+
return aligned;
|
|
4337
|
+
}
|
|
4338
|
+
function compareEntries4(source, destination, options) {
|
|
4339
|
+
const reasons = [];
|
|
4340
|
+
const compareSize = options.compareSize ?? true;
|
|
4341
|
+
const compareModifiedAt = options.compareModifiedAt ?? true;
|
|
4342
|
+
const compareUniqueId = options.compareUniqueId ?? false;
|
|
4343
|
+
const tolerance = options.modifiedAtToleranceMs ?? 1e3;
|
|
4344
|
+
if (source.type !== destination.type) {
|
|
4345
|
+
reasons.push("type");
|
|
4346
|
+
}
|
|
4347
|
+
if (compareSize && isSizeRelevant(source, destination) && source.size !== destination.size) {
|
|
4348
|
+
reasons.push("size");
|
|
4349
|
+
}
|
|
4350
|
+
if (compareModifiedAt && isModifiedAtDifferent(source, destination, tolerance)) {
|
|
4351
|
+
reasons.push("modifiedAt");
|
|
4352
|
+
}
|
|
4353
|
+
if (compareUniqueId && source.uniqueId !== void 0 && destination.uniqueId !== void 0 && source.uniqueId !== destination.uniqueId) {
|
|
4354
|
+
reasons.push("checksum");
|
|
4355
|
+
}
|
|
4356
|
+
return reasons;
|
|
4357
|
+
}
|
|
4358
|
+
function isSizeRelevant(source, destination) {
|
|
4359
|
+
if (source.type !== "file" || destination.type !== "file") return false;
|
|
4360
|
+
return source.size !== void 0 && destination.size !== void 0;
|
|
4361
|
+
}
|
|
4362
|
+
function isModifiedAtDifferent(source, destination, toleranceMs) {
|
|
4363
|
+
if (source.modifiedAt === void 0 || destination.modifiedAt === void 0) return false;
|
|
4364
|
+
const delta = Math.abs(source.modifiedAt.getTime() - destination.modifiedAt.getTime());
|
|
4365
|
+
return delta > toleranceMs;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
// src/sync/manifest.ts
|
|
4369
|
+
var REMOTE_MANIFEST_FORMAT_VERSION = 1;
|
|
4370
|
+
async function createRemoteManifest(fs, rootPath, options = {}) {
|
|
4371
|
+
const root = normalizeRemotePath(rootPath);
|
|
4372
|
+
const walkOptions = { ...options.walk ?? {} };
|
|
4373
|
+
const resolvedFilter = options.filter ?? options.walk?.filter;
|
|
4374
|
+
if (resolvedFilter !== void 0) walkOptions.filter = resolvedFilter;
|
|
4375
|
+
if (options.signal !== void 0) walkOptions.signal = options.signal;
|
|
4376
|
+
const entries = [];
|
|
4377
|
+
for await (const record of walkRemoteTree(fs, root, walkOptions)) {
|
|
4378
|
+
const relativePath = relativeFromRoot(record.entry.path, root);
|
|
4379
|
+
if (relativePath === void 0) continue;
|
|
4380
|
+
entries.push(toManifestEntry(record.entry, relativePath));
|
|
4381
|
+
}
|
|
4382
|
+
entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
4383
|
+
const generatedAt = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4384
|
+
const manifest = {
|
|
4385
|
+
entries,
|
|
4386
|
+
formatVersion: REMOTE_MANIFEST_FORMAT_VERSION,
|
|
4387
|
+
generatedAt,
|
|
4388
|
+
root
|
|
4389
|
+
};
|
|
4390
|
+
if (options.provider !== void 0) manifest.provider = options.provider;
|
|
4391
|
+
return manifest;
|
|
4392
|
+
}
|
|
4393
|
+
function serializeRemoteManifest(manifest, indent = 2) {
|
|
4394
|
+
return JSON.stringify(manifest, void 0, indent);
|
|
4395
|
+
}
|
|
4396
|
+
function parseRemoteManifest(text) {
|
|
4397
|
+
let parsed;
|
|
4398
|
+
try {
|
|
4399
|
+
parsed = JSON.parse(text);
|
|
4400
|
+
} catch (error) {
|
|
4401
|
+
throw new ConfigurationError({
|
|
4402
|
+
cause: error,
|
|
4403
|
+
message: "Failed to parse remote manifest payload as JSON",
|
|
4404
|
+
retryable: false
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
4408
|
+
throw new ConfigurationError({
|
|
4409
|
+
message: "Remote manifest payload must be a JSON object",
|
|
4410
|
+
retryable: false
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
const candidate = parsed;
|
|
4414
|
+
if (candidate.formatVersion !== REMOTE_MANIFEST_FORMAT_VERSION) {
|
|
4415
|
+
throw new ConfigurationError({
|
|
4416
|
+
details: {
|
|
4417
|
+
expected: REMOTE_MANIFEST_FORMAT_VERSION,
|
|
4418
|
+
received: candidate.formatVersion
|
|
4419
|
+
},
|
|
4420
|
+
message: `Unsupported remote manifest formatVersion: ${String(candidate.formatVersion)}`,
|
|
4421
|
+
retryable: false
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
if (typeof candidate.root !== "string" || candidate.root.length === 0) {
|
|
4425
|
+
throw new ConfigurationError({
|
|
4426
|
+
message: "Remote manifest root must be a non-empty string",
|
|
4427
|
+
retryable: false
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
if (typeof candidate.generatedAt !== "string") {
|
|
4431
|
+
throw new ConfigurationError({
|
|
4432
|
+
message: "Remote manifest generatedAt must be an ISO timestamp string",
|
|
4433
|
+
retryable: false
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
if (!Array.isArray(candidate.entries)) {
|
|
4437
|
+
throw new ConfigurationError({
|
|
4438
|
+
message: "Remote manifest entries must be an array",
|
|
4439
|
+
retryable: false
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
const entries = candidate.entries.map((entry, index) => normalizeManifestEntry(entry, index));
|
|
4443
|
+
const manifest = {
|
|
4444
|
+
entries,
|
|
4445
|
+
formatVersion: REMOTE_MANIFEST_FORMAT_VERSION,
|
|
4446
|
+
generatedAt: candidate.generatedAt,
|
|
4447
|
+
root: normalizeRemotePath(candidate.root)
|
|
4448
|
+
};
|
|
4449
|
+
if (typeof candidate.provider === "string") manifest.provider = candidate.provider;
|
|
4450
|
+
return manifest;
|
|
4451
|
+
}
|
|
4452
|
+
function compareRemoteManifests(source, destination, options = {}) {
|
|
4453
|
+
const includeUnchanged = options.includeUnchanged ?? false;
|
|
4454
|
+
const sourceMap = indexEntries(source);
|
|
4455
|
+
const destinationMap = indexEntries(destination);
|
|
4456
|
+
const paths = /* @__PURE__ */ new Set([...sourceMap.keys(), ...destinationMap.keys()]);
|
|
4457
|
+
const entries = [];
|
|
4458
|
+
const summary = {
|
|
4459
|
+
added: 0,
|
|
4460
|
+
modified: 0,
|
|
4461
|
+
removed: 0,
|
|
4462
|
+
total: 0,
|
|
4463
|
+
unchanged: 0
|
|
4464
|
+
};
|
|
4465
|
+
for (const path2 of paths) {
|
|
4466
|
+
summary.total += 1;
|
|
4467
|
+
const sourceEntry = sourceMap.get(path2);
|
|
4468
|
+
const destinationEntry = destinationMap.get(path2);
|
|
4469
|
+
const reasons = [];
|
|
4470
|
+
let status;
|
|
4471
|
+
if (sourceEntry !== void 0 && destinationEntry === void 0) {
|
|
4472
|
+
status = "added";
|
|
4473
|
+
summary.added += 1;
|
|
4474
|
+
} else if (sourceEntry === void 0 && destinationEntry !== void 0) {
|
|
4475
|
+
status = "removed";
|
|
4476
|
+
summary.removed += 1;
|
|
4477
|
+
} else if (sourceEntry !== void 0 && destinationEntry !== void 0) {
|
|
4478
|
+
const computed = compareManifestEntries(sourceEntry, destinationEntry, options);
|
|
4479
|
+
if (computed.length === 0) {
|
|
4480
|
+
status = "unchanged";
|
|
4481
|
+
summary.unchanged += 1;
|
|
4482
|
+
} else {
|
|
4483
|
+
status = "modified";
|
|
4484
|
+
reasons.push(...computed);
|
|
4485
|
+
summary.modified += 1;
|
|
4486
|
+
}
|
|
4487
|
+
} else {
|
|
4488
|
+
continue;
|
|
4489
|
+
}
|
|
4490
|
+
if (status === "unchanged" && !includeUnchanged) continue;
|
|
4491
|
+
const record = { path: path2, reasons, status };
|
|
4492
|
+
if (sourceEntry !== void 0) {
|
|
4493
|
+
record.source = manifestEntryToRemote(sourceEntry, source.root);
|
|
4494
|
+
}
|
|
4495
|
+
if (destinationEntry !== void 0) {
|
|
4496
|
+
record.destination = manifestEntryToRemote(destinationEntry, destination.root);
|
|
4497
|
+
}
|
|
4498
|
+
entries.push(record);
|
|
4499
|
+
}
|
|
4500
|
+
entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
4501
|
+
return { entries, summary };
|
|
4502
|
+
}
|
|
4503
|
+
function relativeFromRoot(entryPath, root) {
|
|
4504
|
+
const path2 = normalizeRemotePath(entryPath);
|
|
4505
|
+
if (path2 === root) return void 0;
|
|
4506
|
+
if (root === "/") return path2;
|
|
4507
|
+
if (path2.startsWith(`${root}/`)) return path2.slice(root.length);
|
|
4508
|
+
return void 0;
|
|
4509
|
+
}
|
|
4510
|
+
function toManifestEntry(entry, relativePath) {
|
|
4511
|
+
const manifestEntry = {
|
|
4512
|
+
path: relativePath,
|
|
4513
|
+
type: entry.type
|
|
4514
|
+
};
|
|
4515
|
+
if (entry.size !== void 0) manifestEntry.size = entry.size;
|
|
4516
|
+
if (entry.modifiedAt !== void 0) manifestEntry.modifiedAt = entry.modifiedAt.toISOString();
|
|
4517
|
+
if (entry.uniqueId !== void 0) manifestEntry.uniqueId = entry.uniqueId;
|
|
4518
|
+
if (entry.symlinkTarget !== void 0) manifestEntry.symlinkTarget = entry.symlinkTarget;
|
|
4519
|
+
return manifestEntry;
|
|
4520
|
+
}
|
|
4521
|
+
function normalizeManifestEntry(value, index) {
|
|
4522
|
+
if (value === null || typeof value !== "object") {
|
|
4523
|
+
throw new ConfigurationError({
|
|
4524
|
+
details: { index },
|
|
4525
|
+
message: `Remote manifest entry at index ${index} must be an object`,
|
|
4526
|
+
retryable: false
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
const candidate = value;
|
|
4530
|
+
if (typeof candidate.path !== "string" || candidate.path.length === 0) {
|
|
4531
|
+
throw new ConfigurationError({
|
|
4532
|
+
details: { index },
|
|
4533
|
+
message: `Remote manifest entry at index ${index} must have a non-empty path`,
|
|
4534
|
+
retryable: false
|
|
4535
|
+
});
|
|
4536
|
+
}
|
|
4537
|
+
if (!isRemoteEntryType(candidate.type)) {
|
|
4538
|
+
throw new ConfigurationError({
|
|
4539
|
+
details: { index, received: candidate.type },
|
|
4540
|
+
message: `Remote manifest entry at index ${index} has an invalid type`,
|
|
4541
|
+
retryable: false
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
const entry = {
|
|
4545
|
+
path: candidate.path,
|
|
4546
|
+
type: candidate.type
|
|
4547
|
+
};
|
|
4548
|
+
if (typeof candidate.size === "number") entry.size = candidate.size;
|
|
4549
|
+
if (typeof candidate.modifiedAt === "string") entry.modifiedAt = candidate.modifiedAt;
|
|
4550
|
+
if (typeof candidate.uniqueId === "string") entry.uniqueId = candidate.uniqueId;
|
|
4551
|
+
if (typeof candidate.symlinkTarget === "string") entry.symlinkTarget = candidate.symlinkTarget;
|
|
4552
|
+
return entry;
|
|
4553
|
+
}
|
|
4554
|
+
function isRemoteEntryType(value) {
|
|
4555
|
+
return value === "file" || value === "directory" || value === "symlink" || value === "unknown";
|
|
4556
|
+
}
|
|
4557
|
+
function indexEntries(manifest) {
|
|
4558
|
+
const map = /* @__PURE__ */ new Map();
|
|
4559
|
+
for (const entry of manifest.entries) map.set(entry.path, entry);
|
|
4560
|
+
return map;
|
|
4561
|
+
}
|
|
4562
|
+
function manifestEntryToRemote(entry, root) {
|
|
4563
|
+
const absolutePath2 = root === "/" ? entry.path : `${root}${entry.path}`;
|
|
4564
|
+
const remote = {
|
|
4565
|
+
name: deriveName(entry.path),
|
|
4566
|
+
path: absolutePath2,
|
|
4567
|
+
type: entry.type
|
|
4568
|
+
};
|
|
4569
|
+
if (entry.size !== void 0) remote.size = entry.size;
|
|
4570
|
+
if (entry.modifiedAt !== void 0) {
|
|
4571
|
+
const parsed = new Date(entry.modifiedAt);
|
|
4572
|
+
if (!Number.isNaN(parsed.getTime())) remote.modifiedAt = parsed;
|
|
4573
|
+
}
|
|
4574
|
+
if (entry.uniqueId !== void 0) remote.uniqueId = entry.uniqueId;
|
|
4575
|
+
if (entry.symlinkTarget !== void 0) remote.symlinkTarget = entry.symlinkTarget;
|
|
4576
|
+
return remote;
|
|
4577
|
+
}
|
|
4578
|
+
function deriveName(path2) {
|
|
4579
|
+
const segments = path2.split("/").filter(Boolean);
|
|
4580
|
+
return segments.length === 0 ? "/" : segments[segments.length - 1] ?? "/";
|
|
4581
|
+
}
|
|
4582
|
+
function compareManifestEntries(source, destination, options) {
|
|
4583
|
+
const reasons = [];
|
|
4584
|
+
const compareSize = options.compareSize ?? true;
|
|
4585
|
+
const compareModifiedAt = options.compareModifiedAt ?? true;
|
|
4586
|
+
const compareUniqueId = options.compareUniqueId ?? false;
|
|
4587
|
+
const tolerance = options.modifiedAtToleranceMs ?? 1e3;
|
|
4588
|
+
if (source.type !== destination.type) reasons.push("type");
|
|
4589
|
+
if (compareSize && source.type === "file" && destination.type === "file" && source.size !== void 0 && destination.size !== void 0 && source.size !== destination.size) {
|
|
4590
|
+
reasons.push("size");
|
|
4591
|
+
}
|
|
4592
|
+
if (compareModifiedAt && isModifiedAtDifferent2(source, destination, tolerance)) {
|
|
4593
|
+
reasons.push("modifiedAt");
|
|
4594
|
+
}
|
|
4595
|
+
if (compareUniqueId && source.uniqueId !== void 0 && destination.uniqueId !== void 0 && source.uniqueId !== destination.uniqueId) {
|
|
4596
|
+
reasons.push("checksum");
|
|
4597
|
+
}
|
|
4598
|
+
return reasons;
|
|
4599
|
+
}
|
|
4600
|
+
function isModifiedAtDifferent2(source, destination, toleranceMs) {
|
|
4601
|
+
if (source.modifiedAt === void 0 || destination.modifiedAt === void 0) return false;
|
|
4602
|
+
const sourceTime = Date.parse(source.modifiedAt);
|
|
4603
|
+
const destinationTime = Date.parse(destination.modifiedAt);
|
|
4604
|
+
if (Number.isNaN(sourceTime) || Number.isNaN(destinationTime)) return false;
|
|
4605
|
+
return Math.abs(sourceTime - destinationTime) > toleranceMs;
|
|
4606
|
+
}
|
|
4607
|
+
export {
|
|
4608
|
+
AbortError,
|
|
4609
|
+
AuthenticationError,
|
|
4610
|
+
AuthorizationError,
|
|
4611
|
+
CLASSIC_PROVIDER_IDS,
|
|
4612
|
+
ConfigurationError,
|
|
4613
|
+
ConnectionError,
|
|
4614
|
+
ParseError,
|
|
4615
|
+
PathAlreadyExistsError,
|
|
4616
|
+
PathNotFoundError,
|
|
4617
|
+
PermissionDeniedError,
|
|
4618
|
+
ProtocolError,
|
|
4619
|
+
ProviderRegistry,
|
|
4620
|
+
REDACTED,
|
|
4621
|
+
REMOTE_MANIFEST_FORMAT_VERSION,
|
|
4622
|
+
TimeoutError,
|
|
4623
|
+
TransferClient,
|
|
4624
|
+
TransferEngine,
|
|
4625
|
+
TransferError,
|
|
4626
|
+
TransferQueue,
|
|
4627
|
+
UnsupportedFeatureError,
|
|
4628
|
+
VerificationError,
|
|
4629
|
+
ZeroTransfer,
|
|
4630
|
+
ZeroTransferError,
|
|
4631
|
+
assertSafeFtpArgument,
|
|
4632
|
+
basenameRemotePath,
|
|
4633
|
+
buildRemoteBreadcrumbs,
|
|
4634
|
+
compareRemoteManifests,
|
|
4635
|
+
copyBetween,
|
|
4636
|
+
createAtomicDeployPlan,
|
|
4637
|
+
createBandwidthThrottle,
|
|
4638
|
+
createLocalProviderFactory,
|
|
4639
|
+
createMemoryProviderFactory,
|
|
4640
|
+
createOAuthTokenSecretSource,
|
|
4641
|
+
createProgressEvent,
|
|
4642
|
+
createProviderTransferExecutor,
|
|
4643
|
+
createRemoteBrowser,
|
|
4644
|
+
createRemoteManifest,
|
|
4645
|
+
createSyncPlan,
|
|
4646
|
+
createTransferClient,
|
|
4647
|
+
createTransferJobsFromPlan,
|
|
4648
|
+
createTransferPlan,
|
|
4649
|
+
createTransferResult,
|
|
4650
|
+
diffRemoteTrees,
|
|
4651
|
+
downloadFile,
|
|
4652
|
+
emitLog,
|
|
4653
|
+
errorFromFtpReply,
|
|
4654
|
+
filterRemoteEntries,
|
|
4655
|
+
importFileZillaSites,
|
|
4656
|
+
importOpenSshConfig,
|
|
4657
|
+
importWinScpSessions,
|
|
4658
|
+
isClassicProviderId,
|
|
4659
|
+
isSensitiveKey,
|
|
4660
|
+
joinRemotePath,
|
|
4661
|
+
matchKnownHosts,
|
|
4662
|
+
matchKnownHostsEntry,
|
|
4663
|
+
noopLogger,
|
|
4664
|
+
normalizeRemotePath,
|
|
4665
|
+
parentRemotePath,
|
|
4666
|
+
parseKnownHosts,
|
|
4667
|
+
parseOpenSshConfig,
|
|
4668
|
+
parseRemoteManifest,
|
|
4669
|
+
redactCommand,
|
|
4670
|
+
redactConnectionProfile,
|
|
4671
|
+
redactObject,
|
|
4672
|
+
redactSecretSource,
|
|
4673
|
+
redactValue,
|
|
4674
|
+
resolveConnectionProfileSecrets,
|
|
4675
|
+
resolveOpenSshHost,
|
|
4676
|
+
resolveProviderId,
|
|
4677
|
+
resolveSecret,
|
|
4678
|
+
runConnectionDiagnostics,
|
|
4679
|
+
serializeRemoteManifest,
|
|
4680
|
+
sortRemoteEntries,
|
|
4681
|
+
summarizeClientDiagnostics,
|
|
4682
|
+
summarizeTransferPlan,
|
|
4683
|
+
throttleByteIterable,
|
|
4684
|
+
uploadFile,
|
|
4685
|
+
validateConnectionProfile,
|
|
4686
|
+
walkRemoteTree
|
|
4687
|
+
};
|
|
4688
|
+
//# sourceMappingURL=index.mjs.map
|