tabby-sftp-ui 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.js +408 -90
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +23 -2
- package/dist/sftp.service.d.ts +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
2
2
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
3
|
-
module.exports = factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs"));
|
|
3
|
+
module.exports = factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"));
|
|
4
4
|
else if(typeof define === 'function' && define.amd)
|
|
5
|
-
define(["@angular/common", "@angular/forms", "@angular/core", "tabby-core", "tabby-terminal", "path", "fs"], factory);
|
|
5
|
+
define(["@angular/common", "@angular/forms", "@angular/core", "tabby-core", "tabby-terminal", "path", "electron", "fs"], factory);
|
|
6
6
|
else {
|
|
7
|
-
var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs")) : factory(root["@angular/common"], root["@angular/forms"], root["@angular/core"], root["tabby-core"], root["tabby-terminal"], root["path"], root["fs"]);
|
|
7
|
+
var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs")) : factory(root["@angular/common"], root["@angular/forms"], root["@angular/core"], root["tabby-core"], root["tabby-terminal"], root["path"], root["electron"], root["fs"]);
|
|
8
8
|
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
9
9
|
}
|
|
10
|
-
})(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_fs__) => {
|
|
10
|
+
})(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_electron__, __WEBPACK_EXTERNAL_MODULE_fs__) => {
|
|
11
11
|
return /******/ (() => { // webpackBootstrap
|
|
12
12
|
/******/ "use strict";
|
|
13
13
|
/******/ var __webpack_modules__ = ({
|
|
@@ -267,6 +267,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
267
267
|
this.app = app;
|
|
268
268
|
// injected from the SSH tab when opened via SFTP-UI button
|
|
269
269
|
this.sshSession = null;
|
|
270
|
+
this.shellSession = null;
|
|
270
271
|
this.profile = null;
|
|
271
272
|
this.recoveredStub = false;
|
|
272
273
|
this.connecting = false;
|
|
@@ -281,6 +282,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
281
282
|
this.remotePath = '/';
|
|
282
283
|
this.remoteEntries = [];
|
|
283
284
|
this.sftpSession = null;
|
|
285
|
+
this.connectPromise = null;
|
|
284
286
|
this.localDropActive = false;
|
|
285
287
|
this.remoteDropActive = false;
|
|
286
288
|
this.transfers = [];
|
|
@@ -368,11 +370,32 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
368
370
|
this.remotePathInput = this.remotePath;
|
|
369
371
|
this.localPathInput = this.localPath;
|
|
370
372
|
if (this.sshSession) {
|
|
371
|
-
void this.
|
|
373
|
+
void this.waitForConnection();
|
|
372
374
|
}
|
|
373
375
|
this.loadRecentProfiles();
|
|
374
376
|
}
|
|
375
377
|
async connect() {
|
|
378
|
+
if (this.connected) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (this.connectPromise) {
|
|
382
|
+
return this.connectPromise;
|
|
383
|
+
}
|
|
384
|
+
this.connectPromise = this.connectInternal();
|
|
385
|
+
try {
|
|
386
|
+
await this.connectPromise;
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
this.connectPromise = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async waitForConnection() {
|
|
393
|
+
if (this.connected) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
await this.connect();
|
|
397
|
+
}
|
|
398
|
+
async connectInternal() {
|
|
376
399
|
if (this.connecting || this.connected) {
|
|
377
400
|
return;
|
|
378
401
|
}
|
|
@@ -384,17 +407,30 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
384
407
|
try {
|
|
385
408
|
this.sftpSession = await this.sftp.openFromSSHSession(this.sshSession);
|
|
386
409
|
this.connected = true;
|
|
387
|
-
|
|
388
|
-
this.remotePathInput = this.remotePath;
|
|
410
|
+
await this.initializeRemotePath();
|
|
389
411
|
await this.refreshRemote();
|
|
390
412
|
}
|
|
391
413
|
catch (e) {
|
|
392
414
|
console.error('[SFTP-UI] SFTP connection failed', e);
|
|
415
|
+
this.connected = false;
|
|
416
|
+
this.sftpSession = null;
|
|
393
417
|
}
|
|
394
418
|
finally {
|
|
395
419
|
this.connecting = false;
|
|
396
420
|
}
|
|
397
421
|
}
|
|
422
|
+
async initializeRemotePath() {
|
|
423
|
+
this.remotePath = await this.resolveDefaultRemotePath();
|
|
424
|
+
this.remotePathInput = this.remotePath;
|
|
425
|
+
if (await this.ensureRemoteDirectoryReady()) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Shell CWD may not be ready yet when SFTP-UI opens immediately after SSH connect.
|
|
429
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
430
|
+
this.remotePath = await this.resolveDefaultRemotePath();
|
|
431
|
+
this.remotePathInput = this.remotePath;
|
|
432
|
+
await this.ensureRemoteDirectoryReady();
|
|
433
|
+
}
|
|
398
434
|
async disconnect() {
|
|
399
435
|
this.sftpSession = null;
|
|
400
436
|
this.connected = false;
|
|
@@ -413,14 +449,26 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
413
449
|
}
|
|
414
450
|
}
|
|
415
451
|
remoteUp() {
|
|
416
|
-
if (!this.
|
|
452
|
+
if (!this.canRemoteUp()) {
|
|
417
453
|
return;
|
|
418
454
|
}
|
|
419
|
-
const next =
|
|
420
|
-
this.remotePath = next
|
|
455
|
+
const next = this.remoteParentPath(this.remotePath);
|
|
456
|
+
this.remotePath = next;
|
|
421
457
|
this.remotePathInput = this.remotePath;
|
|
422
458
|
void this.refreshRemote();
|
|
423
459
|
}
|
|
460
|
+
canRemoteUp() {
|
|
461
|
+
if (!this.connected) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
if (this.remotePath === '/') {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (this.isWindowsSftpPath(this.remotePath)) {
|
|
468
|
+
return !/^\/[A-Za-z]:\/?$/.test(this.remotePath);
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
424
472
|
async refreshLocal() {
|
|
425
473
|
try {
|
|
426
474
|
const names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(this.localPath);
|
|
@@ -484,6 +532,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
484
532
|
}
|
|
485
533
|
onDragOver(ev) {
|
|
486
534
|
ev.preventDefault();
|
|
535
|
+
if (ev.dataTransfer) {
|
|
536
|
+
ev.dataTransfer.dropEffect = 'copy';
|
|
537
|
+
}
|
|
487
538
|
}
|
|
488
539
|
onLocalMouseDown(entry, event) {
|
|
489
540
|
if (event.button === 2) {
|
|
@@ -633,17 +684,19 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
633
684
|
async onDropOnRemote(ev) {
|
|
634
685
|
ev.preventDefault();
|
|
635
686
|
this.remoteDropActive = false;
|
|
636
|
-
|
|
687
|
+
const captured = this.captureDropData(ev);
|
|
688
|
+
await this.waitForConnection();
|
|
689
|
+
if (!this.connected || !this.sftpSession) {
|
|
637
690
|
return;
|
|
638
691
|
}
|
|
639
|
-
if (!this.
|
|
692
|
+
if (!(await this.ensureRemoteDirectoryReady())) {
|
|
693
|
+
console.error('[SFTP-UI] Remote path is not ready for upload');
|
|
640
694
|
return;
|
|
641
695
|
}
|
|
642
696
|
// Drag & drop from OS file manager (Explorer/Finder) into the remote pane
|
|
643
|
-
|
|
644
|
-
if (osPaths.length) {
|
|
697
|
+
if (captured.osPaths.length) {
|
|
645
698
|
try {
|
|
646
|
-
for (const p of osPaths) {
|
|
699
|
+
for (const p of captured.osPaths) {
|
|
647
700
|
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
|
|
648
701
|
const existing = this.remoteEntries.find(e => e.name === baseName);
|
|
649
702
|
if (existing) {
|
|
@@ -663,73 +716,85 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
663
716
|
}
|
|
664
717
|
return;
|
|
665
718
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
await this.refreshRemote();
|
|
672
|
-
return;
|
|
719
|
+
const raw = captured.internalPayload;
|
|
720
|
+
if (raw) {
|
|
721
|
+
let payload;
|
|
722
|
+
try {
|
|
723
|
+
payload = JSON.parse(raw);
|
|
673
724
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
|
|
677
|
-
}
|
|
678
|
-
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
679
|
-
if (!raw) {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
let payload;
|
|
683
|
-
try {
|
|
684
|
-
payload = JSON.parse(raw);
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
try {
|
|
690
|
-
if (payload.kind === 'local-file') {
|
|
691
|
-
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
692
|
-
const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
|
|
693
|
-
if (existsOnRemote) {
|
|
694
|
-
const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
|
|
695
|
-
if (!ok) {
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
await this.deleteRemotePathRecursive(targetRemotePath);
|
|
699
|
-
}
|
|
700
|
-
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
701
|
-
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
702
|
-
await this.sftpSession.upload(targetRemotePath, upload);
|
|
703
|
-
await this.refreshRemote();
|
|
704
|
-
return;
|
|
725
|
+
catch {
|
|
726
|
+
payload = null;
|
|
705
727
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath,
|
|
709
|
-
const existsOnRemote = this.remoteEntries.some(e => e.name ===
|
|
728
|
+
try {
|
|
729
|
+
if (payload && payload.kind === 'local-file') {
|
|
730
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
731
|
+
const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
|
|
710
732
|
if (existsOnRemote) {
|
|
711
|
-
const ok = await this.showReplaceConfirm(`Replace existing "${
|
|
733
|
+
const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
|
|
712
734
|
if (!ok) {
|
|
713
|
-
|
|
735
|
+
return;
|
|
714
736
|
}
|
|
715
737
|
await this.deleteRemotePathRecursive(targetRemotePath);
|
|
716
738
|
}
|
|
717
|
-
|
|
718
|
-
|
|
739
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
740
|
+
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
741
|
+
await this.sftpSession.upload(targetRemotePath, upload);
|
|
742
|
+
await this.refreshRemote();
|
|
743
|
+
return;
|
|
719
744
|
}
|
|
745
|
+
if (payload && payload.kind === 'local-paths') {
|
|
746
|
+
for (const p of payload.paths) {
|
|
747
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, p.name);
|
|
748
|
+
const existsOnRemote = this.remoteEntries.some(e => e.name === p.name);
|
|
749
|
+
if (existsOnRemote) {
|
|
750
|
+
const ok = await this.showReplaceConfirm(`Replace existing "${p.name}" on remote?`);
|
|
751
|
+
if (!ok) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
await this.deleteRemotePathRecursive(targetRemotePath);
|
|
755
|
+
}
|
|
756
|
+
// uploadLocalPathToRemote handles both files and directories
|
|
757
|
+
await this.uploadLocalPathToRemote(this.remotePath, p.fullPath);
|
|
758
|
+
}
|
|
759
|
+
await this.refreshRemote();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
console.error('[SFTP-UI] Upload failed', e);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
|
|
768
|
+
try {
|
|
769
|
+
const dirUpload = captured.dirUploadPromise ? await captured.dirUploadPromise : null;
|
|
770
|
+
const childCount = this.countDirectoryUploadItems(dirUpload);
|
|
771
|
+
if (dirUpload && childCount > 0 && this.sftpSession) {
|
|
772
|
+
await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
|
|
720
773
|
await this.refreshRemote();
|
|
721
774
|
return;
|
|
722
775
|
}
|
|
723
776
|
}
|
|
724
777
|
catch (e) {
|
|
725
|
-
console.error('[SFTP-UI]
|
|
778
|
+
console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
|
|
779
|
+
}
|
|
780
|
+
if (captured.files.length > 0) {
|
|
781
|
+
try {
|
|
782
|
+
const uploaded = await this.uploadDroppedFilesToRemote(captured.files, this.remotePath);
|
|
783
|
+
if (uploaded > 0) {
|
|
784
|
+
await this.refreshRemote();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (e) {
|
|
788
|
+
console.error('[SFTP-UI] Blob upload failed', e);
|
|
789
|
+
}
|
|
726
790
|
}
|
|
727
791
|
}
|
|
728
792
|
async onDropOnLocal(ev) {
|
|
729
793
|
ev.preventDefault();
|
|
730
794
|
this.localDropActive = false;
|
|
795
|
+
const captured = this.captureDropData(ev);
|
|
731
796
|
// 1) Tabby's internal drag (remote -> local download)
|
|
732
|
-
const rawInternal =
|
|
797
|
+
const rawInternal = captured.internalPayload;
|
|
733
798
|
if (rawInternal) {
|
|
734
799
|
let payload;
|
|
735
800
|
try {
|
|
@@ -813,10 +878,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
813
878
|
}
|
|
814
879
|
}
|
|
815
880
|
// Drag & drop from OS file manager into the local pane (copy into current local folder)
|
|
816
|
-
|
|
817
|
-
if (osPaths.length) {
|
|
881
|
+
if (captured.osPaths.length) {
|
|
818
882
|
try {
|
|
819
|
-
for (const p of osPaths) {
|
|
883
|
+
for (const p of captured.osPaths) {
|
|
820
884
|
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
|
|
821
885
|
const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, baseName);
|
|
822
886
|
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(destPath)) {
|
|
@@ -836,8 +900,8 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
836
900
|
}
|
|
837
901
|
// Fallback: use Tabby's native drag parser, then write files to disk
|
|
838
902
|
try {
|
|
839
|
-
const dirUpload = await
|
|
840
|
-
if (dirUpload) {
|
|
903
|
+
const dirUpload = captured.dirUploadPromise ? await captured.dirUploadPromise : null;
|
|
904
|
+
if (dirUpload && this.countDirectoryUploadItems(dirUpload) > 0) {
|
|
841
905
|
await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
|
|
842
906
|
await this.refreshLocal();
|
|
843
907
|
return;
|
|
@@ -846,7 +910,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
846
910
|
catch (e) {
|
|
847
911
|
console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
|
|
848
912
|
}
|
|
849
|
-
const raw =
|
|
913
|
+
const raw = captured.internalPayload;
|
|
850
914
|
if (!raw) {
|
|
851
915
|
return;
|
|
852
916
|
}
|
|
@@ -1017,11 +1081,85 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1017
1081
|
}
|
|
1018
1082
|
}
|
|
1019
1083
|
}
|
|
1084
|
+
getElectronWebUtils() {
|
|
1085
|
+
try {
|
|
1086
|
+
const electron = __webpack_require__(/*! electron */ "electron");
|
|
1087
|
+
return electron?.webUtils ?? null;
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
countDirectoryUploadItems(dirUpload) {
|
|
1094
|
+
const childrens = dirUpload?.getChildrens?.() ?? [];
|
|
1095
|
+
let count = 0;
|
|
1096
|
+
for (const item of childrens) {
|
|
1097
|
+
if (typeof item?.getChildrens === 'function') {
|
|
1098
|
+
count += this.countDirectoryUploadItems(item);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
count++;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return count;
|
|
1105
|
+
}
|
|
1106
|
+
captureDropData(ev) {
|
|
1107
|
+
const dt = ev.dataTransfer;
|
|
1108
|
+
const types = dt ? Array.from(dt.types) : [];
|
|
1109
|
+
const files = dt?.files?.length ? Array.from(dt.files) : [];
|
|
1110
|
+
const uriList = dt?.getData('text/uri-list') || '';
|
|
1111
|
+
const textPlain = dt?.getData('text/plain') || '';
|
|
1112
|
+
const internalPayload = dt?.getData('application/x-tabby-sftp-ui') || '';
|
|
1113
|
+
const localMovePayload = dt?.getData('application/x-tabby-sftp-ui-local-move') || '';
|
|
1114
|
+
const osPaths = this.resolveDroppedOsPaths(files, uriList, textPlain);
|
|
1115
|
+
let dirUploadPromise = null;
|
|
1116
|
+
try {
|
|
1117
|
+
const promise = this.platform.startUploadFromDragEvent?.(ev, true);
|
|
1118
|
+
if (promise) {
|
|
1119
|
+
dirUploadPromise = promise;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
// ignore
|
|
1124
|
+
}
|
|
1125
|
+
return { types, files, osPaths, internalPayload, localMovePayload, dirUploadPromise };
|
|
1126
|
+
}
|
|
1127
|
+
async uploadDroppedFilesToRemote(files, remoteDir) {
|
|
1128
|
+
if (!this.sftpSession) {
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
1131
|
+
if (!files.length) {
|
|
1132
|
+
return 0;
|
|
1133
|
+
}
|
|
1134
|
+
let uploaded = 0;
|
|
1135
|
+
for (const file of files) {
|
|
1136
|
+
const upload = new tabby_core__WEBPACK_IMPORTED_MODULE_6__.HTMLFileUpload(file);
|
|
1137
|
+
const name = upload.getName();
|
|
1138
|
+
const existing = this.remoteEntries.find(e => e.name === name);
|
|
1139
|
+
if (existing) {
|
|
1140
|
+
const ok = await this.showReplaceConfirm(`Replace existing "${name}" on remote?`);
|
|
1141
|
+
if (!ok) {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
1145
|
+
await this.deleteRemotePathRecursive(remoteTarget);
|
|
1146
|
+
}
|
|
1147
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
1148
|
+
this.trackTransfer(upload, 'upload', targetRemotePath, name);
|
|
1149
|
+
await this.sftpSession.upload(targetRemotePath, upload);
|
|
1150
|
+
uploaded++;
|
|
1151
|
+
}
|
|
1152
|
+
return uploaded;
|
|
1153
|
+
}
|
|
1020
1154
|
getDroppedOsPaths(ev) {
|
|
1021
1155
|
const dt = ev.dataTransfer;
|
|
1022
1156
|
if (!dt) {
|
|
1023
1157
|
return [];
|
|
1024
1158
|
}
|
|
1159
|
+
const files = dt.files?.length ? Array.from(dt.files) : [];
|
|
1160
|
+
return this.resolveDroppedOsPaths(files, dt.getData('text/uri-list') || '', dt.getData('text/plain') || '');
|
|
1161
|
+
}
|
|
1162
|
+
resolveDroppedOsPaths(files, uriList, textPlain) {
|
|
1025
1163
|
const isWin = os__WEBPACK_IMPORTED_MODULE_1__.platform() === 'win32';
|
|
1026
1164
|
const isLocalPath = (p) => {
|
|
1027
1165
|
if (isWin) {
|
|
@@ -1030,15 +1168,34 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1030
1168
|
}
|
|
1031
1169
|
return p.startsWith('/');
|
|
1032
1170
|
};
|
|
1033
|
-
|
|
1034
|
-
|
|
1171
|
+
const dedupePaths = (paths) => {
|
|
1172
|
+
return [...new Set(paths)];
|
|
1173
|
+
};
|
|
1174
|
+
// 1) Electron-style File.path (legacy)
|
|
1175
|
+
const legacyFilePaths = dedupePaths(files
|
|
1035
1176
|
.map(f => f.path)
|
|
1036
|
-
.filter((p) => Boolean(p));
|
|
1037
|
-
if (
|
|
1038
|
-
return
|
|
1177
|
+
.filter((p) => Boolean(p && isLocalPath(p))));
|
|
1178
|
+
if (legacyFilePaths.length) {
|
|
1179
|
+
return legacyFilePaths;
|
|
1180
|
+
}
|
|
1181
|
+
// 1b) Electron webUtils.getPathForFile (modern Electron / Tabby)
|
|
1182
|
+
const webUtils = this.getElectronWebUtils();
|
|
1183
|
+
if (webUtils && files.length) {
|
|
1184
|
+
const webUtilsPaths = dedupePaths(files
|
|
1185
|
+
.map(f => {
|
|
1186
|
+
try {
|
|
1187
|
+
return webUtils.getPathForFile(f);
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
return '';
|
|
1191
|
+
}
|
|
1192
|
+
})
|
|
1193
|
+
.filter(p => p && isLocalPath(p)));
|
|
1194
|
+
if (webUtilsPaths.length) {
|
|
1195
|
+
return webUtilsPaths;
|
|
1196
|
+
}
|
|
1039
1197
|
}
|
|
1040
1198
|
// 2) Sometimes paths are exposed as URIs
|
|
1041
|
-
const uriList = dt.getData('text/uri-list') || '';
|
|
1042
1199
|
const uris = uriList
|
|
1043
1200
|
.split(/\r?\n/g)
|
|
1044
1201
|
.map(x => x.trim())
|
|
@@ -1066,8 +1223,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1066
1223
|
return uris;
|
|
1067
1224
|
}
|
|
1068
1225
|
// 3) Plain text sometimes contains a local path
|
|
1069
|
-
const
|
|
1070
|
-
const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
|
|
1226
|
+
const textLines = textPlain.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
|
|
1071
1227
|
const textPaths = textLines
|
|
1072
1228
|
.map(x => {
|
|
1073
1229
|
if (x.startsWith('file://')) {
|
|
@@ -1370,6 +1526,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1370
1526
|
}
|
|
1371
1527
|
}
|
|
1372
1528
|
getRemoteBreadcrumbs() {
|
|
1529
|
+
if (this.isWindowsSftpPath(this.remotePath)) {
|
|
1530
|
+
return this.getWindowsSftpBreadcrumbs(this.remotePath);
|
|
1531
|
+
}
|
|
1373
1532
|
const parts = this.remotePath.split('/').filter(Boolean);
|
|
1374
1533
|
const crumbs = [];
|
|
1375
1534
|
let current = '/';
|
|
@@ -1394,9 +1553,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1394
1553
|
if (!this.connected) {
|
|
1395
1554
|
return;
|
|
1396
1555
|
}
|
|
1397
|
-
|
|
1398
|
-
this.remotePath = target;
|
|
1399
|
-
this.remotePathInput = target;
|
|
1556
|
+
this.syncRemotePathFromInput();
|
|
1400
1557
|
void this.refreshRemote();
|
|
1401
1558
|
}
|
|
1402
1559
|
goToLocalPathInput() {
|
|
@@ -1583,21 +1740,169 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1583
1740
|
if (!p) {
|
|
1584
1741
|
return '/';
|
|
1585
1742
|
}
|
|
1586
|
-
let result = p.trim();
|
|
1743
|
+
let result = p.trim().replace(/\\/g, '/');
|
|
1744
|
+
const winDrive = result.match(/^\/([A-Za-z]):(?:\/(.*))?$/);
|
|
1745
|
+
if (winDrive) {
|
|
1746
|
+
const rest = winDrive[2] ?? '';
|
|
1747
|
+
return rest
|
|
1748
|
+
? `/${winDrive[1]}:/${rest}`.replace(/\/+/g, '/')
|
|
1749
|
+
: `/${winDrive[1]}:/`;
|
|
1750
|
+
}
|
|
1751
|
+
const bareDrive = result.match(/^([A-Za-z]):(?:\/(.*))?$/);
|
|
1752
|
+
if (bareDrive) {
|
|
1753
|
+
const rest = bareDrive[2] ?? '';
|
|
1754
|
+
return rest
|
|
1755
|
+
? `/${bareDrive[1]}:/${rest}`.replace(/\/+/g, '/')
|
|
1756
|
+
: `/${bareDrive[1]}:/`;
|
|
1757
|
+
}
|
|
1587
1758
|
if (!result.startsWith('/')) {
|
|
1588
1759
|
result = '/' + result;
|
|
1589
1760
|
}
|
|
1590
|
-
|
|
1591
|
-
result = result.replace(/\/+/g, '/');
|
|
1592
|
-
return result;
|
|
1761
|
+
return result.replace(/\/+/g, '/');
|
|
1593
1762
|
}
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
if (
|
|
1597
|
-
|
|
1763
|
+
async resolveDefaultRemotePath() {
|
|
1764
|
+
const canonical = await this.canonicalizeRemotePath('.');
|
|
1765
|
+
if (canonical) {
|
|
1766
|
+
const normalized = this.normalizeRemotePath(canonical);
|
|
1767
|
+
if (await this.remotePathExists(normalized)) {
|
|
1768
|
+
return normalized;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (this.shellSession) {
|
|
1772
|
+
try {
|
|
1773
|
+
const cwd = await this.shellSession.getWorkingDirectory?.();
|
|
1774
|
+
if (cwd) {
|
|
1775
|
+
const normalized = this.toRemoteSftpPath(cwd);
|
|
1776
|
+
if (await this.remotePathExists(normalized)) {
|
|
1777
|
+
return normalized;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
catch {
|
|
1782
|
+
// fall through to SFTP-based detection
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const username = this.getRemoteUsername();
|
|
1786
|
+
for (const candidate of this.buildDefaultRemotePathCandidates(username)) {
|
|
1787
|
+
if (await this.remotePathExists(candidate)) {
|
|
1788
|
+
return candidate;
|
|
1789
|
+
}
|
|
1598
1790
|
}
|
|
1599
1791
|
return '/';
|
|
1600
1792
|
}
|
|
1793
|
+
syncRemotePathFromInput() {
|
|
1794
|
+
const normalized = this.normalizeRemotePath(this.remotePathInput || this.remotePath || '/');
|
|
1795
|
+
this.remotePath = normalized;
|
|
1796
|
+
this.remotePathInput = normalized;
|
|
1797
|
+
}
|
|
1798
|
+
async ensureRemoteDirectoryReady() {
|
|
1799
|
+
if (!this.sftpSession) {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
this.syncRemotePathFromInput();
|
|
1803
|
+
if (await this.remotePathExists(this.remotePath)) {
|
|
1804
|
+
return true;
|
|
1805
|
+
}
|
|
1806
|
+
const resolved = await this.resolveDefaultRemotePath();
|
|
1807
|
+
this.remotePath = resolved;
|
|
1808
|
+
this.remotePathInput = resolved;
|
|
1809
|
+
return await this.remotePathExists(resolved);
|
|
1810
|
+
}
|
|
1811
|
+
getRemoteUsername() {
|
|
1812
|
+
return (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
|
|
1813
|
+
}
|
|
1814
|
+
buildDefaultRemotePathCandidates(username) {
|
|
1815
|
+
const candidates = [];
|
|
1816
|
+
if (username) {
|
|
1817
|
+
candidates.push(`/C:/Users/${username}`);
|
|
1818
|
+
if (username.toLowerCase() === 'administrator') {
|
|
1819
|
+
candidates.push('/C:/Users/Administrator');
|
|
1820
|
+
}
|
|
1821
|
+
if (username === 'root') {
|
|
1822
|
+
candidates.push('/root');
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
candidates.push(`/home/${username}`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
candidates.push('/');
|
|
1829
|
+
return candidates;
|
|
1830
|
+
}
|
|
1831
|
+
async canonicalizeRemotePath(p) {
|
|
1832
|
+
const session = this.sftpSession;
|
|
1833
|
+
const candidates = [
|
|
1834
|
+
session?.canonicalize?.bind(session),
|
|
1835
|
+
session?.realpath?.bind(session),
|
|
1836
|
+
session?.sftp?.canonicalize?.bind(session.sftp),
|
|
1837
|
+
session?.sftp?.realpath?.bind(session.sftp),
|
|
1838
|
+
].filter((fn) => typeof fn === 'function');
|
|
1839
|
+
for (const fn of candidates) {
|
|
1840
|
+
try {
|
|
1841
|
+
return await fn(p);
|
|
1842
|
+
}
|
|
1843
|
+
catch {
|
|
1844
|
+
// try the next available SFTP realpath implementation
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
async remotePathExists(p) {
|
|
1850
|
+
if (!this.sftpSession) {
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
try {
|
|
1854
|
+
if (this.sftpSession.stat) {
|
|
1855
|
+
const st = await this.sftpSession.stat(p);
|
|
1856
|
+
return st.isDirectory;
|
|
1857
|
+
}
|
|
1858
|
+
await this.sftpSession.readdir(p);
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
catch {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
toRemoteSftpPath(p) {
|
|
1866
|
+
return this.normalizeRemotePath(p);
|
|
1867
|
+
}
|
|
1868
|
+
isWindowsSftpPath(p) {
|
|
1869
|
+
return /^\/[A-Za-z]:\//.test(p) || /^\/[A-Za-z]:\/?$/.test(p);
|
|
1870
|
+
}
|
|
1871
|
+
getWindowsSftpBreadcrumbs(remotePath) {
|
|
1872
|
+
const match = remotePath.match(/^\/([A-Za-z]):\/?(.*)$/);
|
|
1873
|
+
if (!match) {
|
|
1874
|
+
return [{ label: '/', path: '/' }];
|
|
1875
|
+
}
|
|
1876
|
+
const drive = match[1].toUpperCase();
|
|
1877
|
+
const crumbs = [
|
|
1878
|
+
{ label: `${drive}:`, path: `/${drive}:/` },
|
|
1879
|
+
];
|
|
1880
|
+
let current = `/${drive}:/`;
|
|
1881
|
+
for (const part of match[2].split('/').filter(Boolean)) {
|
|
1882
|
+
current = `${current}${part}/`;
|
|
1883
|
+
crumbs.push({ label: part, path: current.replace(/\/+$/, '') || `/${drive}:/` });
|
|
1884
|
+
}
|
|
1885
|
+
return crumbs;
|
|
1886
|
+
}
|
|
1887
|
+
remoteParentPath(p) {
|
|
1888
|
+
if (this.isWindowsSftpPath(p)) {
|
|
1889
|
+
const match = p.match(/^\/([A-Za-z]):\/?(.*)$/);
|
|
1890
|
+
if (!match) {
|
|
1891
|
+
return '/';
|
|
1892
|
+
}
|
|
1893
|
+
const drive = match[1].toUpperCase();
|
|
1894
|
+
const parts = match[2].split('/').filter(Boolean);
|
|
1895
|
+
if (!parts.length) {
|
|
1896
|
+
return '/';
|
|
1897
|
+
}
|
|
1898
|
+
parts.pop();
|
|
1899
|
+
return parts.length
|
|
1900
|
+
? `/${drive}:/${parts.join('/')}`
|
|
1901
|
+
: `/${drive}:/`;
|
|
1902
|
+
}
|
|
1903
|
+
const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(p);
|
|
1904
|
+
return next === '.' ? '/' : next;
|
|
1905
|
+
}
|
|
1601
1906
|
loadRecentProfiles() {
|
|
1602
1907
|
try {
|
|
1603
1908
|
const rec = this.profilesService.getRecentProfiles?.();
|
|
@@ -2494,7 +2799,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
2494
2799
|
/>
|
|
2495
2800
|
</div>
|
|
2496
2801
|
<div class="pane-actions">
|
|
2497
|
-
<button (click)="remoteUp()" [disabled]="!connected ||
|
|
2802
|
+
<button (click)="remoteUp()" [disabled]="!connected || !canRemoteUp()">Up</button>
|
|
2498
2803
|
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
2499
2804
|
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2500
2805
|
</div>
|
|
@@ -2532,7 +2837,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
2532
2837
|
</div>
|
|
2533
2838
|
<div
|
|
2534
2839
|
class="entry"
|
|
2535
|
-
*ngIf="connected &&
|
|
2840
|
+
*ngIf="connected && canRemoteUp()"
|
|
2536
2841
|
(dblclick)="remoteUp()"
|
|
2537
2842
|
>
|
|
2538
2843
|
<span class="icon">⬆</span>
|
|
@@ -2969,6 +3274,7 @@ let SftpUiService = class SftpUiService {
|
|
|
2969
3274
|
inputs: {
|
|
2970
3275
|
sshSession,
|
|
2971
3276
|
profile,
|
|
3277
|
+
shellSession: sourceTab?.session ?? null,
|
|
2972
3278
|
},
|
|
2973
3279
|
});
|
|
2974
3280
|
tab.setTitle(`${baseTitle} + SFTP`);
|
|
@@ -3086,6 +3392,18 @@ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_forms__;
|
|
|
3086
3392
|
|
|
3087
3393
|
/***/ },
|
|
3088
3394
|
|
|
3395
|
+
/***/ "electron"
|
|
3396
|
+
/*!***************************!*\
|
|
3397
|
+
!*** external "electron" ***!
|
|
3398
|
+
\***************************/
|
|
3399
|
+
(module) {
|
|
3400
|
+
|
|
3401
|
+
if(typeof __WEBPACK_EXTERNAL_MODULE_electron__ === 'undefined') { var e = new Error("Cannot find module 'electron'"); e.code = 'MODULE_NOT_FOUND'; throw e; }
|
|
3402
|
+
|
|
3403
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_electron__;
|
|
3404
|
+
|
|
3405
|
+
/***/ },
|
|
3406
|
+
|
|
3089
3407
|
/***/ "fs"
|
|
3090
3408
|
/*!*********************!*\
|
|
3091
3409
|
!*** external "fs" ***!
|