From a859baac97890f460e80ed772cdb7ea08f3e6be8 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Thu, 22 Jul 2021 21:35:39 +0200 Subject: [PATCH] fixed #4237 sftp: upload into a temp file first --- tabby-ssh/src/api.ts | 118 +--------------- .../components/sftpDeleteModal.component.ts | 2 +- .../src/components/sftpPanel.component.ts | 8 +- tabby-ssh/src/session/sftp.ts | 127 ++++++++++++++++++ 4 files changed, 135 insertions(+), 120 deletions(-) create mode 100644 tabby-ssh/src/session/sftp.ts diff --git a/tabby-ssh/src/api.ts b/tabby-ssh/src/api.ts index 3072fbc6..f69aebc8 100644 --- a/tabby-ssh/src/api.ts +++ b/tabby-ssh/src/api.ts @@ -1,9 +1,7 @@ import * as fs from 'mz/fs' import * as crypto from 'crypto' import * as path from 'path' -import * as C from 'constants' // eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports -import { posix as posixPath } from 'path' import * as sshpk from 'sshpk' import colors from 'ansi-colors' import stripAnsi from 'strip-ansi' @@ -14,11 +12,11 @@ import { ConfigService, FileProvidersService, HostAppService, NotificationsServi import { BaseSession, LoginScriptsOptions } from 'tabby-terminal' import { Server, Socket, createServer, createConnection } from 'net' import { Client, ClientChannel, SFTPWrapper } from 'ssh2' -import type { FileEntry, Stats } from 'ssh2-streams' import { Subject, Observable } from 'rxjs' import { ProxyCommandStream } from './services/ssh.service' import { PasswordStorageService } from './services/passwordStorage.service' import { promisify } from 'util' +import { SFTPSession } from './session/sftp' const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent' @@ -133,120 +131,6 @@ interface AuthMethod { contents?: Buffer } -export interface SFTPFile { - name: string - fullPath: string - isDirectory: boolean - isSymlink: boolean - mode: number - size: number - modified: Date -} - -export class SFTPFileHandle { - position = 0 - - constructor ( - private sftp: SFTPWrapper, - 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 - } - } - })) - } - - 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 - } - } - })) - } - - close (): Promise { - return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle)) - } -} - -export class SFTPSession { - constructor (private sftp: SFTPWrapper, private zone: NgZone) { } - - async readdir (p: string): Promise { - const entries = await wrapPromise(this.zone, promisify(f => this.sftp.readdir(p, f))()) - return entries.map(entry => this._makeFile( - posixPath.join(p, entry.filename), entry, - )) - } - - readlink (p: string): Promise { - return wrapPromise(this.zone, promisify(f => this.sftp.readlink(p, f))()) - } - - async stat (p: string): Promise { - 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 { - 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 { - await promisify((f: any) => this.sftp.rmdir(p, f))() - } - - async unlink (p: string): Promise { - await promisify((f: any) => this.sftp.unlink(p, f))() - } - - private _makeFile (p: string, entry: FileEntry): 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), - } - } -} - export class SSHSession extends BaseSession { shell?: ClientChannel ssh: Client diff --git a/tabby-ssh/src/components/sftpDeleteModal.component.ts b/tabby-ssh/src/components/sftpDeleteModal.component.ts index b2a362cb..18a88645 100644 --- a/tabby-ssh/src/components/sftpDeleteModal.component.ts +++ b/tabby-ssh/src/components/sftpDeleteModal.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { BaseComponent } from 'tabby-core' -import { SFTPFile, SFTPSession } from '../api' +import { SFTPFile, SFTPSession } from '../session/sftp' /** @hidden */ @Component({ diff --git a/tabby-ssh/src/components/sftpPanel.component.ts b/tabby-ssh/src/components/sftpPanel.component.ts index 6207b7b3..8d3c7dd8 100644 --- a/tabby-ssh/src/components/sftpPanel.component.ts +++ b/tabby-ssh/src/components/sftpPanel.component.ts @@ -1,6 +1,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { SSHSession, SFTPSession, SFTPFile } from '../api' +import { SSHSession } from '../api' +import { SFTPSession, SFTPFile } from '../session/sftp' import { posix as path } from 'path' import * as C from 'constants' import { FileUpload, PlatformService } from 'tabby-core' @@ -96,9 +97,10 @@ export class SFTPPanelComponent { async uploadOne (transfer: FileUpload): Promise { const itemPath = path.join(this.path, transfer.getName()) + const tempPath = itemPath + '.tabby-upload' const savedPath = this.path try { - const handle = await this.sftp.open(itemPath, 'w') + const handle = await this.sftp.open(tempPath, 'w') while (true) { const chunk = await transfer.read() if (!chunk.length) { @@ -107,12 +109,14 @@ export class SFTPPanelComponent { await handle.write(chunk) } handle.close() + await this.sftp.rename(tempPath, itemPath) transfer.close() if (this.path === savedPath) { await this.navigate(this.path) } } catch (e) { transfer.cancel() + this.sftp.unlink(tempPath) throw e } } diff --git a/tabby-ssh/src/session/sftp.ts b/tabby-ssh/src/session/sftp.ts new file mode 100644 index 00000000..d440e53c --- /dev/null +++ b/tabby-ssh/src/session/sftp.ts @@ -0,0 +1,127 @@ +import * as C from 'constants' +// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports +import { posix as posixPath } from 'path' +import { NgZone } from '@angular/core' +import { wrapPromise } from 'tabby-core' +import { SFTPWrapper } from 'ssh2' +import { promisify } from 'util' + +import type { FileEntry, Stats } from 'ssh2-streams' + +export interface SFTPFile { + name: string + fullPath: string + isDirectory: boolean + isSymlink: boolean + mode: number + size: number + modified: Date +} + +export class SFTPFileHandle { + position = 0 + + constructor ( + private sftp: SFTPWrapper, + 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 + } + } + })) + } + + 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 + } + } + })) + } + + close (): Promise { + return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle)) + } +} + +export class SFTPSession { + constructor (private sftp: SFTPWrapper, private zone: NgZone) { } + + async readdir (p: string): Promise { + const entries = await wrapPromise(this.zone, promisify(f => this.sftp.readdir(p, f))()) + return entries.map(entry => this._makeFile( + posixPath.join(p, entry.filename), entry, + )) + } + + readlink (p: string): Promise { + return wrapPromise(this.zone, promisify(f => this.sftp.readlink(p, f))()) + } + + async stat (p: string): Promise { + 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 { + 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 { + await promisify((f: any) => this.sftp.rmdir(p, f))() + } + + async rename (oldPath: string, newPath: string): Promise { + await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))() + } + + async unlink (p: string): Promise { + await promisify((f: any) => this.sftp.unlink(p, f))() + } + + private _makeFile (p: string, entry: FileEntry): 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), + } + } +}