mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-27 06:49:53 +00:00
fixed #4237 sftp: upload into a temp file first
This commit is contained in:
parent
b7a676f668
commit
a859baac97
@ -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<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) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
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) { }
|
||||
|
||||
async readdir (p: string): Promise<SFTPFile[]> {
|
||||
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
||||
return entries.map(entry => this._makeFile(
|
||||
posixPath.join(p, entry.filename), entry,
|
||||
))
|
||||
}
|
||||
|
||||
readlink (p: string): Promise<string> {
|
||||
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
||||
}
|
||||
|
||||
async stat (p: string): Promise<SFTPFile> {
|
||||
const stats = await wrapPromise(this.zone, promisify<Stats>(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<SFTPFileHandle> {
|
||||
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> {
|
||||
await promisify((f: any) => this.sftp.rmdir(p, f))()
|
||||
}
|
||||
|
||||
async unlink (p: string): Promise<void> {
|
||||
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
|
||||
|
@ -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({
|
||||
|
@ -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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
127
tabby-ssh/src/session/sftp.ts
Normal file
127
tabby-ssh/src/session/sftp.ts
Normal file
@ -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<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) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
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) { }
|
||||
|
||||
async readdir (p: string): Promise<SFTPFile[]> {
|
||||
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
||||
return entries.map(entry => this._makeFile(
|
||||
posixPath.join(p, entry.filename), entry,
|
||||
))
|
||||
}
|
||||
|
||||
readlink (p: string): Promise<string> {
|
||||
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
||||
}
|
||||
|
||||
async stat (p: string): Promise<SFTPFile> {
|
||||
const stats = await wrapPromise(this.zone, promisify<Stats>(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<SFTPFileHandle> {
|
||||
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> {
|
||||
await promisify((f: any) => this.sftp.rmdir(p, f))()
|
||||
}
|
||||
|
||||
async rename (oldPath: string, newPath: string): Promise<void> {
|
||||
await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
|
||||
}
|
||||
|
||||
async unlink (p: string): Promise<void> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user