This commit is contained in:
Eugene 2024-07-11 23:56:33 +02:00
parent c8d5b7ab61
commit 1f2bf12ed7
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
2 changed files with 131 additions and 127 deletions

View File

@ -1,12 +1,9 @@
import * as C from 'constants' /* eslint-disable @typescript-eslint/no-unused-vars */
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { posix as posixPath } from 'path' import { posix as posixPath } from 'path'
import { Injector, NgZone } from '@angular/core' import { Injector } from '@angular/core'
import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core' import { FileDownload, FileUpload, Logger, LogService } from 'tabby-core'
import { SFTPWrapper } from 'ssh2' import * as russh from 'russh'
import { promisify } from 'util'
import type { FileEntry, Stats } from 'ssh2-streams'
export interface SFTPFile { export interface SFTPFile {
name: string name: string
@ -21,64 +18,65 @@ export interface SFTPFile {
export class SFTPFileHandle { export class SFTPFileHandle {
position = 0 position = 0
constructor ( // constructor (
private sftp: SFTPWrapper, // private sftp: russh.SFTP,
private handle: Buffer, // private handle: Buffer,
private zone: NgZone, // private zone: NgZone,
) { } // ) { }
read (): Promise<Buffer> { read (): Promise<Buffer> {
const buffer = Buffer.alloc(256 * 1024) throw new Error('Not implemented')
return wrapPromise(this.zone, new Promise((resolve, reject) => { // const buffer = Buffer.alloc(256 * 1024)
while (true) { // return wrapPromise(this.zone, new Promise((resolve, reject) => {
const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => { // while (true) {
if (err) { // const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => {
reject(err) // if (err) {
return // reject(err)
} // return
this.position += read // }
resolve(buffer.slice(0, read)) // this.position += read
}) // resolve(buffer.slice(0, read))
if (!wait) { // })
break // if (!wait) {
} // break
} // }
})) // }
// }))
} }
write (chunk: Buffer): Promise<void> { write (chunk: Buffer): Promise<void> {
return wrapPromise(this.zone, new Promise<void>((resolve, reject) => { throw new Error('Not implemented')
while (true) { // return wrapPromise(this.zone, new Promise<void>((resolve, reject) => {
const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => { // while (true) {
if (err) { // const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => {
reject(err) // if (err) {
return // reject(err)
} // return
this.position += chunk.length // }
resolve() // this.position += chunk.length
}) // resolve()
if (!wait) { // })
break // if (!wait) {
} // break
} // }
})) // }
// }))
} }
close (): Promise<void> { close (): Promise<void> {
return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle)) throw new Error('Not implemented')
// return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle))
} }
} }
export class SFTPSession { export class SFTPSession {
get closed$ (): Observable<void> { return this.closed } get closed$ (): Observable<void> { return this.closed }
private closed = new Subject<void>() private closed = new Subject<void>()
private zone: NgZone
private logger: Logger private logger: Logger
constructor (private sftp: SFTPWrapper, injector: Injector) { constructor (private sftp: russh.SFTP, injector: Injector) {
this.zone = injector.get(NgZone)
this.logger = injector.get(LogService).create('sftp') this.logger = injector.get(LogService).create('sftp')
sftp.on('close', () => { sftp.closed$.subscribe(() => {
this.closed.next() this.closed.next()
this.closed.complete() this.closed.complete()
}) })
@ -86,115 +84,119 @@ export class SFTPSession {
async readdir (p: string): Promise<SFTPFile[]> { async readdir (p: string): Promise<SFTPFile[]> {
this.logger.debug('readdir', p) this.logger.debug('readdir', p)
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))()) const entries = await this.sftp.readDirectory(p)
return entries.map(entry => this._makeFile( return entries.map(entry => this._makeFile(
posixPath.join(p, entry.filename), entry, posixPath.join(p, entry.name), entry,
)) ))
} }
readlink (p: string): Promise<string> { readlink (p: string): Promise<string> {
this.logger.debug('readlink', p) throw new Error('Not implemented')
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))()) // this.logger.debug('readlink', p)
// return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
} }
async stat (p: string): Promise<SFTPFile> { async stat (p: string): Promise<SFTPFile> {
this.logger.debug('stat', p) throw new Error('Not implemented')
const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))()) // this.logger.debug('stat', p)
return { // const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
name: posixPath.basename(p), // return {
fullPath: p, // name: posixPath.basename(p),
isDirectory: stats.isDirectory(), // fullPath: p,
isSymlink: stats.isSymbolicLink(), // isDirectory: stats.isDirectory(),
mode: stats.mode, // isSymlink: stats.isSymbolicLink(),
size: stats.size, // mode: stats.mode,
modified: new Date(stats.mtime * 1000), // size: stats.size,
} // modified: new Date(stats.mtime * 1000),
// }
} }
async open (p: string, mode: string): Promise<SFTPFileHandle> { async open (p: string, mode: string): Promise<SFTPFileHandle> {
this.logger.debug('open', p) throw new Error('Not implemented')
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))()) // this.logger.debug('open', p)
return new SFTPFileHandle(this.sftp, handle, this.zone) // 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> { async rmdir (p: string): Promise<void> {
this.logger.debug('rmdir', p) await this.sftp.removeDirectory(p)
await promisify((f: any) => this.sftp.rmdir(p, f))()
} }
async mkdir (p: string): Promise<void> { async mkdir (p: string): Promise<void> {
this.logger.debug('mkdir', p) await this.sftp.createDirectory(p)
await promisify((f: any) => this.sftp.mkdir(p, f))()
} }
async rename (oldPath: string, newPath: string): Promise<void> { async rename (oldPath: string, newPath: string): Promise<void> {
this.logger.debug('rename', oldPath, newPath) throw new Error('Not implemented')
await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))() // this.logger.debug('rename', oldPath, newPath)
// await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
} }
async unlink (p: string): Promise<void> { async unlink (p: string): Promise<void> {
this.logger.debug('unlink', p) await this.sftp.removeFile(p)
await promisify((f: any) => this.sftp.unlink(p, f))()
} }
async chmod (p: string, mode: string|number): Promise<void> { async chmod (p: string, mode: string|number): Promise<void> {
this.logger.debug('chmod', p, mode) throw new Error('Not implemented')
await promisify((f: any) => this.sftp.chmod(p, mode, f))() // this.logger.debug('chmod', p, mode)
// await promisify((f: any) => this.sftp.chmod(p, mode, f))()
} }
async upload (path: string, transfer: FileUpload): Promise<void> { async upload (path: string, transfer: FileUpload): Promise<void> {
this.logger.info('Uploading into', path) throw new Error('Not implemented')
const tempPath = path + '.tabby-upload' // this.logger.info('Uploading into', path)
try { // const tempPath = path + '.tabby-upload'
const handle = await this.open(tempPath, 'w') // try {
while (true) { // const handle = await this.open(tempPath, 'w')
const chunk = await transfer.read() // while (true) {
if (!chunk.length) { // const chunk = await transfer.read()
break // if (!chunk.length) {
} // break
await handle.write(chunk) // }
} // await handle.write(chunk)
handle.close() // }
try { // handle.close()
await this.unlink(path) // try {
} catch { } // await this.unlink(path)
await this.rename(tempPath, path) // } catch { }
transfer.close() // await this.rename(tempPath, path)
} catch (e) { // transfer.close()
transfer.cancel() // } catch (e) {
this.unlink(tempPath) // transfer.cancel()
throw e // this.unlink(tempPath)
} // throw e
// }
} }
async download (path: string, transfer: FileDownload): Promise<void> { async download (path: string, transfer: FileDownload): Promise<void> {
this.logger.info('Downloading', path) throw new Error('Not implemented')
try { // this.logger.info('Downloading', path)
const handle = await this.open(path, 'r') // try {
while (true) { // const handle = await this.open(path, 'r')
const chunk = await handle.read() // while (true) {
if (!chunk.length) { // const chunk = await handle.read()
break // if (!chunk.length) {
} // break
await transfer.write(chunk) // }
} // await transfer.write(chunk)
transfer.close() // }
handle.close() // transfer.close()
} catch (e) { // handle.close()
transfer.cancel() // } catch (e) {
throw e // transfer.cancel()
} // throw e
// }
} }
private _makeFile (p: string, entry: FileEntry): SFTPFile { private _makeFile (p: string, entry: russh.SFTPDirectoryEntry): SFTPFile {
return { return {
fullPath: p, fullPath: p,
name: posixPath.basename(p), name: posixPath.basename(p),
isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR, isDirectory: entry.type === russh.SFTPFileType.Directory,
isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK, isSymlink: entry.type === russh.SFTPFileType.Symlink,
mode: entry.attrs.mode, mode: entry.permissions ?? 0,
size: entry.attrs.size, size: entry.objectSize,
modified: new Date(entry.attrs.mtime * 1000), modified: new Date((entry.mtime ?? 0) * 1000),
} }
} }
} }

