tabby-sftp-ui 0.2.0 → 0.2.1
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 +6 -0
- package/dist/index.js +333 -57
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,12 @@ Then restart Tabby.
|
|
|
126
126
|
|
|
127
127
|
### Changelog
|
|
128
128
|
|
|
129
|
+
- See [`CHANGELOG.md`](CHANGELOG.md)
|
|
130
|
+
|
|
131
|
+
- **0.2.1**
|
|
132
|
+
- Fix: New Folder/Rename input dialog
|
|
133
|
+
- Fix: Robust remote file edit sync-back
|
|
134
|
+
- Feature: OS drag-and-drop into panes
|
|
129
135
|
- **0.2.0**
|
|
130
136
|
- UI: removed the main toolbar SFTP icon next to Settings (terminal button remains).
|
|
131
137
|
- **0.1.0**
|
package/dist/index.js
CHANGED
|
@@ -229,12 +229,14 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
229
229
|
/* harmony import */ var fs_promises__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs_promises__WEBPACK_IMPORTED_MODULE_2__);
|
|
230
230
|
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! fs */ "fs");
|
|
231
231
|
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_3__);
|
|
232
|
-
/* harmony import */ var
|
|
233
|
-
/* harmony import */ var
|
|
234
|
-
/* harmony import */ var
|
|
235
|
-
/* harmony import */ var
|
|
236
|
-
/* harmony import */ var
|
|
237
|
-
/* harmony import */ var
|
|
232
|
+
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! crypto */ "crypto");
|
|
233
|
+
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_4__);
|
|
234
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
235
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_5__);
|
|
236
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
237
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_6__);
|
|
238
|
+
/* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
|
|
239
|
+
/* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
|
|
238
240
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
239
241
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
240
242
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -252,7 +254,8 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
252
254
|
|
|
253
255
|
|
|
254
256
|
|
|
255
|
-
|
|
257
|
+
|
|
258
|
+
let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_6__.BaseTabComponent {
|
|
256
259
|
constructor(injector, sftp, profilesService, app) {
|
|
257
260
|
// Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
|
|
258
261
|
// @ts-expect-error runtime-compatible super(injector)
|
|
@@ -306,6 +309,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
306
309
|
this.deleteConfirmText = '';
|
|
307
310
|
this.pendingLocalDelete = [];
|
|
308
311
|
this.pendingRemoteDelete = [];
|
|
312
|
+
this.inputDialogVisible = false;
|
|
313
|
+
this.inputDialogTitle = '';
|
|
314
|
+
this.inputDialogPlaceholder = '';
|
|
315
|
+
this.inputDialogValue = '';
|
|
316
|
+
this.inputDialogMode = null;
|
|
317
|
+
this.inputDialogTargetPath = null;
|
|
318
|
+
this.inputDialogRemotePath = null;
|
|
309
319
|
this.openedRemoteFiles = new Map();
|
|
310
320
|
this.localPathPresets = [];
|
|
311
321
|
this.localFavorites = [];
|
|
@@ -314,7 +324,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
314
324
|
this.localMenuX = 0;
|
|
315
325
|
this.localMenuY = 0;
|
|
316
326
|
this.localMenuItems = [];
|
|
317
|
-
this.platform = injector.get(
|
|
327
|
+
this.platform = injector.get(tabby_core__WEBPACK_IMPORTED_MODULE_6__.PlatformService);
|
|
318
328
|
// build local path presets (similar to Termius quick locations)
|
|
319
329
|
const home = os__WEBPACK_IMPORTED_MODULE_1__.homedir();
|
|
320
330
|
this.localPathPresets.push({ id: 'home', label: 'Home', path: home });
|
|
@@ -621,6 +631,25 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
621
631
|
if (!this.sftpSession) {
|
|
622
632
|
return;
|
|
623
633
|
}
|
|
634
|
+
// Drag & drop from OS file manager (Explorer/Finder) into the remote pane
|
|
635
|
+
const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
|
|
636
|
+
if (dtFiles.length) {
|
|
637
|
+
const paths = dtFiles
|
|
638
|
+
.map(f => f.path)
|
|
639
|
+
.filter((p) => Boolean(p));
|
|
640
|
+
if (paths.length) {
|
|
641
|
+
try {
|
|
642
|
+
for (const p of paths) {
|
|
643
|
+
await this.uploadLocalPathToRemote(this.remotePath, p);
|
|
644
|
+
}
|
|
645
|
+
await this.refreshRemote();
|
|
646
|
+
}
|
|
647
|
+
catch (e) {
|
|
648
|
+
console.error('[SFTP-UI] Upload from OS drop failed', e);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
624
653
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
625
654
|
if (!raw) {
|
|
626
655
|
return;
|
|
@@ -637,7 +666,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
637
666
|
}
|
|
638
667
|
try {
|
|
639
668
|
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
640
|
-
const upload = new
|
|
669
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
641
670
|
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
642
671
|
await this.sftpSession.upload(targetRemotePath, upload);
|
|
643
672
|
await this.refreshRemote();
|
|
@@ -649,6 +678,25 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
649
678
|
async onDropOnLocal(ev) {
|
|
650
679
|
ev.preventDefault();
|
|
651
680
|
this.localDropActive = false;
|
|
681
|
+
// Drag & drop from OS file manager into the local pane (copy into current local folder)
|
|
682
|
+
const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
|
|
683
|
+
if (dtFiles.length) {
|
|
684
|
+
const paths = dtFiles
|
|
685
|
+
.map(f => f.path)
|
|
686
|
+
.filter((p) => Boolean(p));
|
|
687
|
+
if (paths.length) {
|
|
688
|
+
try {
|
|
689
|
+
for (const p of paths) {
|
|
690
|
+
await this.copyLocalPathIntoLocalDir(this.localPath, p);
|
|
691
|
+
}
|
|
692
|
+
await this.refreshLocal();
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
console.error('[SFTP-UI] Local copy from OS drop failed', e);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
652
700
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
653
701
|
if (!raw) {
|
|
654
702
|
return;
|
|
@@ -668,7 +716,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
668
716
|
if (!this.sftpSession) {
|
|
669
717
|
throw new Error('Not connected');
|
|
670
718
|
}
|
|
671
|
-
const dl = new
|
|
719
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
|
|
672
720
|
this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
|
|
673
721
|
await this.sftpSession.download(payload.remotePath, dl);
|
|
674
722
|
await this.refreshLocal();
|
|
@@ -677,6 +725,51 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
677
725
|
console.error('[SFTP-UI] Download failed', e);
|
|
678
726
|
}
|
|
679
727
|
}
|
|
728
|
+
async uploadLocalPathToRemote(remoteDir, localPath) {
|
|
729
|
+
if (!this.sftpSession) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
|
|
733
|
+
if (!st) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(localPath);
|
|
737
|
+
const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, baseName);
|
|
738
|
+
if (st.isDirectory()) {
|
|
739
|
+
// Ensure destination folder exists, then recursively upload children
|
|
740
|
+
try {
|
|
741
|
+
await this.sftpSession.mkdir(remoteTarget);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
// ignore (might already exist)
|
|
745
|
+
}
|
|
746
|
+
const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(localPath);
|
|
747
|
+
for (const child of children) {
|
|
748
|
+
await this.uploadLocalPathToRemote(remoteTarget, path__WEBPACK_IMPORTED_MODULE_0__.join(localPath, child));
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
|
|
753
|
+
this.trackTransfer(upload, 'upload', remoteTarget, localPath);
|
|
754
|
+
await this.sftpSession.upload(remoteTarget, upload);
|
|
755
|
+
}
|
|
756
|
+
async copyLocalPathIntoLocalDir(destDir, srcPath) {
|
|
757
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(srcPath).catch(() => null);
|
|
758
|
+
if (!st) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(srcPath);
|
|
762
|
+
const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(destDir, baseName);
|
|
763
|
+
if (st.isDirectory()) {
|
|
764
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(destPath, { recursive: true });
|
|
765
|
+
const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(srcPath);
|
|
766
|
+
for (const child of children) {
|
|
767
|
+
await this.copyLocalPathIntoLocalDir(destPath, path__WEBPACK_IMPORTED_MODULE_0__.join(srcPath, child));
|
|
768
|
+
}
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
|
|
772
|
+
}
|
|
680
773
|
getFilteredLocalEntries() {
|
|
681
774
|
const entriesRef = this.localEntries;
|
|
682
775
|
const filter = this.localFilter;
|
|
@@ -1215,18 +1308,17 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1215
1308
|
this.localMenuVisible = false;
|
|
1216
1309
|
}
|
|
1217
1310
|
localRename() {
|
|
1218
|
-
if (this.selectedLocal.length !== 1
|
|
1311
|
+
if (this.selectedLocal.length !== 1) {
|
|
1219
1312
|
return;
|
|
1220
1313
|
}
|
|
1221
1314
|
const entry = this.selectedLocal[0];
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
.catch(e => console.error('[SFTP-UI] Local rename failed', e));
|
|
1315
|
+
this.openInputDialog({
|
|
1316
|
+
mode: 'local-rename',
|
|
1317
|
+
title: 'Rename (local)',
|
|
1318
|
+
placeholder: 'New name',
|
|
1319
|
+
value: entry.name,
|
|
1320
|
+
targetPath: entry.fullPath,
|
|
1321
|
+
});
|
|
1230
1322
|
}
|
|
1231
1323
|
localDelete() {
|
|
1232
1324
|
if (!this.selectedLocal.length) {
|
|
@@ -1239,14 +1331,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1239
1331
|
this.deleteConfirmVisible = true;
|
|
1240
1332
|
}
|
|
1241
1333
|
localNewFolder() {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
.catch(e => console.error('[SFTP-UI] Local mkdir failed', e));
|
|
1334
|
+
this.openInputDialog({
|
|
1335
|
+
mode: 'local-new-folder',
|
|
1336
|
+
title: 'New folder (local)',
|
|
1337
|
+
placeholder: 'Folder name',
|
|
1338
|
+
value: 'New folder',
|
|
1339
|
+
targetPath: this.localPath,
|
|
1340
|
+
});
|
|
1250
1341
|
}
|
|
1251
1342
|
localEditPermissions() {
|
|
1252
1343
|
if (this.selectedLocal.length !== 1 || !this.localActionPerms?.trim()) {
|
|
@@ -1268,18 +1359,18 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1268
1359
|
}
|
|
1269
1360
|
}
|
|
1270
1361
|
remoteRename() {
|
|
1271
|
-
if (this.selectedRemote.length !== 1 || !this.
|
|
1362
|
+
if (this.selectedRemote.length !== 1 || !this.sftpSession) {
|
|
1272
1363
|
return;
|
|
1273
1364
|
}
|
|
1274
1365
|
const entry = this.selectedRemote[0];
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1366
|
+
this.openInputDialog({
|
|
1367
|
+
mode: 'remote-rename',
|
|
1368
|
+
title: 'Rename (remote)',
|
|
1369
|
+
placeholder: 'New name',
|
|
1370
|
+
value: entry.name,
|
|
1371
|
+
remotePath: entry.fullPath,
|
|
1372
|
+
targetPath: this.remotePath,
|
|
1373
|
+
});
|
|
1283
1374
|
}
|
|
1284
1375
|
remoteDelete() {
|
|
1285
1376
|
if (!this.selectedRemote.length || !this.sftpSession) {
|
|
@@ -1295,14 +1386,87 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1295
1386
|
if (!this.sftpSession) {
|
|
1296
1387
|
return;
|
|
1297
1388
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1389
|
+
this.openInputDialog({
|
|
1390
|
+
mode: 'remote-new-folder',
|
|
1391
|
+
title: 'New folder (remote)',
|
|
1392
|
+
placeholder: 'Folder name',
|
|
1393
|
+
value: 'New folder',
|
|
1394
|
+
targetPath: this.remotePath,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
openInputDialog(opts) {
|
|
1398
|
+
this.inputDialogMode = opts.mode;
|
|
1399
|
+
this.inputDialogTitle = opts.title;
|
|
1400
|
+
this.inputDialogPlaceholder = opts.placeholder;
|
|
1401
|
+
this.inputDialogValue = opts.value;
|
|
1402
|
+
this.inputDialogTargetPath = opts.targetPath;
|
|
1403
|
+
this.inputDialogRemotePath = opts.remotePath ?? null;
|
|
1404
|
+
this.inputDialogVisible = true;
|
|
1405
|
+
}
|
|
1406
|
+
cancelInputDialog() {
|
|
1407
|
+
this.inputDialogVisible = false;
|
|
1408
|
+
this.inputDialogMode = null;
|
|
1409
|
+
this.inputDialogTitle = '';
|
|
1410
|
+
this.inputDialogPlaceholder = '';
|
|
1411
|
+
this.inputDialogValue = '';
|
|
1412
|
+
this.inputDialogTargetPath = null;
|
|
1413
|
+
this.inputDialogRemotePath = null;
|
|
1414
|
+
}
|
|
1415
|
+
async confirmInputDialog() {
|
|
1416
|
+
if (!this.inputDialogVisible || !this.inputDialogMode) {
|
|
1300
1417
|
return;
|
|
1301
1418
|
}
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1419
|
+
const mode = this.inputDialogMode;
|
|
1420
|
+
const value = this.inputDialogValue.trim();
|
|
1421
|
+
const targetPath = this.inputDialogTargetPath;
|
|
1422
|
+
const remotePath = this.inputDialogRemotePath;
|
|
1423
|
+
this.cancelInputDialog();
|
|
1424
|
+
if (!value || !targetPath) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
if (mode === 'local-new-folder') {
|
|
1429
|
+
const dir = targetPath;
|
|
1430
|
+
const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.join(dir, value);
|
|
1431
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(folderPath, { recursive: true });
|
|
1432
|
+
await this.refreshLocal();
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (mode === 'local-rename') {
|
|
1436
|
+
const from = targetPath;
|
|
1437
|
+
const to = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, value);
|
|
1438
|
+
if (path__WEBPACK_IMPORTED_MODULE_0__.basename(from) === value) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(from, to);
|
|
1442
|
+
await this.refreshLocal();
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (mode === 'remote-new-folder') {
|
|
1446
|
+
if (!this.sftpSession) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const dir = targetPath;
|
|
1450
|
+
const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(dir, value);
|
|
1451
|
+
await this.sftpSession.mkdir(folderPath);
|
|
1452
|
+
await this.refreshRemote();
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (mode === 'remote-rename') {
|
|
1456
|
+
if (!this.sftpSession || !remotePath) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const to = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, value);
|
|
1460
|
+
if (path__WEBPACK_IMPORTED_MODULE_0__.posix.basename(remotePath) === value) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
await this.sftpSession.rename(remotePath, to);
|
|
1464
|
+
await this.refreshRemote();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch (e) {
|
|
1468
|
+
console.error('[SFTP-UI] Input dialog action failed', e);
|
|
1469
|
+
}
|
|
1306
1470
|
}
|
|
1307
1471
|
remoteEditPermissions() {
|
|
1308
1472
|
if (this.selectedRemote.length !== 1 || !this.remoteActionPerms?.trim() || !this.sftpSession) {
|
|
@@ -1332,7 +1496,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1332
1496
|
continue;
|
|
1333
1497
|
}
|
|
1334
1498
|
const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, entry.name);
|
|
1335
|
-
const dl = new
|
|
1499
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
|
|
1336
1500
|
this.trackTransfer(dl, 'download', entry.fullPath, targetLocalPath);
|
|
1337
1501
|
void this.sftpSession.download(entry.fullPath, dl)
|
|
1338
1502
|
.then(() => this.refreshLocal())
|
|
@@ -1503,6 +1667,18 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1503
1667
|
}
|
|
1504
1668
|
}
|
|
1505
1669
|
onKeyDown(event) {
|
|
1670
|
+
if (event.key === 'Escape') {
|
|
1671
|
+
if (this.inputDialogVisible) {
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
this.cancelInputDialog();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (this.deleteConfirmVisible) {
|
|
1677
|
+
event.preventDefault();
|
|
1678
|
+
this.cancelDelete();
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1506
1682
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
1507
1683
|
event.preventDefault();
|
|
1508
1684
|
if (this.selectedRemote.length) {
|
|
@@ -1675,7 +1851,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1675
1851
|
try {
|
|
1676
1852
|
const tmpRoot = path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'tabby-sftp-ui');
|
|
1677
1853
|
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(tmpRoot, { recursive: true });
|
|
1678
|
-
const
|
|
1854
|
+
const hash = crypto__WEBPACK_IMPORTED_MODULE_4__.createHash('sha1').update(entry.fullPath).digest('hex').slice(0, 10);
|
|
1855
|
+
const safeName = entry.name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
|
|
1856
|
+
const localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, `${hash}-${safeName}`);
|
|
1679
1857
|
// если уже есть watcher на этот файл – закроем его и перезапишем
|
|
1680
1858
|
const existing = this.openedRemoteFiles.get(localPath);
|
|
1681
1859
|
if (existing?.watcher) {
|
|
@@ -1686,22 +1864,72 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1686
1864
|
// ignore
|
|
1687
1865
|
}
|
|
1688
1866
|
}
|
|
1689
|
-
|
|
1867
|
+
if (existing?.debounceTimer != null) {
|
|
1868
|
+
try {
|
|
1869
|
+
clearTimeout(existing.debounceTimer);
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
// ignore
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(localPath, entry.mode, entry.size);
|
|
1690
1876
|
this.trackTransfer(dl, 'download', entry.fullPath, localPath);
|
|
1691
1877
|
await this.sftpSession.download(entry.fullPath, dl);
|
|
1692
1878
|
// настроим наблюдение за изменениями локального файла
|
|
1879
|
+
const schedule = () => this.scheduleSyncBackRemoteFile(localPath);
|
|
1693
1880
|
const watcher = fs__WEBPACK_IMPORTED_MODULE_3__.watch(localPath, { persistent: false }, (eventType) => {
|
|
1694
|
-
|
|
1695
|
-
|
|
1881
|
+
// Many editors save atomically (rename) or emit multiple change events.
|
|
1882
|
+
if (eventType === 'change' || eventType === 'rename') {
|
|
1883
|
+
schedule();
|
|
1696
1884
|
}
|
|
1697
1885
|
});
|
|
1698
|
-
this.openedRemoteFiles.set(localPath, {
|
|
1886
|
+
this.openedRemoteFiles.set(localPath, {
|
|
1887
|
+
remotePath: entry.fullPath,
|
|
1888
|
+
mode: entry.mode,
|
|
1889
|
+
watcher,
|
|
1890
|
+
debounceTimer: null,
|
|
1891
|
+
syncing: false,
|
|
1892
|
+
pending: false,
|
|
1893
|
+
lastUploadedSignature: null,
|
|
1894
|
+
});
|
|
1699
1895
|
this.platform.openPath(localPath);
|
|
1700
1896
|
}
|
|
1701
1897
|
catch (e) {
|
|
1702
1898
|
console.error('[SFTP-UI] Open remote file failed', e);
|
|
1703
1899
|
}
|
|
1704
1900
|
}
|
|
1901
|
+
scheduleSyncBackRemoteFile(localPath) {
|
|
1902
|
+
const info = this.openedRemoteFiles.get(localPath);
|
|
1903
|
+
if (!info) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (info.debounceTimer != null) {
|
|
1907
|
+
clearTimeout(info.debounceTimer);
|
|
1908
|
+
}
|
|
1909
|
+
// Debounce a burst of editor save events
|
|
1910
|
+
info.debounceTimer = window.setTimeout(() => {
|
|
1911
|
+
info.debounceTimer = null;
|
|
1912
|
+
void this.syncBackRemoteFile(localPath);
|
|
1913
|
+
}, 650);
|
|
1914
|
+
}
|
|
1915
|
+
async waitForStableLocalFile(localPath) {
|
|
1916
|
+
// Wait until the file stops changing (editors often write in multiple passes)
|
|
1917
|
+
let last = null;
|
|
1918
|
+
for (let i = 0; i < 10; i++) {
|
|
1919
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
|
|
1920
|
+
if (!st || !st.isFile()) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
const cur = { size: st.size, mtimeMs: st.mtimeMs };
|
|
1924
|
+
if (last && cur.size === last.size && cur.mtimeMs === last.mtimeMs) {
|
|
1925
|
+
// stable for one interval
|
|
1926
|
+
return cur;
|
|
1927
|
+
}
|
|
1928
|
+
last = cur;
|
|
1929
|
+
await new Promise(resolve => setTimeout(resolve, 180));
|
|
1930
|
+
}
|
|
1931
|
+
return last;
|
|
1932
|
+
}
|
|
1705
1933
|
async syncBackRemoteFile(localPath) {
|
|
1706
1934
|
if (!this.sftpSession || !this.connected) {
|
|
1707
1935
|
return;
|
|
@@ -1710,25 +1938,46 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1710
1938
|
if (!info) {
|
|
1711
1939
|
return;
|
|
1712
1940
|
}
|
|
1941
|
+
if (info.syncing) {
|
|
1942
|
+
info.pending = true;
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
info.syncing = true;
|
|
1713
1946
|
try {
|
|
1714
|
-
const
|
|
1947
|
+
const stable = await this.waitForStableLocalFile(localPath);
|
|
1948
|
+
if (!stable) {
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
const signature = `${stable.size}:${stable.mtimeMs}`;
|
|
1952
|
+
if (info.lastUploadedSignature === signature) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
|
|
1715
1956
|
this.trackTransfer(upload, 'upload', info.remotePath, localPath);
|
|
1716
1957
|
await this.sftpSession.upload(info.remotePath, upload);
|
|
1958
|
+
info.lastUploadedSignature = signature;
|
|
1717
1959
|
await this.refreshRemote();
|
|
1718
1960
|
}
|
|
1719
1961
|
catch (e) {
|
|
1720
1962
|
console.error('[SFTP-UI] Sync-back remote file failed', e);
|
|
1721
1963
|
}
|
|
1964
|
+
finally {
|
|
1965
|
+
info.syncing = false;
|
|
1966
|
+
if (info.pending) {
|
|
1967
|
+
info.pending = false;
|
|
1968
|
+
this.scheduleSyncBackRemoteFile(localPath);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1722
1971
|
}
|
|
1723
1972
|
};
|
|
1724
1973
|
__decorate([
|
|
1725
|
-
(0,
|
|
1974
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.HostListener)('document:click'),
|
|
1726
1975
|
__metadata("design:type", Function),
|
|
1727
1976
|
__metadata("design:paramtypes", []),
|
|
1728
1977
|
__metadata("design:returntype", void 0)
|
|
1729
1978
|
], SftpManagerTabComponent.prototype, "onDocumentClick", null);
|
|
1730
1979
|
SftpManagerTabComponent = __decorate([
|
|
1731
|
-
(0,
|
|
1980
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
|
|
1732
1981
|
selector: 'tabby-sftp-manager-tab',
|
|
1733
1982
|
template: `
|
|
1734
1983
|
<div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
|
|
@@ -1842,7 +2091,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
1842
2091
|
<input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
|
|
1843
2092
|
</div>
|
|
1844
2093
|
<div class="action-buttons">
|
|
1845
|
-
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1
|
|
2094
|
+
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
|
|
1846
2095
|
<button (click)="refreshLocal()">Refresh</button>
|
|
1847
2096
|
<button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
|
|
1848
2097
|
<button (click)="localNewFolder()">New Folder</button>
|
|
@@ -1943,7 +2192,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
1943
2192
|
<input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
|
|
1944
2193
|
</div>
|
|
1945
2194
|
<div class="action-buttons">
|
|
1946
|
-
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1
|
|
2195
|
+
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
|
|
1947
2196
|
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
1948
2197
|
<button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
|
|
1949
2198
|
<button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
|
|
@@ -1991,6 +2240,22 @@ SftpManagerTabComponent = __decorate([
|
|
|
1991
2240
|
</div>
|
|
1992
2241
|
</div>
|
|
1993
2242
|
|
|
2243
|
+
<div class="delete-overlay" *ngIf="inputDialogVisible">
|
|
2244
|
+
<div class="delete-dialog" (click)="$event.stopPropagation()">
|
|
2245
|
+
<div class="delete-text">{{ inputDialogTitle }}</div>
|
|
2246
|
+
<input
|
|
2247
|
+
class="dialog-input"
|
|
2248
|
+
[(ngModel)]="inputDialogValue"
|
|
2249
|
+
[placeholder]="inputDialogPlaceholder"
|
|
2250
|
+
(keyup.enter)="confirmInputDialog()"
|
|
2251
|
+
/>
|
|
2252
|
+
<div class="delete-buttons">
|
|
2253
|
+
<button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
|
|
2254
|
+
<button (click)="cancelInputDialog()">Cancel</button>
|
|
2255
|
+
</div>
|
|
2256
|
+
</div>
|
|
2257
|
+
</div>
|
|
2258
|
+
|
|
1994
2259
|
<div
|
|
1995
2260
|
class="local-menu"
|
|
1996
2261
|
*ngIf="localMenuVisible"
|
|
@@ -2070,6 +2335,7 @@ SftpManagerTabComponent = __decorate([
|
|
|
2070
2335
|
.delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
|
|
2071
2336
|
.delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
|
|
2072
2337
|
.delete-text { font-size: 13px; }
|
|
2338
|
+
.dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
|
|
2073
2339
|
.delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2074
2340
|
.delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
|
|
2075
2341
|
.local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
|
|
@@ -2077,10 +2343,10 @@ SftpManagerTabComponent = __decorate([
|
|
|
2077
2343
|
.local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
|
|
2078
2344
|
`],
|
|
2079
2345
|
}),
|
|
2080
|
-
__metadata("design:paramtypes", [
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2346
|
+
__metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
|
|
2347
|
+
_sftp_service__WEBPACK_IMPORTED_MODULE_8__.SftpConnectionService,
|
|
2348
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_6__.ProfilesService,
|
|
2349
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_6__.AppService])
|
|
2084
2350
|
], SftpManagerTabComponent);
|
|
2085
2351
|
|
|
2086
2352
|
|
|
@@ -2314,6 +2580,16 @@ SftpConnectionService = __decorate([
|
|
|
2314
2580
|
|
|
2315
2581
|
|
|
2316
2582
|
|
|
2583
|
+
/***/ },
|
|
2584
|
+
|
|
2585
|
+
/***/ "crypto"
|
|
2586
|
+
/*!*************************!*\
|
|
2587
|
+
!*** external "crypto" ***!
|
|
2588
|
+
\*************************/
|
|
2589
|
+
(module) {
|
|
2590
|
+
|
|
2591
|
+
module.exports = require("crypto");
|
|
2592
|
+
|
|
2317
2593
|
/***/ },
|
|
2318
2594
|
|
|
2319
2595
|
/***/ "fs/promises"
|