mirror of
https://github.com/Eugeny/tabby
synced 2024-11-15 01:17:14 +00:00
added experimental SFTP implementation - fixes #296
This commit is contained in:
parent
019cba06d4
commit
a397884d3c
16 changed files with 462 additions and 6 deletions
|
@ -88,6 +88,21 @@ export abstract class PlatformService {
|
|||
abstract startDownload (name: string, size: number): Promise<FileDownload|null>
|
||||
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
|
||||
|
||||
startUploadFromDragEvent (event: DragEvent): FileUpload[] {
|
||||
const result: FileUpload[] = []
|
||||
if (!event.dataTransfer) {
|
||||
return []
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < event.dataTransfer.files.length; i++) {
|
||||
const file = event.dataTransfer.files[i]
|
||||
const transfer = new DropUpload(file)
|
||||
this.fileTransferStarted.next(transfer)
|
||||
result.push(transfer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getConfigPath (): string|null {
|
||||
return null
|
||||
}
|
||||
|
@ -144,3 +159,36 @@ export abstract class PlatformService {
|
|||
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
|
||||
abstract quit (): void
|
||||
}
|
||||
|
||||
|
||||
class DropUpload extends FileUpload {
|
||||
private stream: ReadableStream
|
||||
private reader: ReadableStreamDefaultReader
|
||||
|
||||
constructor (private file: File) {
|
||||
super()
|
||||
this.stream = this.file.stream()
|
||||
this.reader = this.stream.getReader()
|
||||
}
|
||||
|
||||
getName (): string {
|
||||
return this.file.name
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.file.size
|
||||
}
|
||||
|
||||
async read (): Promise<Buffer> {
|
||||
const result: any = await this.reader.read()
|
||||
if (result.done || !result.value) {
|
||||
return Buffer.from('')
|
||||
}
|
||||
const chunk = Buffer.from(result.value)
|
||||
this.increaseProgress(chunk.length)
|
||||
return chunk
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
close (): void { }
|
||||
}
|
||||
|
|
1
terminus-core/src/directives/dropZone.directive.pug
Normal file
1
terminus-core/src/directives/dropZone.directive.pug
Normal file
|
@ -0,0 +1 @@
|
|||
i.fas.fa-upload
|
24
terminus-core/src/directives/dropZone.directive.scss
Normal file
24
terminus-core/src/directives/dropZone.directive.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
.drop-zone-hint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
transition: .25s opacity ease-out;
|
||||
opacity: 0;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
49
terminus-core/src/directives/dropZone.directive.ts
Normal file
49
terminus-core/src/directives/dropZone.directive.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { Directive, Output, ElementRef, EventEmitter, AfterViewInit } from '@angular/core'
|
||||
import { FileUpload, PlatformService } from '../api/platform'
|
||||
import './dropZone.directive.scss'
|
||||
|
||||
/** @hidden */
|
||||
@Directive({
|
||||
selector: '[dropZone]',
|
||||
})
|
||||
export class DropZoneDirective implements AfterViewInit {
|
||||
@Output() transfer = new EventEmitter<FileUpload>()
|
||||
private dropHint?: HTMLElement
|
||||
|
||||
constructor (
|
||||
private el: ElementRef,
|
||||
private platform: PlatformService,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit (): void {
|
||||
this.el.nativeElement.addEventListener('dragover', () => {
|
||||
if (!this.dropHint) {
|
||||
this.dropHint = document.createElement('div')
|
||||
this.dropHint.className = 'drop-zone-hint'
|
||||
this.dropHint.innerHTML = require('./dropZone.directive.pug')
|
||||
this.el.nativeElement.appendChild(this.dropHint)
|
||||
setTimeout(() => {
|
||||
this.dropHint!.classList.add('visible')
|
||||
})
|
||||
}
|
||||
})
|
||||
this.el.nativeElement.addEventListener('drop', (event: DragEvent) => {
|
||||
this.removeHint()
|
||||
for (const transfer of this.platform.startUploadFromDragEvent(event)) {
|
||||
this.transfer.emit(transfer)
|
||||
}
|
||||
})
|
||||
this.el.nativeElement.addEventListener('dragleave', () => {
|
||||
this.removeHint()
|
||||
})
|
||||
}
|
||||
|
||||
private removeHint () {
|
||||
const element = this.dropHint
|
||||
delete this.dropHint
|
||||
element?.classList.remove('visible')
|
||||
setTimeout(() => {
|
||||
element?.remove()
|
||||
}, 500)
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import { TransfersMenuComponent } from './components/transfersMenu.component'
|
|||
|
||||
import { AutofocusDirective } from './directives/autofocus.directive'
|
||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from './api'
|
||||
|
||||
|
@ -83,6 +84,7 @@ const PROVIDERS = [
|
|||
UnlockVaultModalComponent,
|
||||
WelcomeTabComponent,
|
||||
TransfersMenuComponent,
|
||||
DropZoneDirective,
|
||||
],
|
||||
entryComponents: [
|
||||
RenameTabModalComponent,
|
||||
|
@ -96,6 +98,7 @@ const PROVIDERS = [
|
|||
CheckboxComponent,
|
||||
ToggleComponent,
|
||||
AutofocusDirective,
|
||||
DropZoneDirective,
|
||||
],
|
||||
})
|
||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
|
|
@ -38,7 +38,7 @@ $theme-colors: (
|
|||
warning: $orange,
|
||||
danger: $red,
|
||||
light: $gray-300,
|
||||
dark: $gray-800,
|
||||
dark: #0e151d,
|
||||
rare: $purple
|
||||
);
|
||||
|
||||
|
@ -150,7 +150,7 @@ $navbar-padding-x: 0;
|
|||
$dropdown-bg: $content-bg-solid;
|
||||
$dropdown-color: $body-color;
|
||||
$dropdown-border-width: 1px;
|
||||
$dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175);
|
||||
$dropdown-box-shadow: 0 0 1rem rgba($black, .25), 0 1px 1px rgba($black, .12);
|
||||
$dropdown-header-color: $gray-500;
|
||||
|
||||
$dropdown-link-color: $body-color;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "14.14.31",
|
||||
"@types/ssh2": "^0.5.46",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"clone-deep": "^4.0.1",
|
||||
|
|
|
@ -10,11 +10,12 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|||
import { HostAppService, Logger, NotificationsService, Platform, PlatformService } from 'terminus-core'
|
||||
import { BaseSession } from 'terminus-terminal'
|
||||
import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { Client, ClientChannel } from 'ssh2'
|
||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { ProxyCommandStream } from './services/ssh.service'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
|
@ -140,6 +141,7 @@ export class SSHSession extends BaseSession {
|
|||
scripts?: LoginScript[]
|
||||
shell?: ClientChannel
|
||||
ssh: Client
|
||||
sftp?: SFTPWrapper
|
||||
forwardedPorts: ForwardedPort[] = []
|
||||
logger: Logger
|
||||
jumpStream: any
|
||||
|
@ -221,6 +223,13 @@ export class SSHSession extends BaseSession {
|
|||
this.remainingAuthMethods.push({ type: 'hostbased' })
|
||||
}
|
||||
|
||||
async openSFTP (): Promise<SFTPWrapper> {
|
||||
if (!this.sftp) {
|
||||
this.sftp = await promisify<SFTPWrapper>(f => this.ssh.sftp(f))()
|
||||
}
|
||||
return this.sftp
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.open = true
|
||||
|
||||
|
@ -273,7 +282,7 @@ export class SSHSession extends BaseSession {
|
|||
|
||||
if (match) {
|
||||
this.logger.info('Executing script: "' + cmd + '"')
|
||||
this.shell.write(cmd + '\n')
|
||||
this.shell?.write(cmd + '\n')
|
||||
this.scripts = this.scripts.filter(x => x !== script)
|
||||
} else {
|
||||
if (script.optional) {
|
||||
|
@ -569,7 +578,7 @@ export class SSHSession extends BaseSession {
|
|||
for (const script of this.scripts) {
|
||||
if (!script.expect) {
|
||||
console.log('Executing script:', script.send)
|
||||
this.shell.write(script.send + '\n')
|
||||
this.shell?.write(script.send + '\n')
|
||||
this.scripts = this.scripts.filter(x => x !== script)
|
||||
} else {
|
||||
break
|
||||
|
|
33
terminus-ssh/src/components/sftpPanel.component.pug
Normal file
33
terminus-ssh/src/components/sftpPanel.component.pug
Normal file
|
@ -0,0 +1,33 @@
|
|||
.header
|
||||
.breadcrumb.mr-auto
|
||||
a.breadcrumb-item((click)='navigate("/")') SFTP
|
||||
a.breadcrumb-item(
|
||||
*ngFor='let segment of pathSegments',
|
||||
(click)='navigate(segment.path)'
|
||||
) {{segment.name}}
|
||||
|
||||
button.btn.btn-link.btn-sm.d-flex((click)='upload()')
|
||||
i.fas.fa-upload.mr-1
|
||||
div Upload
|
||||
|
||||
button.btn.btn-link.btn-close((click)='close()') !{require('../../../terminus-core/src/icons/times.svg')}
|
||||
|
||||
.body(dropZone, (transfer)='uploadOne($event)')
|
||||
div(*ngIf='!sftp') Connecting
|
||||
div(*ngIf='sftp')
|
||||
div(*ngIf='fileList === null') Loading
|
||||
.list-group.list-group-light(*ngIf='fileList !== null')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
*ngIf='path !== "/"',
|
||||
(click)='goUp()'
|
||||
)
|
||||
i.fas.fa-fw.fa-level-up-alt
|
||||
div Go up
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
*ngFor='let item of fileList',
|
||||
(click)='open(item)'
|
||||
)
|
||||
i.fa-fw([class]='getIcon(item)')
|
||||
div {{item.filename}}
|
||||
.mr-auto
|
||||
.mode {{getModeString(item)}}
|
40
terminus-ssh/src/components/sftpPanel.component.scss
Normal file
40
terminus-ssh/src/components/sftpPanel.component.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;;
|
||||
|
||||
> * {
|
||||
}
|
||||
|
||||
> .header {
|
||||
padding: 5px 15px 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 10px 20px;
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mode {
|
||||
font-family: monospace;
|
||||
opacity: .5;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-close svg {
|
||||
width: 12px;
|
||||
}
|
196
terminus-ssh/src/components/sftpPanel.component.ts
Normal file
196
terminus-ssh/src/components/sftpPanel.component.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
import { SFTPWrapper } from 'ssh2'
|
||||
import type { FileEntry, Stats } from 'ssh2-streams'
|
||||
import { promisify } from 'util'
|
||||
import { SSHSession } from '../api'
|
||||
import * as path from 'path'
|
||||
import * as C from 'constants'
|
||||
import { FileUpload, PlatformService } from 'terminus-core'
|
||||
|
||||
interface PathSegment {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'sftp-panel',
|
||||
template: require('./sftpPanel.component.pug'),
|
||||
styles: [require('./sftpPanel.component.scss')],
|
||||
})
|
||||
export class SFTPPanelComponent {
|
||||
@Input() session: SSHSession
|
||||
@Output() closed = new EventEmitter<void>()
|
||||
sftp: SFTPWrapper
|
||||
fileList: FileEntry[]|null = null
|
||||
path = '/'
|
||||
pathSegments: PathSegment[] = []
|
||||
|
||||
constructor (
|
||||
private platform: PlatformService,
|
||||
) { }
|
||||
|
||||
async ngOnInit (): Promise<void> {
|
||||
this.sftp = await this.session.openSFTP()
|
||||
this.navigate('/')
|
||||
}
|
||||
|
||||
async navigate (newPath: string): Promise<void> {
|
||||
this.path = newPath
|
||||
|
||||
let p = newPath
|
||||
this.pathSegments = []
|
||||
while (p !== '/') {
|
||||
this.pathSegments.unshift({
|
||||
name: path.basename(p),
|
||||
path: p,
|
||||
})
|
||||
p = path.dirname(p)
|
||||
}
|
||||
|
||||
this.fileList = null
|
||||
this.fileList = await promisify<FileEntry[]>(f => this.sftp.readdir(this.path, f))()
|
||||
console.log(this.fileList)
|
||||
|
||||
const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0
|
||||
this.fileList.sort((a, b) =>
|
||||
dirKey(b) - dirKey(a) ||
|
||||
a.filename.localeCompare(b.filename))
|
||||
}
|
||||
|
||||
getIcon (item: FileEntry): string {
|
||||
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
||||
return 'fas fa-folder text-info'
|
||||
}
|
||||
if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
||||
return 'fas fa-link text-warning'
|
||||
}
|
||||
return 'fas fa-file'
|
||||
}
|
||||
|
||||
goUp (): void {
|
||||
this.navigate(path.dirname(this.path))
|
||||
}
|
||||
|
||||
async open (item: FileEntry): Promise<void> {
|
||||
const itemPath = path.join(this.path, item.filename)
|
||||
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
||||
this.navigate(path.join(this.path, item.filename))
|
||||
} else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
||||
const target = await promisify<string>(f => this.sftp.readlink(itemPath, f))()
|
||||
const stat = await promisify<Stats>(f => this.sftp.stat(target, f))()
|
||||
if (stat.isDirectory()) {
|
||||
this.navigate(itemPath)
|
||||
} else {
|
||||
this.download(itemPath, stat.size)
|
||||
}
|
||||
} else {
|
||||
this.download(itemPath, item.attrs.size)
|
||||
}
|
||||
}
|
||||
|
||||
async upload (): Promise<void> {
|
||||
const transfers = await this.platform.startUpload({ multiple: true })
|
||||
const savedPath = this.path
|
||||
for (const transfer of transfers) {
|
||||
this.uploadOne(transfer).then(() => {
|
||||
if (this.path === savedPath) {
|
||||
this.navigate(this.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async uploadOne (transfer: FileUpload): Promise<void> {
|
||||
const itemPath = path.join(this.path, transfer.getName())
|
||||
try {
|
||||
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'w', f))()
|
||||
let position = 0
|
||||
while (true) {
|
||||
const chunk = await transfer.read()
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
const p = position
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
while (true) {
|
||||
const wait = this.sftp.write(handle, chunk, 0, chunk.length, p, err => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
if (!wait) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
position += chunk.length
|
||||
}
|
||||
this.sftp.close(handle, () => null)
|
||||
transfer.close()
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async download (itemPath: string, size: number): Promise<void> {
|
||||
const transfer = await this.platform.startDownload(path.basename(itemPath), size)
|
||||
if (!transfer) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'r', f))()
|
||||
const buffer = Buffer.alloc(256 * 1024)
|
||||
let position = 0
|
||||
while (true) {
|
||||
const p = position
|
||||
const chunk: Buffer = await new Promise((resolve, reject) => {
|
||||
while (true) {
|
||||
const wait = this.sftp.read(handle, buffer, 0, buffer.length, p, (err, read) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(buffer.slice(0, read))
|
||||
})
|
||||
if (!wait) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!chunk.length) {
|
||||
break
|
||||
}
|
||||
await transfer.write(chunk)
|
||||
position += chunk.length
|
||||
}
|
||||
transfer.close()
|
||||
this.sftp.close(handle, () => null)
|
||||
} catch (e) {
|
||||
transfer.cancel()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
getModeString (item: FileEntry): string {
|
||||
const s = 'SGdrwxrwxrwx'
|
||||
const e = ' ---------'
|
||||
const c = [
|
||||
0o4000, 0o2000, C.S_IFDIR,
|
||||
C.S_IRUSR, C.S_IWUSR, C.S_IXUSR,
|
||||
C.S_IRGRP, C.S_IWGRP, C.S_IXGRP,
|
||||
C.S_IROTH, C.S_IWOTH, C.S_IXOTH,
|
||||
]
|
||||
let result = ''
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
result += item.attrs.mode & c[i] ? s[i] : e[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.closed.emit()
|
||||
}
|
||||
}
|
|
@ -9,6 +9,16 @@
|
|||
button.btn.btn-secondary.mr-2((click)='reconnect()', [class.btn-info]='!session || !session.open')
|
||||
span Reconnect
|
||||
|
||||
button.btn.btn-secondary.mr-2((click)='openSFTP()', *ngIf='session && session.open')
|
||||
span SFTP
|
||||
|
||||
button.btn.btn-secondary((click)='showPortForwarding()', *ngIf='session && session.open')
|
||||
i.fas.fa-plug
|
||||
span Ports
|
||||
|
||||
sftp-panel.bg-dark(
|
||||
*ngIf='sftpPanelVisible',
|
||||
(click)='$event.stopPropagation()',
|
||||
[session]='session',
|
||||
(closed)='sftpPanelVisible = false'
|
||||
)
|
||||
|
|
|
@ -70,3 +70,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
sftp-panel {
|
||||
position: absolute;
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import colors from 'ansi-colors'
|
||||
import { Spinner } from 'cli-spinner'
|
||||
import { Component, Injector } from '@angular/core'
|
||||
import { Component, Injector, HostListener } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { RecoveryToken } from 'terminus-core'
|
||||
|
@ -20,6 +20,7 @@ import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.compon
|
|||
export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
connection?: SSHConnection
|
||||
session: SSHSession|null = null
|
||||
sftpPanelVisible = false
|
||||
private sessionStack: SSHSession[] = []
|
||||
private recentInputs = ''
|
||||
private reconnectOffered = false
|
||||
|
@ -225,6 +226,17 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||
)).response === 1
|
||||
}
|
||||
|
||||
openSFTP (): void {
|
||||
setTimeout(() => {
|
||||
this.sftpPanelVisible = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick (): void {
|
||||
this.sftpPanelVisible = false
|
||||
}
|
||||
|
||||
private startSpinner () {
|
||||
this.spinner.setSpinnerString(6)
|
||||
this.spinner.start()
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SSHPortForwardingConfigComponent } from './components/sshPortForwarding
|
|||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
||||
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SSHConfigProvider } from './config'
|
||||
|
@ -55,6 +56,7 @@ import { SSHCLIHandler } from './cli'
|
|||
SSHPortForwardingConfigComponent,
|
||||
SSHSettingsTabComponent,
|
||||
SSHTabComponent,
|
||||
SFTPPanelComponent,
|
||||
],
|
||||
})
|
||||
export default class SSHModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
|
|
@ -2,11 +2,31 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@*":
|
||||
version "15.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d"
|
||||
integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==
|
||||
|
||||
"@types/node@14.14.31":
|
||||
version "14.14.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
|
||||
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
|
||||
|
||||
"@types/ssh2-streams@*":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.8.tgz#142af404dae059931aea7fcd1511b5478964feb6"
|
||||
integrity sha512-I7gixRPUvVIyJuCEvnmhr3KvA2dC0639kKswqD4H5b4/FOcnPtNU+qWLiXdKIqqX9twUvi5j0U1mwKE5CUsrfA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ssh2@^0.5.46":
|
||||
version "0.5.46"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584"
|
||||
integrity sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/ssh2-streams" "*"
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
|
|
Loading…
Reference in a new issue