View File

@ -64,7 +64,7 @@ export class KeyboardInteractivePrompt {
export class SSHSession { export class SSHSession {
shell?: russh.Channel shell?: russh.Channel
ssh: russh.SSHClient|russh.AuthenticatedSSHClient ssh: russh.SSHClient|russh.AuthenticatedSSHClient
// sftp?: SFTPWrapper sftp?: russh.SFTP
forwardedPorts: ForwardedPort[] = [] forwardedPorts: ForwardedPort[] = []
jumpStream: any jumpStream: any
proxyCommandStream: SSHProxyStream|null = null proxyCommandStream: SSHProxyStream|null = null
@ -100,7 +100,7 @@ export class SSHSession {
private privateKeyImporters: AutoPrivateKeyLocator[] private privateKeyImporters: AutoPrivateKeyLocator[]
constructor ( constructor (
injector: Injector, private injector: Injector,
public profile: SSHProfile, public profile: SSHProfile,
) { ) {
this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`) this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`)
@ -189,11 +189,13 @@ export class SSHSession {
} }
async openSFTP (): Promise<SFTPSession> { async openSFTP (): Promise<SFTPSession> {
throw new Error('Not implemented') if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
// if (!this.sftp) { throw new Error('Cannot open SFTP session before auth')
// this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))()) }
// } if (!this.sftp) {
// return new SFTPSession(this.sftp, this.injector) this.sftp = await this.ssh.openSFTPChannel()
}
return new SFTPSession(this.sftp, this.injector)
} }
async start (): Promise<void> { async start (): Promise<void> {