From 1f2bf12ed79f451f0ed267ccc7a3ae7771bc9200 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jul 2024 23:56:33 +0200 Subject: [PATCH] sftp wip --- tabby-ssh/src/session/sftp.ts | 242 +++++++++++++++++----------------- tabby-ssh/src/session/ssh.ts | 16 ++- 2 files changed, 131 insertions(+), 127 deletions(-) diff --git a/tabby-ssh/src/session/sftp.ts b/tabby-ssh/src/session/sftp.ts index c07fb645..60d03d64 100644 --- a/tabby-ssh/src/session/sftp.ts +++ b/tabby-ssh/src/session/sftp.ts @@ -1,12 +1,9 @@ -import * as C from 'constants' +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Subject, Observable } from 'rxjs' import { posix as posixPath } from 'path' -import { Injector, NgZone } from '@angular/core' -import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core' -import { SFTPWrapper } from 'ssh2' -import { promisify } from 'util' - -import type { FileEntry, Stats } from 'ssh2-streams' +import { Injector } from '@angular/core' +import { FileDownload, FileUpload, Logger, LogService } from 'tabby-core' +import * as russh from 'russh' export interface SFTPFile { name: string @@ -21,64 +18,65 @@ export interface SFTPFile { export class SFTPFileHandle { position = 0 - constructor ( - private sftp: SFTPWrapper, - private handle: Buffer, - private zone: NgZone, - ) { } + // constructor ( + // private sftp: russh.SFTP, + // private handle: Buffer, + // private zone: NgZone, + // ) { } read (): Promise { - 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 - } - } - })) + throw new Error('Not implemented') + // 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 { - return wrapPromise(this.zone, new Promise((resolve, reject) => { - while (true) { - const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => { - if (err) { - reject(err) - return - } - this.position += chunk.length - resolve() - }) - if (!wait) { - break - } - } - })) + throw new Error('Not implemented') + // return wrapPromise(this.zone, new Promise((resolve, reject) => { + // while (true) { + // const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => { + // if (err) { + // reject(err) + // return + // } + // this.position += chunk.length + // resolve() + // }) + // if (!wait) { + // break + // } + // } + // })) } close (): Promise { - 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 { get closed$ (): Observable { return this.closed } private closed = new Subject() - private zone: NgZone private logger: Logger - constructor (private sftp: SFTPWrapper, injector: Injector) { - this.zone = injector.get(NgZone) + constructor (private sftp: russh.SFTP, injector: Injector) { this.logger = injector.get(LogService).create('sftp') - sftp.on('close', () => { + sftp.closed$.subscribe(() => { this.closed.next() this.closed.complete() }) @@ -86,115 +84,119 @@ export class SFTPSession { async readdir (p: string): Promise { this.logger.debug('readdir', p) - const entries = await wrapPromise(this.zone, promisify(f => this.sftp.readdir(p, f))()) + const entries = await this.sftp.readDirectory(p) return entries.map(entry => this._makeFile( - posixPath.join(p, entry.filename), entry, + posixPath.join(p, entry.name), entry, )) } readlink (p: string): Promise { - this.logger.debug('readlink', p) - return wrapPromise(this.zone, promisify(f => this.sftp.readlink(p, f))()) + throw new Error('Not implemented') + // this.logger.debug('readlink', p) + // return wrapPromise(this.zone, promisify(f => this.sftp.readlink(p, f))()) } async stat (p: string): Promise { - this.logger.debug('stat', p) - const stats = await wrapPromise(this.zone, promisify(f => this.sftp.stat(p, f))()) - return { - name: posixPath.basename(p), - fullPath: p, - isDirectory: stats.isDirectory(), - isSymlink: stats.isSymbolicLink(), - mode: stats.mode, - size: stats.size, - modified: new Date(stats.mtime * 1000), - } + throw new Error('Not implemented') + // this.logger.debug('stat', p) + // const stats = await wrapPromise(this.zone, promisify(f => this.sftp.stat(p, f))()) + // return { + // name: posixPath.basename(p), + // fullPath: p, + // isDirectory: stats.isDirectory(), + // isSymlink: stats.isSymbolicLink(), + // mode: stats.mode, + // size: stats.size, + // modified: new Date(stats.mtime * 1000), + // } } async open (p: string, mode: string): Promise { - this.logger.debug('open', p) - const handle = await wrapPromise(this.zone, promisify(f => this.sftp.open(p, mode, f))()) - return new SFTPFileHandle(this.sftp, handle, this.zone) + throw new Error('Not implemented') + // this.logger.debug('open', p) + // const handle = await wrapPromise(this.zone, promisify(f => this.sftp.open(p, mode, f))()) + // return new SFTPFileHandle(this.sftp, handle, this.zone) } async rmdir (p: string): Promise { - this.logger.debug('rmdir', p) - await promisify((f: any) => this.sftp.rmdir(p, f))() + await this.sftp.removeDirectory(p) } async mkdir (p: string): Promise { - this.logger.debug('mkdir', p) - await promisify((f: any) => this.sftp.mkdir(p, f))() + await this.sftp.createDirectory(p) } async rename (oldPath: string, newPath: string): Promise { - this.logger.debug('rename', oldPath, newPath) - await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))() + throw new Error('Not implemented') + // this.logger.debug('rename', oldPath, newPath) + // await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))() } async unlink (p: string): Promise { - this.logger.debug('unlink', p) - await promisify((f: any) => this.sftp.unlink(p, f))() + await this.sftp.removeFile(p) } async chmod (p: string, mode: string|number): Promise { - this.logger.debug('chmod', p, mode) - await promisify((f: any) => this.sftp.chmod(p, mode, f))() + throw new Error('Not implemented') + // this.logger.debug('chmod', p, mode) + // await promisify((f: any) => this.sftp.chmod(p, mode, f))() } async upload (path: string, transfer: FileUpload): Promise { - 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() - try { - await this.unlink(path) - } catch { } - await this.rename(tempPath, path) - transfer.close() - } catch (e) { - transfer.cancel() - this.unlink(tempPath) - throw e - } + throw new Error('Not implemented') + // 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() + // try { + // await this.unlink(path) + // } catch { } + // await this.rename(tempPath, path) + // transfer.close() + // } catch (e) { + // transfer.cancel() + // this.unlink(tempPath) + // throw e + // } } async download (path: string, transfer: FileDownload): Promise { - 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 - } + throw new Error('Not implemented') + // 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 { + private _makeFile (p: string, entry: russh.SFTPDirectoryEntry): SFTPFile { return { fullPath: p, name: posixPath.basename(p), - isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR, - isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK, - mode: entry.attrs.mode, - size: entry.attrs.size, - modified: new Date(entry.attrs.mtime * 1000), + isDirectory: entry.type === russh.SFTPFileType.Directory, + isSymlink: entry.type === russh.SFTPFileType.Symlink, + mode: entry.permissions ?? 0, + size: entry.objectSize, + modified: new Date((entry.mtime ?? 0) * 1000), } } } diff --git a/tabby-ssh/src/session/ssh.ts b/tabby-ssh/src/session/ssh.ts index c4605e78..838431a3 100644 --- a/tabby-ssh/src/session/ssh.ts +++ b/tabby-ssh/src/session/ssh.ts @@ -64,7 +64,7 @@ export class KeyboardInteractivePrompt { export class SSHSession { shell?: russh.Channel ssh: russh.SSHClient|russh.AuthenticatedSSHClient - // sftp?: SFTPWrapper + sftp?: russh.SFTP forwardedPorts: ForwardedPort[] = [] jumpStream: any proxyCommandStream: SSHProxyStream|null = null @@ -100,7 +100,7 @@ export class SSHSession { private privateKeyImporters: AutoPrivateKeyLocator[] constructor ( - injector: Injector, + private injector: Injector, public profile: SSHProfile, ) { this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`) @@ -189,11 +189,13 @@ export class SSHSession { } async openSFTP (): Promise { - throw new Error('Not implemented') - // if (!this.sftp) { - // this.sftp = await wrapPromise(this.zone, promisify(f => this.ssh.sftp(f))()) - // } - // return new SFTPSession(this.sftp, this.injector) + if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) { + throw new Error('Cannot open SFTP session before auth') + } + if (!this.sftp) { + this.sftp = await this.ssh.openSFTPChannel() + } + return new SFTPSession(this.sftp, this.injector) } async start (): Promise {