scp-next 1.0.4 → 1.0.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/README.md +17 -26
- package/dist/cli/index.js +228 -38
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +233 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +234 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -88,6 +88,12 @@ function redactSensitiveValues(value) {
|
|
|
88
88
|
}
|
|
89
89
|
return value;
|
|
90
90
|
}
|
|
91
|
+
function formatErrorMessage(error) {
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
return String(redactSensitiveValues(error.message));
|
|
94
|
+
}
|
|
95
|
+
return String(redactSensitiveValues(error));
|
|
96
|
+
}
|
|
91
97
|
|
|
92
98
|
// src/errors/index.ts
|
|
93
99
|
var ScpNextError = class extends Error {
|
|
@@ -144,7 +150,7 @@ function toScpNextError(error) {
|
|
|
144
150
|
if (lowerMessage.includes("authentication") || lowerMessage.includes("permission denied")) {
|
|
145
151
|
return new AuthenticationError("SSH authentication failed.", { cause: error });
|
|
146
152
|
}
|
|
147
|
-
if (lowerMessage.includes("host verification") || lowerMessage.includes("host key")) {
|
|
153
|
+
if (lowerMessage.includes("host verification") || lowerMessage.includes("host key") || lowerMessage.includes("fingerprint") || lowerMessage.includes("host") && lowerMessage.includes("denied")) {
|
|
148
154
|
return new HostVerificationError("SSH host verification failed.", { cause: error });
|
|
149
155
|
}
|
|
150
156
|
if (lowerMessage.includes("timed out") || lowerMessage.includes("connection") || lowerMessage.includes("econn")) {
|
|
@@ -218,8 +224,39 @@ async function getLocalPathKind(inputPath) {
|
|
|
218
224
|
function normalizeFingerprint(fingerprint) {
|
|
219
225
|
return fingerprint.trim().replace(/^SHA256:/i, "").replace(/^sha256:/i, "").replace(/=+$/g, "");
|
|
220
226
|
}
|
|
227
|
+
function isHexFingerprint(fingerprint) {
|
|
228
|
+
return /^[0-9a-f]+$/i.test(fingerprint);
|
|
229
|
+
}
|
|
230
|
+
function fingerprintCandidates(fingerprint) {
|
|
231
|
+
const normalized = normalizeFingerprint(fingerprint);
|
|
232
|
+
const candidates = /* @__PURE__ */ new Set([normalized]);
|
|
233
|
+
if (isHexFingerprint(normalized)) {
|
|
234
|
+
candidates.add(normalized.toLowerCase());
|
|
235
|
+
} else {
|
|
236
|
+
try {
|
|
237
|
+
candidates.add(Buffer.from(normalized, "base64").toString("hex"));
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return [...candidates];
|
|
242
|
+
}
|
|
221
243
|
function isFingerprintMatch(actual, expected) {
|
|
222
|
-
|
|
244
|
+
const normalizedActual = normalizeFingerprint(actual);
|
|
245
|
+
const normalizedExpected = normalizeFingerprint(expected);
|
|
246
|
+
if (normalizedActual === normalizedExpected) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return isHexFingerprint(normalizedActual) && isHexFingerprint(normalizedExpected) && normalizedActual.toLowerCase() === normalizedExpected.toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
function hostVerificationHint(host) {
|
|
252
|
+
return [
|
|
253
|
+
"",
|
|
254
|
+
"Typical fix:",
|
|
255
|
+
` ssh ${host}`,
|
|
256
|
+
"",
|
|
257
|
+
"If you trust the host key, accept it and retry the scp-next command.",
|
|
258
|
+
"Alternatively configure hostFingerprint or knownHostsFile explicitly."
|
|
259
|
+
].join("\n");
|
|
223
260
|
}
|
|
224
261
|
async function resolveAllowedFingerprints(options) {
|
|
225
262
|
const fingerprints = /* @__PURE__ */ new Set();
|
|
@@ -228,7 +265,9 @@ async function resolveAllowedFingerprints(options) {
|
|
|
228
265
|
throw new HostVerificationError("A host is required for SSH host verification.");
|
|
229
266
|
}
|
|
230
267
|
if (options.hostFingerprint) {
|
|
231
|
-
|
|
268
|
+
for (const fingerprint of fingerprintCandidates(options.hostFingerprint)) {
|
|
269
|
+
fingerprints.add(fingerprint);
|
|
270
|
+
}
|
|
232
271
|
}
|
|
233
272
|
try {
|
|
234
273
|
await (0, import_promises2.access)(expandHome(knownHostsFile));
|
|
@@ -239,11 +278,15 @@ async function resolveAllowedFingerprints(options) {
|
|
|
239
278
|
continue;
|
|
240
279
|
}
|
|
241
280
|
const [hosts, algorithm, key] = trimmed.split(/\s+/);
|
|
242
|
-
|
|
281
|
+
const hostPatterns = hosts?.split(",") ?? [];
|
|
282
|
+
const portHost = options.port ? `[${options.host}]:${options.port}` : void 0;
|
|
283
|
+
const matchesHost = hostPatterns.includes(options.host) || Boolean(portHost && hostPatterns.includes(portHost));
|
|
284
|
+
if (!hosts || !algorithm || !key || !matchesHost) {
|
|
243
285
|
continue;
|
|
244
286
|
}
|
|
245
|
-
const
|
|
246
|
-
fingerprints.add(
|
|
287
|
+
const keyBuffer = Buffer.from(key, "base64");
|
|
288
|
+
fingerprints.add((0, import_node_crypto.createHash)("sha256").update(keyBuffer).digest("base64").replace(/=+$/g, ""));
|
|
289
|
+
fingerprints.add((0, import_node_crypto.createHash)("sha256").update(keyBuffer).digest("hex"));
|
|
247
290
|
}
|
|
248
291
|
} catch (error) {
|
|
249
292
|
if (options.knownHostsFile || !options.hostFingerprint) {
|
|
@@ -255,7 +298,9 @@ async function resolveAllowedFingerprints(options) {
|
|
|
255
298
|
}
|
|
256
299
|
if (fingerprints.size === 0) {
|
|
257
300
|
throw new HostVerificationError(
|
|
258
|
-
|
|
301
|
+
`SSH host verification failed before connecting: no matching host fingerprint was configured.${hostVerificationHint(
|
|
302
|
+
options.host
|
|
303
|
+
)}`
|
|
259
304
|
);
|
|
260
305
|
}
|
|
261
306
|
return fingerprints;
|
|
@@ -282,7 +327,11 @@ function toRemoteFileInfo(info) {
|
|
|
282
327
|
var Ssh2SftpTransport = class {
|
|
283
328
|
client;
|
|
284
329
|
constructor(name = "scp-next") {
|
|
285
|
-
this.client = new import_ssh2_sftp_client.default(name
|
|
330
|
+
this.client = new import_ssh2_sftp_client.default(name, {
|
|
331
|
+
error: () => void 0,
|
|
332
|
+
end: () => void 0,
|
|
333
|
+
close: () => void 0
|
|
334
|
+
});
|
|
286
335
|
}
|
|
287
336
|
async connect(options) {
|
|
288
337
|
const allowedFingerprints = await resolveAllowedFingerprints(options);
|
|
@@ -312,15 +361,24 @@ var Ssh2SftpTransport = class {
|
|
|
312
361
|
} catch (error) {
|
|
313
362
|
const converted = toScpNextError(error);
|
|
314
363
|
if (converted instanceof HostVerificationError) {
|
|
315
|
-
throw
|
|
364
|
+
throw new HostVerificationError(
|
|
365
|
+
`SSH host verification failed during connection: ${formatErrorMessage(
|
|
366
|
+
error
|
|
367
|
+
)}.${hostVerificationTroubleshooting(options.host)}`,
|
|
368
|
+
{ cause: error, context: connectionContext(options) }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (isLikelyHostVerificationFailure(error)) {
|
|
372
|
+
throw new HostVerificationError(
|
|
373
|
+
`SSH host verification failed during connection.${hostVerificationTroubleshooting(
|
|
374
|
+
options.host
|
|
375
|
+
)}`,
|
|
376
|
+
{ cause: error, context: connectionContext(options) }
|
|
377
|
+
);
|
|
316
378
|
}
|
|
317
|
-
throw new ConnectionError(
|
|
379
|
+
throw new ConnectionError(`Unable to connect to SSH server: ${formatErrorMessage(error)}`, {
|
|
318
380
|
cause: error,
|
|
319
|
-
context:
|
|
320
|
-
host: options.host,
|
|
321
|
-
port: options.port,
|
|
322
|
-
username: options.username
|
|
323
|
-
}
|
|
381
|
+
context: connectionContext(options)
|
|
324
382
|
});
|
|
325
383
|
}
|
|
326
384
|
}
|
|
@@ -341,7 +399,7 @@ var Ssh2SftpTransport = class {
|
|
|
341
399
|
try {
|
|
342
400
|
await this.client.fastPut(localPath, remotePath, this.transferOptions(onStep));
|
|
343
401
|
} catch (error) {
|
|
344
|
-
throw new TransferError(`Upload failed for ${localPath}
|
|
402
|
+
throw new TransferError(`Upload failed for ${localPath}: ${formatErrorMessage(error)}`, {
|
|
345
403
|
cause: error,
|
|
346
404
|
context: { localPath, remotePath }
|
|
347
405
|
});
|
|
@@ -351,7 +409,7 @@ var Ssh2SftpTransport = class {
|
|
|
351
409
|
try {
|
|
352
410
|
await this.client.fastGet(remotePath, localPath, this.transferOptions(onStep));
|
|
353
411
|
} catch (error) {
|
|
354
|
-
throw new TransferError(`Download failed for ${remotePath}
|
|
412
|
+
throw new TransferError(`Download failed for ${remotePath}: ${formatErrorMessage(error)}`, {
|
|
355
413
|
cause: error,
|
|
356
414
|
context: { remotePath, localPath }
|
|
357
415
|
});
|
|
@@ -368,6 +426,31 @@ var Ssh2SftpTransport = class {
|
|
|
368
426
|
};
|
|
369
427
|
}
|
|
370
428
|
};
|
|
429
|
+
function connectionContext(options) {
|
|
430
|
+
return {
|
|
431
|
+
host: options.host,
|
|
432
|
+
port: options.port,
|
|
433
|
+
username: options.username
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function isLikelyHostVerificationFailure(error) {
|
|
437
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
438
|
+
return message.includes("host") && (message.includes("verif") || message.includes("fingerprint") || message.includes("host key") || message.includes("key mismatch") || message.includes("denied"));
|
|
439
|
+
}
|
|
440
|
+
function hostVerificationTroubleshooting(host) {
|
|
441
|
+
const displayHost = host ?? "<host>";
|
|
442
|
+
return [
|
|
443
|
+
"",
|
|
444
|
+
"",
|
|
445
|
+
"Typical fix:",
|
|
446
|
+
` ssh ${displayHost}`,
|
|
447
|
+
"",
|
|
448
|
+
"If plain ssh works but scp-next fails, check that the host entry is available in",
|
|
449
|
+
"`~/.ssh/known_hosts` for the same host and port used by scp-next. For non-default ports,",
|
|
450
|
+
"OpenSSH usually stores entries as `[host]:port`.",
|
|
451
|
+
"You can also configure hostFingerprint or knownHostsFile explicitly."
|
|
452
|
+
].join("\n");
|
|
453
|
+
}
|
|
371
454
|
|
|
372
455
|
// src/client/walk.ts
|
|
373
456
|
var import_promises4 = require("fs/promises");
|
|
@@ -538,6 +621,22 @@ function percentage(transferredBytes, totalBytes) {
|
|
|
538
621
|
function mergeOptions(serverOptions, transferOptions) {
|
|
539
622
|
return { ...serverOptions, ...transferOptions ?? {} };
|
|
540
623
|
}
|
|
624
|
+
function hasTrailingPathSeparator(inputPath) {
|
|
625
|
+
return /[\\/]$/.test(inputPath);
|
|
626
|
+
}
|
|
627
|
+
function trimmedRemoteDirectory(remotePath) {
|
|
628
|
+
return remotePath.replace(/\/+$/g, "") || "/";
|
|
629
|
+
}
|
|
630
|
+
function remoteBasename(remotePath) {
|
|
631
|
+
return import_node_path4.default.posix.basename(trimmedRemoteDirectory(remotePath));
|
|
632
|
+
}
|
|
633
|
+
async function isExistingLocalDirectory(localPath) {
|
|
634
|
+
try {
|
|
635
|
+
return (await (0, import_promises6.stat)(localPath)).isDirectory();
|
|
636
|
+
} catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
541
640
|
var ScpNextClientImpl = class {
|
|
542
641
|
serverOptions;
|
|
543
642
|
transport;
|
|
@@ -595,10 +694,9 @@ var ScpNextClientImpl = class {
|
|
|
595
694
|
throw new ValidationError("Uploading a directory requires recursive mode.");
|
|
596
695
|
}
|
|
597
696
|
if (localKind === "file") {
|
|
598
|
-
await this.
|
|
599
|
-
await this.assertRemoteOverwrite(remotePath, options.overwrite);
|
|
697
|
+
const destinationFile = await this.resolveRemoteDestinationForLocalFile(options, remotePath);
|
|
600
698
|
const totalBytes2 = void 0;
|
|
601
|
-
await this.transport.uploadFile(resolveLocalPath(options.localPath),
|
|
699
|
+
await this.transport.uploadFile(resolveLocalPath(options.localPath), destinationFile, (step) => {
|
|
602
700
|
options.onProgress?.({
|
|
603
701
|
operation: "upload",
|
|
604
702
|
source: options.localPath,
|
|
@@ -615,22 +713,15 @@ var ScpNextClientImpl = class {
|
|
|
615
713
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
616
714
|
let completedBytes = 0;
|
|
617
715
|
let completedFiles = 0;
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
if (!destinationExists) {
|
|
625
|
-
if (!options.createDirectories) {
|
|
626
|
-
throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
|
|
627
|
-
context: { remotePath }
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
await this.transport.mkdir(remotePath, true);
|
|
631
|
-
}
|
|
716
|
+
const destinationRoot = await this.resolveRemoteDestinationForLocalDirectory(
|
|
717
|
+
options,
|
|
718
|
+
remotePath
|
|
719
|
+
);
|
|
632
720
|
for (const file of files) {
|
|
633
|
-
const destinationFile = remoteJoin(
|
|
721
|
+
const destinationFile = remoteJoin(
|
|
722
|
+
destinationRoot,
|
|
723
|
+
file.relativePath.split(import_node_path4.default.sep).join("/")
|
|
724
|
+
);
|
|
634
725
|
await this.ensureRemoteParent(destinationFile, true);
|
|
635
726
|
await this.assertRemoteOverwrite(destinationFile, options.overwrite);
|
|
636
727
|
await this.transport.uploadFile(file.absolutePath, destinationFile, (step) => {
|
|
@@ -651,6 +742,68 @@ var ScpNextClientImpl = class {
|
|
|
651
742
|
completedFiles += 1;
|
|
652
743
|
}
|
|
653
744
|
}
|
|
745
|
+
async resolveRemoteDestinationForLocalFile(options, remotePath) {
|
|
746
|
+
const remoteKind = await this.transport.exists(remotePath);
|
|
747
|
+
const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
|
|
748
|
+
if (!isDirectoryLikeDestination) {
|
|
749
|
+
await this.ensureRemoteParent(remotePath, options.createDirectories);
|
|
750
|
+
await this.assertRemoteOverwrite(remotePath, options.overwrite);
|
|
751
|
+
return remotePath;
|
|
752
|
+
}
|
|
753
|
+
if (remoteKind && remoteKind !== "d") {
|
|
754
|
+
throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
|
|
755
|
+
context: { remotePath }
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const remoteDirectory = remotePath.replace(/\/+$/g, "") || "/";
|
|
759
|
+
if (!remoteKind) {
|
|
760
|
+
if (!options.createDirectories) {
|
|
761
|
+
throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
|
|
762
|
+
context: { remotePath }
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
await this.transport.mkdir(remoteDirectory, true);
|
|
766
|
+
}
|
|
767
|
+
const destinationFile = remoteJoin(
|
|
768
|
+
remoteDirectory,
|
|
769
|
+
import_node_path4.default.basename(resolveLocalPath(options.localPath))
|
|
770
|
+
);
|
|
771
|
+
await this.assertRemoteOverwrite(destinationFile, options.overwrite);
|
|
772
|
+
return destinationFile;
|
|
773
|
+
}
|
|
774
|
+
async resolveRemoteDestinationForLocalDirectory(options, remotePath) {
|
|
775
|
+
const remoteKind = await this.transport.exists(remotePath);
|
|
776
|
+
const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
|
|
777
|
+
const localDirectoryName = import_node_path4.default.basename(resolveLocalPath(options.localPath));
|
|
778
|
+
if (remoteKind && remoteKind !== "d") {
|
|
779
|
+
throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
|
|
780
|
+
context: { remotePath }
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (isDirectoryLikeDestination) {
|
|
784
|
+
const remoteDirectory = trimmedRemoteDirectory(remotePath);
|
|
785
|
+
if (!remoteKind) {
|
|
786
|
+
if (!options.createDirectories) {
|
|
787
|
+
throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
|
|
788
|
+
context: { remotePath }
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
await this.transport.mkdir(remoteDirectory, true);
|
|
792
|
+
}
|
|
793
|
+
const destinationRoot = remoteJoin(remoteDirectory, localDirectoryName);
|
|
794
|
+
await this.ensureRemoteDirectory(destinationRoot);
|
|
795
|
+
return destinationRoot;
|
|
796
|
+
}
|
|
797
|
+
if (!remoteKind) {
|
|
798
|
+
if (!options.createDirectories) {
|
|
799
|
+
throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
|
|
800
|
+
context: { remotePath }
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
await this.transport.mkdir(remotePath, true);
|
|
804
|
+
}
|
|
805
|
+
return remotePath;
|
|
806
|
+
}
|
|
654
807
|
async downloadWithTransport(options) {
|
|
655
808
|
const remotePath = normalizeRemotePath(options.remotePath);
|
|
656
809
|
const remoteKind = await this.transport.exists(remotePath);
|
|
@@ -669,7 +822,7 @@ var ScpNextClientImpl = class {
|
|
|
669
822
|
await this.downloadSingleFile(options, remotePath);
|
|
670
823
|
}
|
|
671
824
|
async downloadSingleFile(options, remotePath) {
|
|
672
|
-
const localPath =
|
|
825
|
+
const localPath = await this.resolveLocalDestinationForRemoteFile(options, remotePath);
|
|
673
826
|
await this.assertLocalOverwrite(localPath, options.overwrite);
|
|
674
827
|
if (options.createDirectories) {
|
|
675
828
|
await (0, import_promises6.mkdir)(import_node_path4.default.dirname(localPath), { recursive: true });
|
|
@@ -688,7 +841,7 @@ var ScpNextClientImpl = class {
|
|
|
688
841
|
}
|
|
689
842
|
async downloadDirectory(options) {
|
|
690
843
|
const remotePath = normalizeRemotePath(options.remotePath);
|
|
691
|
-
const localRoot =
|
|
844
|
+
const localRoot = await this.resolveLocalDestinationForRemoteDirectory(options, remotePath);
|
|
692
845
|
const files = await walkRemoteFiles(this.transport, remotePath);
|
|
693
846
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
694
847
|
let completedBytes = 0;
|
|
@@ -716,6 +869,38 @@ var ScpNextClientImpl = class {
|
|
|
716
869
|
completedFiles += 1;
|
|
717
870
|
}
|
|
718
871
|
}
|
|
872
|
+
async resolveLocalDestinationForRemoteFile(options, remotePath) {
|
|
873
|
+
const localDestination = resolveLocalPath(options.localPath);
|
|
874
|
+
const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
|
|
875
|
+
if (!isDirectoryLikeDestination) {
|
|
876
|
+
return localDestination;
|
|
877
|
+
}
|
|
878
|
+
if (!await isExistingLocalDirectory(localDestination)) {
|
|
879
|
+
if (!options.createDirectories) {
|
|
880
|
+
throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
|
|
881
|
+
context: { localPath: options.localPath }
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
await (0, import_promises6.mkdir)(localDestination, { recursive: true });
|
|
885
|
+
}
|
|
886
|
+
return import_node_path4.default.join(localDestination, remoteBasename(remotePath));
|
|
887
|
+
}
|
|
888
|
+
async resolveLocalDestinationForRemoteDirectory(options, remotePath) {
|
|
889
|
+
const localDestination = resolveLocalPath(options.localPath);
|
|
890
|
+
const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
|
|
891
|
+
if (!isDirectoryLikeDestination) {
|
|
892
|
+
return localDestination;
|
|
893
|
+
}
|
|
894
|
+
if (!await isExistingLocalDirectory(localDestination)) {
|
|
895
|
+
if (!options.createDirectories) {
|
|
896
|
+
throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
|
|
897
|
+
context: { localPath: options.localPath }
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
await (0, import_promises6.mkdir)(localDestination, { recursive: true });
|
|
901
|
+
}
|
|
902
|
+
return import_node_path4.default.join(localDestination, remoteBasename(remotePath));
|
|
903
|
+
}
|
|
719
904
|
async ensureRemoteParent(remotePath, createDirectories = false) {
|
|
720
905
|
const parent = remoteDirname(remotePath);
|
|
721
906
|
if (parent === "." || parent === "/") {
|
|
@@ -732,6 +917,17 @@ var ScpNextClientImpl = class {
|
|
|
732
917
|
});
|
|
733
918
|
}
|
|
734
919
|
}
|
|
920
|
+
async ensureRemoteDirectory(remotePath) {
|
|
921
|
+
const exists = await this.transport.exists(remotePath);
|
|
922
|
+
if (exists && exists !== "d") {
|
|
923
|
+
throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
|
|
924
|
+
context: { remotePath }
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (!exists) {
|
|
928
|
+
await this.transport.mkdir(remotePath, true);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
735
931
|
async assertRemoteOverwrite(remotePath, overwrite = false) {
|
|
736
932
|
if (overwrite) {
|
|
737
933
|
return;
|