tabby-sftp-ui 0.2.5 → 0.2.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 +5 -0
- package/dist/index.js +221 -21
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +18 -2
- package/dist/sftp.service.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,11 @@ Then restart Tabby.
|
|
|
142
142
|
|
|
143
143
|
### Changelog
|
|
144
144
|
|
|
145
|
+
- **0.2.6**
|
|
146
|
+
- Fix: Default remote path now detects Linux and Windows correctly (`/home/user`, `/root`, `/C:/Users/user`, etc.).
|
|
147
|
+
- Fix: Uploads work immediately after opening SFTP-UI (no need to change directories first).
|
|
148
|
+
- Fix: Remote path is resolved via SFTP canonicalize, shell CWD, and validated before upload.
|
|
149
|
+
|
|
145
150
|
- **0.2.5**
|
|
146
151
|
- Fix: SFTP UI tabs are not persisted across Tabby restarts (no more blank tabs when reopening Tabby).
|
|
147
152
|
- Fix: SFTP-UI button is shown only on SSH tabs with an active session (not on local PowerShell/CMD terminals).
|
package/dist/index.js
CHANGED
|
@@ -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);
|
|
@@ -633,10 +681,12 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
633
681
|
async onDropOnRemote(ev) {
|
|
634
682
|
ev.preventDefault();
|
|
635
683
|
this.remoteDropActive = false;
|
|
636
|
-
|
|
684
|
+
await this.waitForConnection();
|
|
685
|
+
if (!this.connected || !this.sftpSession) {
|
|
637
686
|
return;
|
|
638
687
|
}
|
|
639
|
-
if (!this.
|
|
688
|
+
if (!(await this.ensureRemoteDirectoryReady())) {
|
|
689
|
+
console.error('[SFTP-UI] Remote path is not ready for upload');
|
|
640
690
|
return;
|
|
641
691
|
}
|
|
642
692
|
// Drag & drop from OS file manager (Explorer/Finder) into the remote pane
|
|
@@ -1370,6 +1420,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1370
1420
|
}
|
|
1371
1421
|
}
|
|
1372
1422
|
getRemoteBreadcrumbs() {
|
|
1423
|
+
if (this.isWindowsSftpPath(this.remotePath)) {
|
|
1424
|
+
return this.getWindowsSftpBreadcrumbs(this.remotePath);
|
|
1425
|
+
}
|
|
1373
1426
|
const parts = this.remotePath.split('/').filter(Boolean);
|
|
1374
1427
|
const crumbs = [];
|
|
1375
1428
|
let current = '/';
|
|
@@ -1394,9 +1447,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1394
1447
|
if (!this.connected) {
|
|
1395
1448
|
return;
|
|
1396
1449
|
}
|
|
1397
|
-
|
|
1398
|
-
this.remotePath = target;
|
|
1399
|
-
this.remotePathInput = target;
|
|
1450
|
+
this.syncRemotePathFromInput();
|
|
1400
1451
|
void this.refreshRemote();
|
|
1401
1452
|
}
|
|
1402
1453
|
goToLocalPathInput() {
|
|
@@ -1583,21 +1634,169 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1583
1634
|
if (!p) {
|
|
1584
1635
|
return '/';
|
|
1585
1636
|
}
|
|
1586
|
-
let result = p.trim();
|
|
1637
|
+
let result = p.trim().replace(/\\/g, '/');
|
|
1638
|
+
const winDrive = result.match(/^\/([A-Za-z]):(?:\/(.*))?$/);
|
|
1639
|
+
if (winDrive) {
|
|
1640
|
+
const rest = winDrive[2] ?? '';
|
|
1641
|
+
return rest
|
|
1642
|
+
? `/${winDrive[1]}:/${rest}`.replace(/\/+/g, '/')
|
|
1643
|
+
: `/${winDrive[1]}:/`;
|
|
1644
|
+
}
|
|
1645
|
+
const bareDrive = result.match(/^([A-Za-z]):(?:\/(.*))?$/);
|
|
1646
|
+
if (bareDrive) {
|
|
1647
|
+
const rest = bareDrive[2] ?? '';
|
|
1648
|
+
return rest
|
|
1649
|
+
? `/${bareDrive[1]}:/${rest}`.replace(/\/+/g, '/')
|
|
1650
|
+
: `/${bareDrive[1]}:/`;
|
|
1651
|
+
}
|
|
1587
1652
|
if (!result.startsWith('/')) {
|
|
1588
1653
|
result = '/' + result;
|
|
1589
1654
|
}
|
|
1590
|
-
|
|
1591
|
-
result = result.replace(/\/+/g, '/');
|
|
1592
|
-
return result;
|
|
1655
|
+
return result.replace(/\/+/g, '/');
|
|
1593
1656
|
}
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
if (
|
|
1597
|
-
|
|
1657
|
+
async resolveDefaultRemotePath() {
|
|
1658
|
+
const canonical = await this.canonicalizeRemotePath('.');
|
|
1659
|
+
if (canonical) {
|
|
1660
|
+
const normalized = this.normalizeRemotePath(canonical);
|
|
1661
|
+
if (await this.remotePathExists(normalized)) {
|
|
1662
|
+
return normalized;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (this.shellSession) {
|
|
1666
|
+
try {
|
|
1667
|
+
const cwd = await this.shellSession.getWorkingDirectory?.();
|
|
1668
|
+
if (cwd) {
|
|
1669
|
+
const normalized = this.toRemoteSftpPath(cwd);
|
|
1670
|
+
if (await this.remotePathExists(normalized)) {
|
|
1671
|
+
return normalized;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
// fall through to SFTP-based detection
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const username = this.getRemoteUsername();
|
|
1680
|
+
for (const candidate of this.buildDefaultRemotePathCandidates(username)) {
|
|
1681
|
+
if (await this.remotePathExists(candidate)) {
|
|
1682
|
+
return candidate;
|
|
1683
|
+
}
|
|
1598
1684
|
}
|
|
1599
1685
|
return '/';
|
|
1600
1686
|
}
|
|
1687
|
+
syncRemotePathFromInput() {
|
|
1688
|
+
const normalized = this.normalizeRemotePath(this.remotePathInput || this.remotePath || '/');
|
|
1689
|
+
this.remotePath = normalized;
|
|
1690
|
+
this.remotePathInput = normalized;
|
|
1691
|
+
}
|
|
1692
|
+
async ensureRemoteDirectoryReady() {
|
|
1693
|
+
if (!this.sftpSession) {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
this.syncRemotePathFromInput();
|
|
1697
|
+
if (await this.remotePathExists(this.remotePath)) {
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1700
|
+
const resolved = await this.resolveDefaultRemotePath();
|
|
1701
|
+
this.remotePath = resolved;
|
|
1702
|
+
this.remotePathInput = resolved;
|
|
1703
|
+
return await this.remotePathExists(resolved);
|
|
1704
|
+
}
|
|
1705
|
+
getRemoteUsername() {
|
|
1706
|
+
return (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
|
|
1707
|
+
}
|
|
1708
|
+
buildDefaultRemotePathCandidates(username) {
|
|
1709
|
+
const candidates = [];
|
|
1710
|
+
if (username) {
|
|
1711
|
+
candidates.push(`/C:/Users/${username}`);
|
|
1712
|
+
if (username.toLowerCase() === 'administrator') {
|
|
1713
|
+
candidates.push('/C:/Users/Administrator');
|
|
1714
|
+
}
|
|
1715
|
+
if (username === 'root') {
|
|
1716
|
+
candidates.push('/root');
|
|
1717
|
+
}
|
|
1718
|
+
else {
|
|
1719
|
+
candidates.push(`/home/${username}`);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
candidates.push('/');
|
|
1723
|
+
return candidates;
|
|
1724
|
+
}
|
|
1725
|
+
async canonicalizeRemotePath(p) {
|
|
1726
|
+
const session = this.sftpSession;
|
|
1727
|
+
const candidates = [
|
|
1728
|
+
session?.canonicalize?.bind(session),
|
|
1729
|
+
session?.realpath?.bind(session),
|
|
1730
|
+
session?.sftp?.canonicalize?.bind(session.sftp),
|
|
1731
|
+
session?.sftp?.realpath?.bind(session.sftp),
|
|
1732
|
+
].filter((fn) => typeof fn === 'function');
|
|
1733
|
+
for (const fn of candidates) {
|
|
1734
|
+
try {
|
|
1735
|
+
return await fn(p);
|
|
1736
|
+
}
|
|
1737
|
+
catch {
|
|
1738
|
+
// try the next available SFTP realpath implementation
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
async remotePathExists(p) {
|
|
1744
|
+
if (!this.sftpSession) {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
if (this.sftpSession.stat) {
|
|
1749
|
+
const st = await this.sftpSession.stat(p);
|
|
1750
|
+
return st.isDirectory;
|
|
1751
|
+
}
|
|
1752
|
+
await this.sftpSession.readdir(p);
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
catch {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
toRemoteSftpPath(p) {
|
|
1760
|
+
return this.normalizeRemotePath(p);
|
|
1761
|
+
}
|
|
1762
|
+
isWindowsSftpPath(p) {
|
|
1763
|
+
return /^\/[A-Za-z]:\//.test(p) || /^\/[A-Za-z]:\/?$/.test(p);
|
|
1764
|
+
}
|
|
1765
|
+
getWindowsSftpBreadcrumbs(remotePath) {
|
|
1766
|
+
const match = remotePath.match(/^\/([A-Za-z]):\/?(.*)$/);
|
|
1767
|
+
if (!match) {
|
|
1768
|
+
return [{ label: '/', path: '/' }];
|
|
1769
|
+
}
|
|
1770
|
+
const drive = match[1].toUpperCase();
|
|
1771
|
+
const crumbs = [
|
|
1772
|
+
{ label: `${drive}:`, path: `/${drive}:/` },
|
|
1773
|
+
];
|
|
1774
|
+
let current = `/${drive}:/`;
|
|
1775
|
+
for (const part of match[2].split('/').filter(Boolean)) {
|
|
1776
|
+
current = `${current}${part}/`;
|
|
1777
|
+
crumbs.push({ label: part, path: current.replace(/\/+$/, '') || `/${drive}:/` });
|
|
1778
|
+
}
|
|
1779
|
+
return crumbs;
|
|
1780
|
+
}
|
|
1781
|
+
remoteParentPath(p) {
|
|
1782
|
+
if (this.isWindowsSftpPath(p)) {
|
|
1783
|
+
const match = p.match(/^\/([A-Za-z]):\/?(.*)$/);
|
|
1784
|
+
if (!match) {
|
|
1785
|
+
return '/';
|
|
1786
|
+
}
|
|
1787
|
+
const drive = match[1].toUpperCase();
|
|
1788
|
+
const parts = match[2].split('/').filter(Boolean);
|
|
1789
|
+
if (!parts.length) {
|
|
1790
|
+
return '/';
|
|
1791
|
+
}
|
|
1792
|
+
parts.pop();
|
|
1793
|
+
return parts.length
|
|
1794
|
+
? `/${drive}:/${parts.join('/')}`
|
|
1795
|
+
: `/${drive}:/`;
|
|
1796
|
+
}
|
|
1797
|
+
const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(p);
|
|
1798
|
+
return next === '.' ? '/' : next;
|
|
1799
|
+
}
|
|
1601
1800
|
loadRecentProfiles() {
|
|
1602
1801
|
try {
|
|
1603
1802
|
const rec = this.profilesService.getRecentProfiles?.();
|
|
@@ -2494,7 +2693,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
2494
2693
|
/>
|
|
2495
2694
|
</div>
|
|
2496
2695
|
<div class="pane-actions">
|
|
2497
|
-
<button (click)="remoteUp()" [disabled]="!connected ||
|
|
2696
|
+
<button (click)="remoteUp()" [disabled]="!connected || !canRemoteUp()">Up</button>
|
|
2498
2697
|
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
2499
2698
|
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2500
2699
|
</div>
|
|
@@ -2532,7 +2731,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
2532
2731
|
</div>
|
|
2533
2732
|
<div
|
|
2534
2733
|
class="entry"
|
|
2535
|
-
*ngIf="connected &&
|
|
2734
|
+
*ngIf="connected && canRemoteUp()"
|
|
2536
2735
|
(dblclick)="remoteUp()"
|
|
2537
2736
|
>
|
|
2538
2737
|
<span class="icon">⬆</span>
|
|
@@ -2969,6 +3168,7 @@ let SftpUiService = class SftpUiService {
|
|
|
2969
3168
|
inputs: {
|
|
2970
3169
|
sshSession,
|
|
2971
3170
|
profile,
|
|
3171
|
+
shellSession: sourceTab?.session ?? null,
|
|
2972
3172
|
},
|
|
2973
3173
|
});
|
|
2974
3174
|
tab.setTitle(`${baseTitle} + SFTP`);
|