mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-24 02:53:43 +03:00
sftp: allow editing remote files locally - fixes #4311
This commit is contained in:
parent
923b559857
commit
3b09dfa145
@ -22,6 +22,7 @@
|
||||
min-width: 0;
|
||||
margin-right: auto;
|
||||
margin-bottom: 3px;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
|
@ -20,7 +20,8 @@
|
||||
"@angular/core": "^9.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"winston": "^3.3.3",
|
||||
"electron-promise-ipc": "^2.2.4"
|
||||
"electron-promise-ipc": "^2.2.4",
|
||||
"tmp-promise": "^3.0.2",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
|
||||
import { TerminalColorSchemeProvider } from 'tabby-terminal'
|
||||
import { SFTPContextMenuItemProvider } from 'tabby-ssh'
|
||||
|
||||
import { HyperColorSchemes } from './colorSchemes'
|
||||
import { ElectronPlatformService } from './services/platform.service'
|
||||
@ -14,11 +15,12 @@ import { ElectronHostAppService } from './services/hostApp.service'
|
||||
import { ElectronService } from './services/electron.service'
|
||||
import { ElectronHotkeyProvider } from './hotkeys'
|
||||
import { ElectronConfigProvider } from './config'
|
||||
import { EditSFTPContextMenu } from './sftpContextMenu'
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||
{ provide: PlatformService, useClass: ElectronPlatformService },
|
||||
{ provide: PlatformService, useExisting: ElectronPlatformService },
|
||||
{ provide: HostWindowService, useExisting: ElectronHostWindow },
|
||||
{ provide: HostAppService, useExisting: ElectronHostAppService },
|
||||
{ provide: LogService, useClass: ElectronLogService },
|
||||
@ -27,6 +29,7 @@ import { ElectronConfigProvider } from './config'
|
||||
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
|
||||
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
|
||||
{ provide: SFTPContextMenuItemProvider, useClass: EditSFTPContextMenu, multi: true },
|
||||
],
|
||||
})
|
||||
export default class ElectronModule {
|
||||
|
@ -20,7 +20,7 @@ try {
|
||||
var wnr = require('windows-native-registry')
|
||||
} catch { }
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ElectronPlatformService extends PlatformService {
|
||||
supportsWindowControls = true
|
||||
private configPath: string
|
||||
@ -189,7 +189,7 @@ export class ElectronPlatformService extends PlatformService {
|
||||
this.electron.app.exit(0)
|
||||
}
|
||||
|
||||
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
||||
async startUpload (options?: FileUploadOptions, paths?: string[]): Promise<FileUpload[]> {
|
||||
options ??= { multiple: false }
|
||||
|
||||
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
|
||||
@ -197,18 +197,21 @@ export class ElectronPlatformService extends PlatformService {
|
||||
properties.push('multiSelections')
|
||||
}
|
||||
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Select',
|
||||
properties,
|
||||
},
|
||||
)
|
||||
if (result.canceled) {
|
||||
return []
|
||||
if (!paths) {
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Select',
|
||||
properties,
|
||||
},
|
||||
)
|
||||
if (result.canceled) {
|
||||
return []
|
||||
}
|
||||
paths = result.filePaths
|
||||
}
|
||||
|
||||
return Promise.all(result.filePaths.map(async p => {
|
||||
return Promise.all(paths.map(async p => {
|
||||
const transfer = new ElectronFileUpload(p, this.electron)
|
||||
await wrapPromise(this.zone, transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
@ -216,17 +219,20 @@ export class ElectronPlatformService extends PlatformService {
|
||||
}))
|
||||
}
|
||||
|
||||
async startDownload (name: string, mode: number, size: number): Promise<FileDownload|null> {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
defaultPath: name,
|
||||
},
|
||||
)
|
||||
if (!result.filePath) {
|
||||
return null
|
||||
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {
|
||||
if (!filePath) {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
defaultPath: name,
|
||||
},
|
||||
)
|
||||
if (!result.filePath) {
|
||||
return null
|
||||
}
|
||||
filePath = result.filePath
|
||||
}
|
||||
const transfer = new ElectronFileDownload(result.filePath, mode, size, this.electron)
|
||||
const transfer = new ElectronFileDownload(filePath, mode, size, this.electron)
|
||||
await wrapPromise(this.zone, transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
return transfer
|
||||
|
59
tabby-electron/src/sftpContextMenu.ts
Normal file
59
tabby-electron/src/sftpContextMenu.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import * as tmp from 'tmp-promise'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { Subject, debounceTime, debounce } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { MenuItemOptions } from 'tabby-core'
|
||||
import { SFTPFile, SFTPPanelComponent, SFTPContextMenuItemProvider, SFTPSession } from 'tabby-ssh'
|
||||
import { ElectronPlatformService } from './services/platform.service'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
constructor (
|
||||
private platform: ElectronPlatformService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||
if (item.isDirectory) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
click: () => this.edit(item, panel.sftp),
|
||||
label: 'Edit locally',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private async edit (item: SFTPFile, sftp: SFTPSession) {
|
||||
const tempDir = (await tmp.dir({ unsafeCleanup: true })).path
|
||||
const tempPath = path.join(tempDir, item.name)
|
||||
const transfer = await this.platform.startDownload(item.name, item.mode, item.size, tempPath)
|
||||
if (!transfer) {
|
||||
return
|
||||
}
|
||||
await sftp.download(item.fullPath, transfer)
|
||||
this.platform.openPath(tempPath)
|
||||
|
||||
const events = new Subject<string>()
|
||||
const watcher = fs.watch(tempPath, event => events.next(event))
|
||||
events.pipe(debounceTime(1000), debounce(async event => {
|
||||
if (event === 'rename') {
|
||||
watcher.close()
|
||||
}
|
||||
const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
|
||||
if (!upload.length) {
|
||||
return
|
||||
}
|
||||
sftp.upload(item.fullPath, upload[0])
|
||||
})).subscribe()
|
||||
watcher.on('close', () => events.complete())
|
||||
sftp.closed$.subscribe(() => watcher.close())
|
||||
}
|
||||
}
|
@ -16,6 +16,19 @@ async@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
||||
@ -70,6 +83,11 @@ colorspace@1.1.x:
|
||||
color "3.0.x"
|
||||
text-hex "1.0.x"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
@ -143,6 +161,11 @@ fn.name@1.x.x:
|
||||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
@ -157,6 +180,18 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.7"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
|
||||
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
has-bigints@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
||||
@ -174,7 +209,15 @@ has@^1.0.3:
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
inherits@^2.0.3, inherits@~2.0.3:
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@ -267,6 +310,13 @@ logform@^2.2.0:
|
||||
ms "^2.1.1"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
@ -302,6 +352,13 @@ object.entries@^1.1.3:
|
||||
es-abstract "^1.18.0-next.1"
|
||||
has "^1.0.3"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
one-time@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
|
||||
@ -309,6 +366,11 @@ one-time@^1.0.0:
|
||||
dependencies:
|
||||
fn.name "1.x.x"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
@ -336,6 +398,13 @@ readable-stream@^3.4.0:
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
@ -400,6 +469,20 @@ text-hex@1.0.x:
|
||||
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
||||
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
||||
|
||||
tmp-promise@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a"
|
||||
integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==
|
||||
dependencies:
|
||||
tmp "^0.2.0"
|
||||
|
||||
tmp@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
|
||||
triple-beam@^1.2.0, triple-beam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
||||
@ -463,3 +546,8 @@ winston@^3.3.3:
|
||||
stack-trace "0.0.x"
|
||||
triple-beam "^1.3.0"
|
||||
winston-transport "^4.4.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
|
||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
|
||||
|
||||
export const supportedAlgorithms: Record<string, string> = {}
|
||||
|
||||
|
12
tabby-ssh/src/api/contextMenu.ts
Normal file
12
tabby-ssh/src/api/contextMenu.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { MenuItemOptions } from 'tabby-core'
|
||||
import { SFTPFile } from '../session/sftp'
|
||||
import { SFTPPanelComponent } from '../components/sftpPanel.component'
|
||||
|
||||
/**
|
||||
* Extend to add items to the SFTPPanel context menu
|
||||
*/
|
||||
export abstract class SFTPContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
abstract getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]>
|
||||
}
|
2
tabby-ssh/src/api/index.ts
Normal file
2
tabby-ssh/src/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './contextMenu'
|
||||
export * from './interfaces'
|
53
tabby-ssh/src/api/interfaces.ts
Normal file
53
tabby-ssh/src/api/interfaces.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Profile } from 'tabby-core'
|
||||
import { LoginScriptsOptions } from 'tabby-terminal'
|
||||
|
||||
export enum SSHAlgorithmType {
|
||||
HMAC = 'hmac',
|
||||
KEX = 'kex',
|
||||
CIPHER = 'cipher',
|
||||
HOSTKEY = 'serverHostKey',
|
||||
}
|
||||
|
||||
export interface SSHProfile extends Profile {
|
||||
options: SSHProfileOptions
|
||||
}
|
||||
|
||||
export interface SSHProfileOptions extends LoginScriptsOptions {
|
||||
host: string
|
||||
port?: number
|
||||
user: string
|
||||
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
|
||||
password?: string
|
||||
privateKeys?: string[]
|
||||
keepaliveInterval?: number
|
||||
keepaliveCountMax?: number
|
||||
readyTimeout?: number
|
||||
x11?: boolean
|
||||
skipBanner?: boolean
|
||||
jumpHost?: string
|
||||
agentForward?: boolean
|
||||
warnOnClose?: boolean
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
forwardedPorts?: ForwardedPortConfig[]
|
||||
}
|
||||
|
||||
export enum PortForwardType {
|
||||
Local = 'Local',
|
||||
Remote = 'Remote',
|
||||
Dynamic = 'Dynamic',
|
||||
}
|
||||
|
||||
export interface ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host: string
|
||||
port: number
|
||||
targetAddress: string
|
||||
targetPort: number
|
||||
}
|
||||
|
||||
export const ALGORITHM_BLACKLIST = [
|
||||
// cause native crashes in node crypto, use EC instead
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group-exchange-sha1',
|
||||
]
|
@ -1,18 +1,16 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SSHSession } from '../api'
|
||||
import { SFTPSession, SFTPFile } from '../session/sftp'
|
||||
import { posix as path } from 'path'
|
||||
import * as C from 'constants'
|
||||
import { FileUpload, PlatformService } from 'tabby-core'
|
||||
import { SFTPDeleteModalComponent } from './sftpDeleteModal.component'
|
||||
import { posix as path } from 'path'
|
||||
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
|
||||
import { FileUpload, MenuItemOptions, PlatformService } from 'tabby-core'
|
||||
import { SFTPSession, SFTPFile } from '../session/sftp'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { SFTPContextMenuItemProvider } from '../api'
|
||||
|
||||
interface PathSegment {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'sftp-panel',
|
||||
template: require('./sftpPanel.component.pug'),
|
||||
@ -29,8 +27,10 @@ export class SFTPPanelComponent {
|
||||
|
||||
constructor (
|
||||
private platform: PlatformService,
|
||||
private ngbModal: NgbModal,
|
||||
) { }
|
||||
@Optional() @Inject(SFTPContextMenuItemProvider) protected contextMenuProviders: SFTPContextMenuItemProvider[],
|
||||
) {
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
async ngOnInit (): Promise<void> {
|
||||
this.sftp = await this.session.openSFTP()
|
||||
@ -96,28 +96,10 @@ export class SFTPPanelComponent {
|
||||
}
|
||||
|
||||
async uploadOne (transfer: FileUpload): Promise<void> {
|
||||
const itemPath = path.join(this.path, transfer.getName())
|
||||
const tempPath = itemPath + '.tabby-upload'
|
||||
this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
|
||||
const savedPath = this.path
|
||||
try {
|
||||
const handle = await this.sftp.open(tempPath, 'w')
|
||||
while (true) {
|
||||
const chunk = await transfer.read()
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
await handle.write(chunk)
|
||||
}
|
||||
handle.close()
|
||||
await this.sftp.rename(tempPath, itemPath)
|
||||
transfer.close()
|
||||
if (this.path === savedPath) {
|
||||
await this.navigate(this.path)
|
||||
}
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
this.sftp.unlink(tempPath)
|
||||
throw e
|
||||
if (this.path === savedPath) {
|
||||
await this.navigate(this.path)
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,21 +108,7 @@ export class SFTPPanelComponent {
|
||||
if (!transfer) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const handle = await this.sftp.open(itemPath, 'r')
|
||||
while (true) {
|
||||
const chunk = await handle.read()
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
await transfer.write(chunk)
|
||||
}
|
||||
transfer.close()
|
||||
handle.close()
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
throw e
|
||||
}
|
||||
this.sftp.download(itemPath, transfer)
|
||||
}
|
||||
|
||||
getModeString (item: SFTPFile): string {
|
||||
@ -159,31 +127,18 @@ export class SFTPPanelComponent {
|
||||
return result
|
||||
}
|
||||
|
||||
showContextMenu (item: SFTPFile, event: MouseEvent): void {
|
||||
event.preventDefault()
|
||||
this.platform.popupContextMenu([
|
||||
{
|
||||
click: async () => {
|
||||
if ((await this.platform.showMessageBox({
|
||||
type: 'warning',
|
||||
message: `Delete ${item.fullPath}?`,
|
||||
defaultId: 0,
|
||||
buttons: ['Delete', 'Cancel'],
|
||||
})).response === 0) {
|
||||
await this.deleteItem(item)
|
||||
this.navigate(this.path)
|
||||
}
|
||||
},
|
||||
label: 'Delete',
|
||||
},
|
||||
], event)
|
||||
async buildContextMenu (item: SFTPFile): Promise<MenuItemOptions[]> {
|
||||
let items: MenuItemOptions[] = []
|
||||
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(item, this)))) {
|
||||
items.push({ type: 'separator' })
|
||||
items = items.concat(section)
|
||||
}
|
||||
return items.slice(1)
|
||||
}
|
||||
|
||||
async deleteItem (item: SFTPFile): Promise<void> {
|
||||
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
||||
modal.componentInstance.item = item
|
||||
modal.componentInstance.sftp = this.sftp
|
||||
await modal.result
|
||||
async showContextMenu (item: SFTPFile, event: MouseEvent): Promise<void> {
|
||||
event.preventDefault()
|
||||
this.platform.popupContextMenu(await this.buildContextMenu(item), event)
|
||||
}
|
||||
|
||||
close (): void {
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ForwardedPort, ForwardedPortConfig, SSHSession } from '../api'
|
||||
import { ForwardedPort } from '../session/forwards'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { ForwardedPortConfig } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
|
@ -5,8 +5,9 @@ import { first } from 'rxjs'
|
||||
import { Platform, RecoveryToken } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHProfile, SSHSession } from '../api'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
||||
import { SSHProfile } from '../api'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
|
@ -22,6 +22,8 @@ import { RecoveryProvider } from './recoveryProvider'
|
||||
import { SSHHotkeyProvider } from './hotkeys'
|
||||
import { SFTPContextMenu } from './tabContextMenu'
|
||||
import { SSHProfilesService } from './profiles'
|
||||
import { SFTPContextMenuItemProvider } from './api/contextMenu'
|
||||
import { CommonSFTPContextMenu } from './sftpContextMenu'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
@ -41,6 +43,7 @@ import { SSHProfilesService } from './profiles'
|
||||
{ provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: SFTPContextMenu, multi: true },
|
||||
{ provide: ProfileProvider, useExisting: SSHProfilesService, multi: true },
|
||||
{ provide: SFTPContextMenuItemProvider, useClass: CommonSFTPContextMenu, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
SSHProfileSettingsComponent,
|
||||
@ -105,3 +108,7 @@ export default class SSHModule {
|
||||
await this.selector.show('Select an SSH profile', options)
|
||||
}
|
||||
}
|
||||
|
||||
export * from './api'
|
||||
export { SFTPFile, SFTPSession } from './session/sftp'
|
||||
export { SFTPPanelComponent }
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfileProvider, NewTabParameters, PartialProfile } from 'tabby-core'
|
||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
||||
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
|
||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
||||
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as keytar from 'keytar'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SSHProfile } from '../api'
|
||||
import { VaultService } from 'tabby-core'
|
||||
import { SSHProfile } from '../api'
|
||||
|
||||
export const VAULT_SECRET_TYPE_PASSWORD = 'ssh:password'
|
||||
export const VAULT_SECRET_TYPE_PASSPHRASE = 'ssh:key-passphrase'
|
||||
|
@ -4,11 +4,13 @@ import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
import { exec } from 'child_process'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService, PromptModalComponent } from 'tabby-core'
|
||||
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHAlgorithmType, SSHProfile, SSHSession } from '../api'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { ForwardedPort } from '../session/forwards'
|
||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SSHService {
|
||||
|
64
tabby-ssh/src/session/forwards.ts
Normal file
64
tabby-ssh/src/session/forwards.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import socksv5 from 'socksv5'
|
||||
import { Server, Socket, createServer } from 'net'
|
||||
|
||||
import { ForwardedPortConfig, PortForwardType } from '../api'
|
||||
|
||||
export class ForwardedPort implements ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host = '127.0.0.1'
|
||||
port: number
|
||||
targetAddress: string
|
||||
targetPort: number
|
||||
|
||||
private listener: Server|null = null
|
||||
|
||||
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
|
||||
if (this.type === PortForwardType.Local) {
|
||||
const listener = this.listener = createServer(s => callback(
|
||||
() => s,
|
||||
() => s.destroy(),
|
||||
s.remoteAddress ?? null,
|
||||
s.remotePort ?? null,
|
||||
this.targetAddress,
|
||||
this.targetPort,
|
||||
))
|
||||
return new Promise((resolve, reject) => {
|
||||
listener.listen(this.port, this.host)
|
||||
listener.on('error', reject)
|
||||
listener.on('listening', resolve)
|
||||
})
|
||||
} else if (this.type === PortForwardType.Dynamic) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
|
||||
callback(
|
||||
() => acceptConnection(true),
|
||||
() => rejectConnection(),
|
||||
null,
|
||||
null,
|
||||
info.dstAddr,
|
||||
info.dstPort,
|
||||
)
|
||||
}) as Server
|
||||
this.listener.on('error', reject)
|
||||
this.listener.listen(this.port, this.host, resolve)
|
||||
this.listener['useAuth'](socksv5.auth.None())
|
||||
})
|
||||
} else {
|
||||
throw new Error('Invalid forward type for a local listener')
|
||||
}
|
||||
}
|
||||
|
||||
stopLocalListener (): void {
|
||||
this.listener?.close()
|
||||
}
|
||||
|
||||
toString (): string {
|
||||
if (this.type === PortForwardType.Local) {
|
||||
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
|
||||
} if (this.type === PortForwardType.Remote) {
|
||||
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
|
||||
} else {
|
||||
return `(dynamic) ${this.host}:${this.port}`
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import * as C from 'constants'
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { posix as posixPath } from 'path'
|
||||
import { NgZone } from '@angular/core'
|
||||
import { wrapPromise } from 'tabby-core'
|
||||
import { Injector, NgZone } from '@angular/core'
|
||||
import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core'
|
||||
import { SFTPWrapper } from 'ssh2'
|
||||
import { promisify } from 'util'
|
||||
|
||||
@ -70,9 +71,22 @@ export class SFTPFileHandle {
|
||||
}
|
||||
|
||||
export class SFTPSession {
|
||||
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
|
||||
get closed$ (): Observable<void> { return this.closed }
|
||||
private closed = new Subject<void>()
|
||||
private zone: NgZone
|
||||
private logger: Logger
|
||||
|
||||
constructor (private sftp: SFTPWrapper, injector: Injector) {
|
||||
this.zone = injector.get(NgZone)
|
||||
this.logger = injector.get(LogService).create('sftp')
|
||||
sftp.on('close', () => {
|
||||
this.closed.next()
|
||||
this.closed.complete()
|
||||
})
|
||||
}
|
||||
|
||||
async readdir (p: string): Promise<SFTPFile[]> {
|
||||
this.logger.debug('readdir', p)
|
||||
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
||||
return entries.map(entry => this._makeFile(
|
||||
posixPath.join(p, entry.filename), entry,
|
||||
@ -80,10 +94,12 @@ export class SFTPSession {
|
||||
}
|
||||
|
||||
readlink (p: string): Promise<string> {
|
||||
this.logger.debug('readlink', p)
|
||||
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
||||
}
|
||||
|
||||
async stat (p: string): Promise<SFTPFile> {
|
||||
this.logger.debug('stat', p)
|
||||
const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
|
||||
return {
|
||||
name: posixPath.basename(p),
|
||||
@ -97,22 +113,68 @@ export class SFTPSession {
|
||||
}
|
||||
|
||||
async open (p: string, mode: string): Promise<SFTPFileHandle> {
|
||||
this.logger.debug('open', p)
|
||||
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
|
||||
return new SFTPFileHandle(this.sftp, handle, this.zone)
|
||||
}
|
||||
|
||||
async rmdir (p: string): Promise<void> {
|
||||
this.logger.debug('rmdir', p)
|
||||
await promisify((f: any) => this.sftp.rmdir(p, f))()
|
||||
}
|
||||
|
||||
async rename (oldPath: string, newPath: string): Promise<void> {
|
||||
this.logger.debug('rename', oldPath, newPath)
|
||||
await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
|
||||
}
|
||||
|
||||
async unlink (p: string): Promise<void> {
|
||||
this.logger.debug('unlink', p)
|
||||
await promisify((f: any) => this.sftp.unlink(p, f))()
|
||||
}
|
||||
|
||||
async upload (path: string, transfer: FileUpload): Promise<void> {
|
||||
this.logger.info('Uploading into', path)
|
||||
const tempPath = path + '.tabby-upload'
|
||||
try {
|
||||
const handle = await this.open(tempPath, 'w')
|
||||
while (true) {
|
||||
const chunk = await transfer.read()
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
await handle.write(chunk)
|
||||
}
|
||||
handle.close()
|
||||
await this.unlink(path)
|
||||
await this.rename(tempPath, path)
|
||||
transfer.close()
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
this.unlink(tempPath)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async download (path: string, transfer: FileDownload): Promise<void> {
|
||||
this.logger.info('Downloading', path)
|
||||
try {
|
||||
const handle = await this.open(path, 'r')
|
||||
while (true) {
|
||||
const chunk = await handle.read()
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
await transfer.write(chunk)
|
||||
}
|
||||
transfer.close()
|
||||
handle.close()
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private _makeFile (p: string, entry: FileEntry): SFTPFile {
|
||||
return {
|
||||
fullPath: p,
|
||||
|
@ -5,126 +5,22 @@ import * as path from 'path'
|
||||
import * as sshpk from 'sshpk'
|
||||
import colors from 'ansi-colors'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import socksv5 from 'socksv5'
|
||||
import { Injector, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, Profile, LogService } from 'tabby-core'
|
||||
import { BaseSession, LoginScriptsOptions } from 'tabby-terminal'
|
||||
import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService } from 'tabby-core'
|
||||
import { BaseSession } from 'tabby-terminal'
|
||||
import { Socket, createConnection } from 'net'
|
||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { ProxyCommandStream } from './services/ssh.service'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
import { ProxyCommandStream } from '../services/ssh.service'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { promisify } from 'util'
|
||||
import { SFTPSession } from './session/sftp'
|
||||
import { SFTPSession } from './sftp'
|
||||
import { PortForwardType, SSHProfile } from '../api/interfaces'
|
||||
import { ForwardedPort } from './forwards'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
export enum SSHAlgorithmType {
|
||||
HMAC = 'hmac',
|
||||
KEX = 'kex',
|
||||
CIPHER = 'cipher',
|
||||
HOSTKEY = 'serverHostKey',
|
||||
}
|
||||
|
||||
export interface SSHProfile extends Profile {
|
||||
options: SSHProfileOptions
|
||||
}
|
||||
|
||||
export interface SSHProfileOptions extends LoginScriptsOptions {
|
||||
host: string
|
||||
port?: number
|
||||
user: string
|
||||
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
|
||||
password?: string
|
||||
privateKeys?: string[]
|
||||
keepaliveInterval?: number
|
||||
keepaliveCountMax?: number
|
||||
readyTimeout?: number
|
||||
x11?: boolean
|
||||
skipBanner?: boolean
|
||||
jumpHost?: string
|
||||
agentForward?: boolean
|
||||
warnOnClose?: boolean
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
forwardedPorts?: ForwardedPortConfig[]
|
||||
}
|
||||
|
||||
export enum PortForwardType {
|
||||
Local = 'Local',
|
||||
Remote = 'Remote',
|
||||
Dynamic = 'Dynamic',
|
||||
}
|
||||
|
||||
export interface ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host: string
|
||||
port: number
|
||||
targetAddress: string
|
||||
targetPort: number
|
||||
}
|
||||
|
||||
export class ForwardedPort implements ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host = '127.0.0.1'
|
||||
port: number
|
||||
targetAddress: string
|
||||
targetPort: number
|
||||
|
||||
private listener: Server|null = null
|
||||
|
||||
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
|
||||
if (this.type === PortForwardType.Local) {
|
||||
const listener = this.listener = createServer(s => callback(
|
||||
() => s,
|
||||
() => s.destroy(),
|
||||
s.remoteAddress ?? null,
|
||||
s.remotePort ?? null,
|
||||
this.targetAddress,
|
||||
this.targetPort,
|
||||
))
|
||||
return new Promise((resolve, reject) => {
|
||||
listener.listen(this.port, this.host)
|
||||
listener.on('error', reject)
|
||||
listener.on('listening', resolve)
|
||||
})
|
||||
} else if (this.type === PortForwardType.Dynamic) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
|
||||
callback(
|
||||
() => acceptConnection(true),
|
||||
() => rejectConnection(),
|
||||
null,
|
||||
null,
|
||||
info.dstAddr,
|
||||
info.dstPort,
|
||||
)
|
||||
}) as Server
|
||||
this.listener.on('error', reject)
|
||||
this.listener.listen(this.port, this.host, resolve)
|
||||
this.listener['useAuth'](socksv5.auth.None())
|
||||
})
|
||||
} else {
|
||||
throw new Error('Invalid forward type for a local listener')
|
||||
}
|
||||
}
|
||||
|
||||
stopLocalListener (): void {
|
||||
this.listener?.close()
|
||||
}
|
||||
|
||||
toString (): string {
|
||||
if (this.type === PortForwardType.Local) {
|
||||
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
|
||||
} if (this.type === PortForwardType.Remote) {
|
||||
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
|
||||
} else {
|
||||
return `(dynamic) ${this.host}:${this.port}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthMethod {
|
||||
type: 'none'|'publickey'|'agent'|'password'|'keyboard-interactive'|'hostbased'
|
||||
name?: string
|
||||
@ -158,7 +54,7 @@ export class SSHSession extends BaseSession {
|
||||
private config: ConfigService
|
||||
|
||||
constructor (
|
||||
injector: Injector,
|
||||
private injector: Injector,
|
||||
public profile: SSHProfile,
|
||||
) {
|
||||
super(injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`))
|
||||
@ -241,7 +137,7 @@ export class SSHSession extends BaseSession {
|
||||
if (!this.sftp) {
|
||||
this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
|
||||
}
|
||||
return new SFTPSession(this.sftp, this.zone)
|
||||
return new SFTPSession(this.sftp, this.injector)
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
@ -613,9 +509,3 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ALGORITHM_BLACKLIST = [
|
||||
// cause native crashes in node crypto, use EC instead
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group-exchange-sha1',
|
||||
]
|
47
tabby-ssh/src/sftpContextMenu.ts
Normal file
47
tabby-ssh/src/sftpContextMenu.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { MenuItemOptions, PlatformService } from 'tabby-core'
|
||||
import { SFTPSession, SFTPFile } from './session/sftp'
|
||||
import { SFTPContextMenuItemProvider } from './api'
|
||||
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||
weight = 10
|
||||
|
||||
constructor (
|
||||
private platform: PlatformService,
|
||||
private ngbModal: NgbModal,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||
return [
|
||||
{
|
||||
click: async () => {
|
||||
if ((await this.platform.showMessageBox({
|
||||
type: 'warning',
|
||||
message: `Delete ${item.fullPath}?`,
|
||||
defaultId: 0,
|
||||
buttons: ['Delete', 'Cancel'],
|
||||
})).response === 0) {
|
||||
await this.deleteItem(item, panel.sftp)
|
||||
panel.navigate(panel.path)
|
||||
}
|
||||
},
|
||||
label: 'Delete',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
|
||||
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
||||
modal.componentInstance.item = item
|
||||
modal.componentInstance.sftp = session
|
||||
await modal.result
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user