mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-04 02:19:55 +00:00
zone-ified sftp promises
This commit is contained in:
parent
a2ed674b10
commit
6ce76af9be
@ -5,6 +5,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { AsyncSubject, Subject, Observable } from 'rxjs'
|
import { AsyncSubject, Subject, Observable } from 'rxjs'
|
||||||
import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component'
|
import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component'
|
||||||
import { NotificationsService } from '../services/notifications.service'
|
import { NotificationsService } from '../services/notifications.service'
|
||||||
|
import { wrapPromise } from 'utils'
|
||||||
|
|
||||||
const PBKDF_ITERATIONS = 100000
|
const PBKDF_ITERATIONS = 100000
|
||||||
const PBKDF_DIGEST = 'sha512'
|
const PBKDF_DIGEST = 'sha512'
|
||||||
@ -120,7 +121,7 @@ export class VaultService {
|
|||||||
passphrase = await this.getPassphrase()
|
passphrase = await this.getPassphrase()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await this.wrapPromise(decryptVault(storage, passphrase))
|
return await wrapPromise(this.zone, decryptVault(storage, passphrase))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_rememberedPassphrase = null
|
_rememberedPassphrase = null
|
||||||
if (e.toString().includes('BAD_DECRYPT')) {
|
if (e.toString().includes('BAD_DECRYPT')) {
|
||||||
@ -144,7 +145,7 @@ export class VaultService {
|
|||||||
if (_rememberedPassphrase) {
|
if (_rememberedPassphrase) {
|
||||||
_rememberedPassphrase = passphrase
|
_rememberedPassphrase = passphrase
|
||||||
}
|
}
|
||||||
return this.wrapPromise(encryptVault(vault, passphrase))
|
return wrapPromise(this.zone, encryptVault(vault, passphrase))
|
||||||
}
|
}
|
||||||
|
|
||||||
async save (vault: Vault, passphrase?: string): Promise<void> {
|
async save (vault: Vault, passphrase?: string): Promise<void> {
|
||||||
@ -210,14 +211,4 @@ export class VaultService {
|
|||||||
isEnabled (): boolean {
|
isEnabled (): boolean {
|
||||||
return !!this.store
|
return !!this.store
|
||||||
}
|
}
|
||||||
|
|
||||||
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
promise.then(result => {
|
|
||||||
this.zone.run(() => resolve(result))
|
|
||||||
}).catch(error => {
|
|
||||||
this.zone.run(() => reject(error))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
import { NgZone } from '@angular/core'
|
||||||
|
|
||||||
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
|
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
|
||||||
export const WIN_BUILD_CONPTY_STABLE = 18309
|
export const WIN_BUILD_CONPTY_STABLE = 18309
|
||||||
@ -20,3 +21,13 @@ export function getCSSFontFamily (config: any): string {
|
|||||||
fonts = fonts.map(x => `"${x}"`)
|
fonts = fonts.map(x => `"${x}"`)
|
||||||
return fonts.join(', ')
|
return fonts.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wrapPromise <T> (zone: NgZone, promise: Promise<T>): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
promise.then(result => {
|
||||||
|
zone.run(() => resolve(result))
|
||||||
|
}).catch(error => {
|
||||||
|
zone.run(() => reject(error))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import * as os from 'os'
|
|||||||
import promiseIpc from 'electron-promise-ipc'
|
import promiseIpc from 'electron-promise-ipc'
|
||||||
import { execFile } from 'mz/child_process'
|
import { execFile } from 'mz/child_process'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions } from 'terminus-core'
|
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'terminus-core'
|
||||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||||
|
|
||||||
/* eslint-disable block-scoped-var */
|
/* eslint-disable block-scoped-var */
|
||||||
@ -181,7 +181,7 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
|
|
||||||
return Promise.all(result.filePaths.map(async p => {
|
return Promise.all(result.filePaths.map(async p => {
|
||||||
const transfer = new ElectronFileUpload(p)
|
const transfer = new ElectronFileUpload(p)
|
||||||
await this.wrapPromise(transfer.open())
|
await wrapPromise(this.zone, transfer.open())
|
||||||
this.fileTransferStarted.next(transfer)
|
this.fileTransferStarted.next(transfer)
|
||||||
return transfer
|
return transfer
|
||||||
}))
|
}))
|
||||||
@ -198,20 +198,10 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const transfer = new ElectronFileDownload(result.filePath, size)
|
const transfer = new ElectronFileDownload(result.filePath, size)
|
||||||
await this.wrapPromise(transfer.open())
|
await wrapPromise(this.zone, transfer.open())
|
||||||
this.fileTransferStarted.next(transfer)
|
this.fileTransferStarted.next(transfer)
|
||||||
return transfer
|
return transfer
|
||||||
}
|
}
|
||||||
|
|
||||||
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
promise.then(result => {
|
|
||||||
this.zone.run(() => resolve(result))
|
|
||||||
}).catch(error => {
|
|
||||||
this.zone.run(() => reject(error))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ElectronFileUpload extends FileUpload {
|
class ElectronFileUpload extends FileUpload {
|
||||||
|
@ -5,12 +5,13 @@ import * as sshpk from 'sshpk'
|
|||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import socksv5 from 'socksv5'
|
import socksv5 from 'socksv5'
|
||||||
import { Injector } from '@angular/core'
|
import { Injector, NgZone } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HostAppService, Logger, NotificationsService, Platform, PlatformService } from 'terminus-core'
|
import { HostAppService, Logger, NotificationsService, Platform, PlatformService, wrapPromise } from 'terminus-core'
|
||||||
import { BaseSession } from 'terminus-terminal'
|
import { BaseSession } from 'terminus-terminal'
|
||||||
import { Server, Socket, createServer, createConnection } from 'net'
|
import { Server, Socket, createServer, createConnection } from 'net'
|
||||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||||
|
import type { FileEntry, Stats } from 'ssh2-streams'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { ProxyCommandStream } from './services/ssh.service'
|
import { ProxyCommandStream } from './services/ssh.service'
|
||||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||||
@ -137,6 +138,77 @@ interface AuthMethod {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SFTPFileHandle {
|
||||||
|
position = 0
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private sftp: SFTPWrapper,
|
||||||
|
private handle: Buffer,
|
||||||
|
private zone: NgZone,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
read (): Promise<Buffer> {
|
||||||
|
const buffer = Buffer.alloc(256 * 1024)
|
||||||
|
return wrapPromise(this.zone, new Promise((resolve, reject) => {
|
||||||
|
while (true) {
|
||||||
|
const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.position += read
|
||||||
|
resolve(buffer.slice(0, read))
|
||||||
|
})
|
||||||
|
if (!wait) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
write (chunk: Buffer): Promise<void> {
|
||||||
|
return wrapPromise(this.zone, new Promise<void>((resolve, reject) => {
|
||||||
|
while (true) {
|
||||||
|
const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
this.position += chunk.length
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
if (!wait) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
close (): Promise<void> {
|
||||||
|
return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SFTPSession {
|
||||||
|
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
|
||||||
|
|
||||||
|
readdir (p: string): Promise<FileEntry[]> {
|
||||||
|
return wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
||||||
|
}
|
||||||
|
|
||||||
|
readlink (p: string): Promise<string> {
|
||||||
|
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
||||||
|
}
|
||||||
|
|
||||||
|
stat (p: string): Promise<Stats> {
|
||||||
|
return wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
|
||||||
|
}
|
||||||
|
|
||||||
|
async open (p: string, mode: string): Promise<SFTPFileHandle> {
|
||||||
|
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
|
||||||
|
return new SFTPFileHandle(this.sftp, handle, this.zone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SSHSession extends BaseSession {
|
export class SSHSession extends BaseSession {
|
||||||
scripts?: LoginScript[]
|
scripts?: LoginScript[]
|
||||||
shell?: ClientChannel
|
shell?: ClientChannel
|
||||||
@ -161,6 +233,7 @@ export class SSHSession extends BaseSession {
|
|||||||
private hostApp: HostAppService
|
private hostApp: HostAppService
|
||||||
private platform: PlatformService
|
private platform: PlatformService
|
||||||
private notifications: NotificationsService
|
private notifications: NotificationsService
|
||||||
|
private zone: NgZone
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
@ -172,6 +245,7 @@ export class SSHSession extends BaseSession {
|
|||||||
this.hostApp = injector.get(HostAppService)
|
this.hostApp = injector.get(HostAppService)
|
||||||
this.platform = injector.get(PlatformService)
|
this.platform = injector.get(PlatformService)
|
||||||
this.notifications = injector.get(NotificationsService)
|
this.notifications = injector.get(NotificationsService)
|
||||||
|
this.zone = injector.get(NgZone)
|
||||||
|
|
||||||
this.scripts = connection.scripts ?? []
|
this.scripts = connection.scripts ?? []
|
||||||
this.destroyed$.subscribe(() => {
|
this.destroyed$.subscribe(() => {
|
||||||
@ -223,11 +297,11 @@ export class SSHSession extends BaseSession {
|
|||||||
this.remainingAuthMethods.push({ type: 'hostbased' })
|
this.remainingAuthMethods.push({ type: 'hostbased' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async openSFTP (): Promise<SFTPWrapper> {
|
async openSFTP (): Promise<SFTPSession> {
|
||||||
if (!this.sftp) {
|
if (!this.sftp) {
|
||||||
this.sftp = await promisify<SFTPWrapper>(f => this.ssh.sftp(f))()
|
this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
|
||||||
}
|
}
|
||||||
return this.sftp
|
return new SFTPSession(this.sftp, this.zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
async start (): Promise<void> {
|
async start (): Promise<void> {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||||
import { SFTPWrapper } from 'ssh2'
|
import type { FileEntry } from 'ssh2-streams'
|
||||||
import type { FileEntry, Stats } from 'ssh2-streams'
|
import { SSHSession, SFTPSession } from '../api'
|
||||||
import { promisify } from 'util'
|
|
||||||
import { SSHSession } from '../api'
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as C from 'constants'
|
import * as C from 'constants'
|
||||||
import { FileUpload, PlatformService } from 'terminus-core'
|
import { FileUpload, PlatformService } from 'terminus-core'
|
||||||
@ -21,7 +19,7 @@ interface PathSegment {
|
|||||||
export class SFTPPanelComponent {
|
export class SFTPPanelComponent {
|
||||||
@Input() session: SSHSession
|
@Input() session: SSHSession
|
||||||
@Output() closed = new EventEmitter<void>()
|
@Output() closed = new EventEmitter<void>()
|
||||||
sftp: SFTPWrapper
|
sftp: SFTPSession
|
||||||
fileList: FileEntry[]|null = null
|
fileList: FileEntry[]|null = null
|
||||||
path = '/'
|
path = '/'
|
||||||
pathSegments: PathSegment[] = []
|
pathSegments: PathSegment[] = []
|
||||||
@ -49,8 +47,7 @@ export class SFTPPanelComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fileList = null
|
this.fileList = null
|
||||||
this.fileList = await promisify<FileEntry[]>(f => this.sftp.readdir(this.path, f))()
|
this.fileList = await this.sftp.readdir(this.path)
|
||||||
console.log(this.fileList)
|
|
||||||
|
|
||||||
const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0
|
const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0
|
||||||
this.fileList.sort((a, b) =>
|
this.fileList.sort((a, b) =>
|
||||||
@ -77,8 +74,8 @@ export class SFTPPanelComponent {
|
|||||||
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
||||||
this.navigate(path.join(this.path, item.filename))
|
this.navigate(path.join(this.path, item.filename))
|
||||||
} else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
} else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
||||||
const target = await promisify<string>(f => this.sftp.readlink(itemPath, f))()
|
const target = await this.sftp.readlink(itemPath)
|
||||||
const stat = await promisify<Stats>(f => this.sftp.stat(target, f))()
|
const stat = await this.sftp.stat(target)
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
this.navigate(itemPath)
|
this.navigate(itemPath)
|
||||||
} else {
|
} else {
|
||||||
@ -104,30 +101,15 @@ export class SFTPPanelComponent {
|
|||||||
async uploadOne (transfer: FileUpload): Promise<void> {
|
async uploadOne (transfer: FileUpload): Promise<void> {
|
||||||
const itemPath = path.join(this.path, transfer.getName())
|
const itemPath = path.join(this.path, transfer.getName())
|
||||||
try {
|
try {
|
||||||
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'w', f))()
|
const handle = await this.sftp.open(itemPath, 'w')
|
||||||
let position = 0
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const chunk = await transfer.read()
|
const chunk = await transfer.read()
|
||||||
if (!chunk.length) {
|
if (!chunk.length) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const p = position
|
await handle.write(chunk)
|
||||||
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)
|
handle.close()
|
||||||
transfer.close()
|
transfer.close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
transfer.cancel()
|
transfer.cancel()
|
||||||
@ -141,33 +123,16 @@ export class SFTPPanelComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'r', f))()
|
const handle = await this.sftp.open(itemPath, 'r')
|
||||||
const buffer = Buffer.alloc(256 * 1024)
|
|
||||||
let position = 0
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const p = position
|
const chunk = await handle.read()
|
||||||
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) {
|
if (!chunk.length) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
await transfer.write(chunk)
|
await transfer.write(chunk)
|
||||||
position += chunk.length
|
|
||||||
}
|
}
|
||||||
transfer.close()
|
transfer.close()
|
||||||
this.sftp.close(handle, () => null)
|
handle.close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
transfer.cancel()
|
transfer.cancel()
|
||||||
throw e
|
throw e
|
||||||
|
Loading…
x
Reference in New Issue
Block a user