/* eslint-disable @typescript-eslint/no-unused-vars */ import { Subject, Observable } from 'rxjs' import { posix as posixPath } from 'path' import { Injector } from '@angular/core' import { FileDownload, FileUpload, Logger, LogService } from 'tabby-core' import * as russh from 'russh' export interface SFTPFile { name: string fullPath: string isDirectory: boolean isSymlink: boolean mode: number size: number modified: Date } export class SFTPFileHandle { position = 0 constructor ( private inner: russh.SFTPFile|null, ) { } async read (): Promise { if (!this.inner) { return Promise.resolve(new Uint8Array(0)) } return this.inner.read(256 * 1024) } async write (chunk: Uint8Array): Promise { if (!this.inner) { throw new Error('File handle is closed') } await this.inner.writeAll(chunk) } async close (): Promise { await this.inner?.shutdown() this.inner = null } } export class SFTPSession { get closed$ (): Observable { return this.closed } private closed = new Subject() private logger: Logger constructor (private sftp: russh.SFTP, injector: Injector) { this.logger = injector.get(LogService).create('sftp') sftp.closed$.subscribe(() => { this.closed.next() this.closed.complete() }) } async readdir (p: string): Promise { this.logger.debug('readdir', p) const entries = await this.sftp.readDirectory(p) return entries.map(entry => this._makeFile( posixPath.join(p, entry.name), entry, )) } readlink (p: string): Promise { this.logger.debug('readlink', p) return this.sftp.readlink(p) } async stat (p: string): Promise { this.logger.debug('stat', p) const stats = await this.sftp.stat(p) return { name: posixPath.basename(p), fullPath: p, isDirectory: stats.type === russh.SFTPFileType.Directory, isSymlink: stats.type === russh.SFTPFileType.Symlink, mode: stats.permissions ?? 0, size: stats.size, modified: new Date((stats.mtime ?? 0) * 1000), } } async open (p: string, mode: number): Promise { this.logger.debug('open', p, mode) const handle = await this.sftp.open(p, mode) return new SFTPFileHandle(handle) } async rmdir (p: string): Promise { await this.sftp.removeDirectory(p) } async mkdir (p: string): Promise { await this.sftp.createDirectory(p) } async rename (oldPath: string, newPath: string): Promise { this.logger.debug('rename', oldPath, newPath) await this.sftp.rename(oldPath, newPath) } async unlink (p: string): Promise { await this.sftp.removeFile(p) } async chmod (p: string, mode: string|number): Promise { this.logger.debug('chmod', p, mode) await this.sftp.chmod(p, mode) } 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, russh.OPEN_WRITE | russh.OPEN_CREATE) while (true) { const chunk = await transfer.read() if (!chunk.length) { break } await handle.write(chunk) } await handle.close() await this.unlink(path).catch(() => null) await this.rename(tempPath, path) transfer.close() } catch (e) { transfer.cancel() this.unlink(tempPath).catch(() => null) throw e } } async download (path: string, transfer: FileDownload): Promise { this.logger.info('Downloading', path) try { const handle = await this.open(path, russh.OPEN_READ) 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: russh.SFTPDirectoryEntry): SFTPFile { return { fullPath: p, name: posixPath.basename(p), isDirectory: entry.metadata.type === russh.SFTPFileType.Directory, isSymlink: entry.metadata.type === russh.SFTPFileType.Symlink, mode: entry.metadata.permissions ?? 0, size: entry.metadata.size, modified: new Date((entry.metadata.mtime ?? 0) * 1000), } } }