mirror of
https://github.com/eugeny/tabby
synced 2026-08-25 07:16:37 +00:00
Merge remote-tracking branch 'origin/file-transfers'
This commit is contained in:
@@ -65,9 +65,6 @@ rules:
|
||||
eqeqeq:
|
||||
- error
|
||||
- smart
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
max-depth:
|
||||
- 1
|
||||
- 5
|
||||
|
||||
@@ -10,7 +10,7 @@ export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
export { SelectorOption } from './selector'
|
||||
export { CLIHandler, CLIEvent } from './cli'
|
||||
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions } from './platform'
|
||||
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, FileUploadOptions } from './platform'
|
||||
export { MenuItemOptions } from './menu'
|
||||
export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
|
||||
export { HostWindowService } from './hostWindow'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuItemOptions } from './menu'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export interface ClipboardContent {
|
||||
@@ -18,14 +19,75 @@ export interface MessageBoxResult {
|
||||
response: number
|
||||
}
|
||||
|
||||
export abstract class FileTransfer {
|
||||
abstract getName (): string
|
||||
abstract getSize (): number
|
||||
abstract close (): void
|
||||
|
||||
getCompletedBytes (): number {
|
||||
return this.completedBytes
|
||||
}
|
||||
|
||||
isComplete (): boolean {
|
||||
return this.completedBytes >= this.getSize()
|
||||
}
|
||||
|
||||
isCancelled (): boolean {
|
||||
return this.cancelled
|
||||
}
|
||||
|
||||
cancel (): void {
|
||||
this.cancelled = true
|
||||
this.close()
|
||||
}
|
||||
|
||||
protected increaseProgress (bytes: number): void {
|
||||
this.completedBytes += bytes
|
||||
}
|
||||
|
||||
private completedBytes = 0
|
||||
private cancelled = false
|
||||
}
|
||||
|
||||
export abstract class FileDownload extends FileTransfer {
|
||||
abstract write (buffer: Buffer): Promise<void>
|
||||
}
|
||||
|
||||
export abstract class FileUpload extends FileTransfer {
|
||||
abstract read (): Promise<Buffer>
|
||||
|
||||
async readAll (): Promise<Buffer> {
|
||||
const buffers: Buffer[] = []
|
||||
while (true) {
|
||||
const buf = await this.read()
|
||||
if (!buf.length) {
|
||||
break
|
||||
}
|
||||
buffers.push(Buffer.from(buf))
|
||||
}
|
||||
return Buffer.concat(buffers)
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileUploadOptions {
|
||||
multiple: boolean
|
||||
}
|
||||
|
||||
export abstract class PlatformService {
|
||||
supportsWindowControls = false
|
||||
|
||||
get fileTransferStarted$ (): Observable<FileTransfer> { return this.fileTransferStarted }
|
||||
|
||||
protected fileTransferStarted = new Subject<FileTransfer>()
|
||||
|
||||
abstract readClipboard (): string
|
||||
abstract setClipboard (content: ClipboardContent): void
|
||||
abstract loadConfig (): Promise<string>
|
||||
abstract saveConfig (content: string): Promise<void>
|
||||
|
||||
abstract startDownload (name: string, size: number): Promise<FileDownload|null>
|
||||
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
|
||||
|
||||
getConfigPath (): string|null {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -57,6 +57,17 @@ title-bar(
|
||||
)
|
||||
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
|
||||
|
||||
.d-flex(
|
||||
*ngIf='activeTransfers.length > 0',
|
||||
ngbDropdown,
|
||||
[(open)]='activeTransfersDropdownOpen'
|
||||
)
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
title='File transfers',
|
||||
ngbDropdownToggle
|
||||
) !{require('../icons/download-solid.svg')}
|
||||
transfers-menu(ngbDropdownMenu, [(transfers)]='activeTransfers')
|
||||
|
||||
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
.btn-group.background
|
||||
@@ -86,9 +97,8 @@ title-bar(
|
||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||
*ngIf='updatesAvailable',
|
||||
title='Update available - Click to install',
|
||||
(click)='updater.update()',
|
||||
[fastHtmlBind]='updateIcon'
|
||||
)
|
||||
(click)='updater.update()'
|
||||
) !{require('../icons/gift.svg')}
|
||||
|
||||
window-controls.background(
|
||||
*ngIf='config.store.appearance.frame == "thin" \
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UpdaterService } from '../services/updater.service'
|
||||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
@@ -59,8 +59,9 @@ export class AppRootComponent {
|
||||
@HostBinding('class.no-tabs') noTabs = true
|
||||
tabsDragging = false
|
||||
unsortedTabs: BaseTabComponent[] = []
|
||||
updateIcon: string
|
||||
updatesAvailable = false
|
||||
activeTransfers: FileTransfer[] = []
|
||||
activeTransfersDropdownOpen = false
|
||||
private logger: Logger
|
||||
|
||||
private constructor (
|
||||
@@ -79,8 +80,6 @@ export class AppRootComponent {
|
||||
this.logger = log.create('main')
|
||||
this.logger.info('v', platform.getAppVersion())
|
||||
|
||||
this.updateIcon = require('../icons/gift.svg')
|
||||
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey: string) => {
|
||||
if (hotkey.startsWith('tab-')) {
|
||||
const index = parseInt(hotkey.split('-')[1])
|
||||
@@ -134,6 +133,11 @@ export class AppRootComponent {
|
||||
this.noTabs = app.tabs.length === 0
|
||||
})
|
||||
|
||||
platform.fileTransferStarted$.subscribe(transfer => {
|
||||
this.activeTransfers.push(transfer)
|
||||
this.activeTransfersDropdownOpen = true
|
||||
})
|
||||
|
||||
config.ready$.toPromise().then(() => {
|
||||
this.leftToolbarButtons = this.getToolbarButtons(false)
|
||||
this.rightToolbarButtons = this.getToolbarButtons(true)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.dropdown-header File transfers
|
||||
.dropdown-item.transfer(*ngFor='let transfer of transfers', (click)='showTransfer(transfer)')
|
||||
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
||||
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
||||
.main
|
||||
label {{transfer.getName()}}
|
||||
.status(*ngIf='transfer.isComplete()')
|
||||
ngb-progressbar(type='success', [value]='100')
|
||||
.status(*ngIf='transfer.isCancelled()')
|
||||
ngb-progressbar(type='danger', [value]='100')
|
||||
.status(*ngIf='!transfer.isComplete() && !transfer.isCancelled()')
|
||||
ngb-progressbar(type='info', [value]='getProgress(transfer)')
|
||||
button.btn.btn-link((click)='removeTransfer(transfer); $event.stopPropagation()') !{require('../icons/times.svg')}
|
||||
@@ -0,0 +1,31 @@
|
||||
:host {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.transfer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0 5px 25px;
|
||||
|
||||
.icon {
|
||||
padding: 4px 10px;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
background: rgba(0,0,0,.25);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-bottom: 7px;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
import { FileDownload, FileTransfer, PlatformService } from '../api/platform'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'transfers-menu',
|
||||
template: require('./transfersMenu.component.pug'),
|
||||
styles: [require('./transfersMenu.component.scss')],
|
||||
})
|
||||
export class TransfersMenuComponent {
|
||||
@Input() transfers: FileTransfer[]
|
||||
@Output() transfersChange = new EventEmitter<FileTransfer[]>()
|
||||
|
||||
constructor (private platform: PlatformService) { }
|
||||
|
||||
isDownload (transfer: FileTransfer): boolean {
|
||||
return transfer instanceof FileDownload
|
||||
}
|
||||
|
||||
getProgress (transfer: FileTransfer): number {
|
||||
return Math.round(100 * transfer.getCompletedBytes() / transfer.getSize())
|
||||
}
|
||||
|
||||
showTransfer (transfer: FileTransfer): void {
|
||||
const fp = transfer['filePath']
|
||||
if (fp) {
|
||||
this.platform.showItemInFolder(fp)
|
||||
}
|
||||
}
|
||||
|
||||
removeTransfer (transfer: FileTransfer): void {
|
||||
if (!transfer.isComplete()) {
|
||||
transfer.cancel()
|
||||
}
|
||||
this.transfers = this.transfers.filter(x => x !== transfer)
|
||||
this.transfersChange.emit(this.transfers)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"></path></svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fad" data-icon="download" class="svg-inline--fa fa-download fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="fa-group"><path class="fa-secondary" fill="currentColor" d="M320 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3a19.37 19.37 0 0 1-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24a23.94 23.94 0 0 1 24-24h80a23.94 23.94 0 0 1 24 24z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M488 352H341.3l-49 49a51.24 51.24 0 0 1-72.6 0l-49-49H24a23.94 23.94 0 0 0-24 24v112a23.94 23.94 0 0 0 24 24h464a23.94 23.94 0 0 0 24-24V376a23.94 23.94 0 0 0-24-24zm-120 96a16 16 0 1 1 16-16 16 16 0 0 1-16 16zm64 0a16 16 0 1 1 16-16 16 16 0 0 1-16 16z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 784 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="times" class="svg-inline--fa fa-times fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"></path></svg>
|
||||
|
After Width: | Height: | Size: 637 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fad" data-icon="upload" class="svg-inline--fa fa-upload fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="fa-group"><path class="fa-secondary" fill="currentColor" d="M488 351.92H352v8a56 56 0 0 1-56 56h-80a56 56 0 0 1-56-56v-8H24a23.94 23.94 0 0 0-24 24v112a23.94 23.94 0 0 0 24 24h464a23.94 23.94 0 0 0 24-24v-112a23.94 23.94 0 0 0-24-24zm-120 132a20 20 0 1 1 20-20 20.06 20.06 0 0 1-20 20zm64 0a20 20 0 1 1 20-20 20.06 20.06 0 0 1-20 20z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M192 359.93v-168h-87.7c-17.8 0-26.7-21.5-14.1-34.11L242.3 5.62a19.37 19.37 0 0 1 27.3 0l152.2 152.2c12.6 12.61 3.7 34.11-14.1 34.11H320v168a23.94 23.94 0 0 1-24 24h-80a23.94 23.94 0 0 1-24-24z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 813 B |
@@ -21,6 +21,7 @@ import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitT
|
||||
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||
|
||||
import { AutofocusDirective } from './directives/autofocus.directive'
|
||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||
@@ -81,6 +82,7 @@ const PROVIDERS = [
|
||||
SplitTabSpannerComponent,
|
||||
UnlockVaultModalComponent,
|
||||
WelcomeTabComponent,
|
||||
TransfersMenuComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
RenameTabModalComponent,
|
||||
|
||||
@@ -242,7 +242,7 @@ export class ConfigService {
|
||||
private migrate (config) {
|
||||
config.version ??= 0
|
||||
if (config.version < 1) {
|
||||
for (const connection of config.ssh?.connections) {
|
||||
for (const connection of config.ssh?.connections ?? []) {
|
||||
if (connection.privateKey) {
|
||||
connection.privateKeys = [connection.privateKey]
|
||||
delete connection.privateKey
|
||||
|
||||
@@ -43,7 +43,10 @@ app-root {
|
||||
.btn-tab-bar {
|
||||
background: transparent;
|
||||
&:hover { background: rgba(0, 0, 0, .25) !important; }
|
||||
&:active { background: rgba(0, 0, 0, .5) !important; }
|
||||
&:active, &[aria-expanded-true] { background: rgba(0, 0, 0, .5) !important; }
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
@@ -386,3 +389,7 @@ search-panel {
|
||||
hr {
|
||||
border-color: $list-group-border-color;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
box-shadow: $dropdown-box-shadow;
|
||||
}
|
||||
|
||||
@@ -190,3 +190,6 @@ $modal-header-border-width: 0;
|
||||
$modal-footer-border-color: #222;
|
||||
$modal-footer-border-width: 1px;
|
||||
$modal-content-border-width: 0;
|
||||
|
||||
$progress-bg: $table-bg;
|
||||
$progress-height: 3px;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as fsSync from 'fs'
|
||||
import * as os from 'os'
|
||||
import promiseIpc from 'electron-promise-ipc'
|
||||
import { execFile } from 'mz/child_process'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult } from 'terminus-core'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions } from 'terminus-core'
|
||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||
|
||||
/* eslint-disable block-scoped-var */
|
||||
@@ -25,6 +26,7 @@ export class ElectronPlatformService extends PlatformService {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
super()
|
||||
this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
|
||||
@@ -89,7 +91,7 @@ export class ElectronPlatformService extends PlatformService {
|
||||
}
|
||||
|
||||
async loadConfig (): Promise<string> {
|
||||
if (await fs.exists(this.configPath)) {
|
||||
if (fsSync.existsSync(this.configPath)) {
|
||||
return fs.readFile(this.configPath, 'utf8')
|
||||
} else {
|
||||
return ''
|
||||
@@ -157,4 +159,127 @@ export class ElectronPlatformService extends PlatformService {
|
||||
quit (): void {
|
||||
this.electron.app.exit(0)
|
||||
}
|
||||
|
||||
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
||||
options ??= { multiple: false }
|
||||
|
||||
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
|
||||
if (options.multiple) {
|
||||
properties.push('multiSelections')
|
||||
}
|
||||
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Select',
|
||||
properties,
|
||||
},
|
||||
)
|
||||
if (result.canceled) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Promise.all(result.filePaths.map(async p => {
|
||||
const transfer = new ElectronFileUpload(p)
|
||||
await this.wrapPromise(transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
return transfer
|
||||
}))
|
||||
}
|
||||
|
||||
async startDownload (name: string, size: number): Promise<FileDownload|null> {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: name,
|
||||
},
|
||||
)
|
||||
if (!result.filePath) {
|
||||
return null
|
||||
}
|
||||
const transfer = new ElectronFileDownload(result.filePath, size)
|
||||
await this.wrapPromise(transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
return transfer
|
||||
}
|
||||
|
||||
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
promise.then(result => {
|
||||
this.zone.run(() => resolve(result))
|
||||
}).catch(error => {
|
||||
this.zone.run(() => reject(error))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class ElectronFileUpload extends FileUpload {
|
||||
private size: number
|
||||
private file: fs.FileHandle
|
||||
private buffer: Buffer
|
||||
|
||||
constructor (private filePath: string) {
|
||||
super()
|
||||
this.buffer = Buffer.alloc(256 * 1024)
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
this.size = (await fs.stat(this.filePath)).size
|
||||
this.file = await fs.open(this.filePath, 'r')
|
||||
}
|
||||
|
||||
getName (): string {
|
||||
return path.basename(this.filePath)
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.size
|
||||
}
|
||||
|
||||
async read (): Promise<Buffer> {
|
||||
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
|
||||
this.increaseProgress(result.bytesRead)
|
||||
return this.buffer.slice(0, result.bytesRead)
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.file.close()
|
||||
}
|
||||
}
|
||||
|
||||
class ElectronFileDownload extends FileDownload {
|
||||
private file: fs.FileHandle
|
||||
|
||||
constructor (
|
||||
private filePath: string,
|
||||
private size: number,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
this.file = await fs.open(this.filePath, 'w')
|
||||
}
|
||||
|
||||
getName (): string {
|
||||
return path.basename(this.filePath)
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.size
|
||||
}
|
||||
|
||||
async write (buffer: Buffer): Promise<void> {
|
||||
let pos = 0
|
||||
while (pos < buffer.length) {
|
||||
const result = await this.file.write(buffer, pos, buffer.length - pos, null)
|
||||
this.increaseProgress(result.bytesWritten)
|
||||
pos += result.bytesWritten
|
||||
}
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.file.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import * as fs from 'fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from '../api/decorator'
|
||||
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
||||
import { ElectronService, HostAppService, PlatformService } from 'terminus-core'
|
||||
import { PlatformService } from 'terminus-core'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class DebugDecorator extends TerminalDecorator {
|
||||
constructor (
|
||||
private electron: ElectronService,
|
||||
private platform: PlatformService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -63,28 +60,21 @@ export class DebugDecorator extends TerminalDecorator {
|
||||
}
|
||||
|
||||
private async loadFile (): Promise<string|null> {
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Load',
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
},
|
||||
)
|
||||
if (result.filePaths.length) {
|
||||
return fs.readFileSync(result.filePaths[0], { encoding: 'utf-8' })
|
||||
const transfer = await this.platform.startUpload()
|
||||
if (!transfer.length) {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
const data = await transfer[0].readAll()
|
||||
transfer[0].close()
|
||||
return data.toString()
|
||||
}
|
||||
|
||||
private async saveFile (content: string, name: string) {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: name,
|
||||
},
|
||||
)
|
||||
if (result.filePath) {
|
||||
fs.writeFileSync(result.filePath, content)
|
||||
const data = Buffer.from(content)
|
||||
const transfer = await this.platform.startDownload(name, data.length)
|
||||
if (transfer) {
|
||||
transfer.write(data)
|
||||
transfer.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import colors from 'ansi-colors'
|
||||
import * as ZModem from 'zmodem.js'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { filter, first } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from '../api/decorator'
|
||||
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
||||
import { LogService, Logger, ElectronService, HostAppService, HotkeysService } from 'terminus-core'
|
||||
import { LogService, Logger, HotkeysService, PlatformService, FileUpload } from 'terminus-core'
|
||||
|
||||
const SPACER = ' '
|
||||
|
||||
@@ -21,8 +19,7 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
constructor (
|
||||
log: LogService,
|
||||
hotkeys: HotkeysService,
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
private platform: PlatformService,
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('zmodem')
|
||||
@@ -87,22 +84,13 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
this.logger.info('new session', zsession)
|
||||
|
||||
if (zsession.type === 'send') {
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Send',
|
||||
properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'],
|
||||
},
|
||||
)
|
||||
if (result.canceled) {
|
||||
zsession.close()
|
||||
return
|
||||
}
|
||||
|
||||
let filesRemaining = result.filePaths.length
|
||||
for (const filePath of result.filePaths) {
|
||||
await this.sendFile(terminal, zsession, filePath, filesRemaining)
|
||||
const transfers = await this.platform.startUpload({ multiple: true })
|
||||
let filesRemaining = transfers.length
|
||||
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
||||
for (const transfer of transfers) {
|
||||
await this.sendFile(terminal, zsession, transfer, filesRemaining, sizeRemaining)
|
||||
filesRemaining--
|
||||
sizeRemaining -= transfer.getSize()
|
||||
}
|
||||
this.activeSession = null
|
||||
await zsession.close()
|
||||
@@ -125,20 +113,14 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
} = xfer.get_details()
|
||||
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + details.name, true)
|
||||
this.logger.info('offered', xfer)
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: details.name,
|
||||
},
|
||||
)
|
||||
if (!result.filePath) {
|
||||
|
||||
const transfer = await this.platform.startDownload(details.name, details.size)
|
||||
if (!transfer) {
|
||||
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + details.name)
|
||||
xfer.skip()
|
||||
return
|
||||
}
|
||||
|
||||
const stream = fs.createWriteStream(result.filePath)
|
||||
let bytesSent = 0
|
||||
let canceled = false
|
||||
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
||||
if (terminal.hasFocus) {
|
||||
@@ -156,18 +138,19 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
if (canceled) {
|
||||
return
|
||||
}
|
||||
stream.write(Buffer.from(chunk))
|
||||
bytesSent += chunk.length
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true)
|
||||
transfer.write(Buffer.from(chunk))
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true)
|
||||
},
|
||||
}),
|
||||
this.cancelEvent.toPromise(),
|
||||
this.cancelEvent.pipe(first()).toPromise(),
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (canceled) {
|
||||
transfer.cancel()
|
||||
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name)
|
||||
} else {
|
||||
transfer.close()
|
||||
this.showMessage(terminal, colors.bgGreen.black(' Received ') + ' ' + details.name)
|
||||
}
|
||||
} catch {
|
||||
@@ -175,47 +158,45 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
}
|
||||
|
||||
cancelSubscription.unsubscribe()
|
||||
stream.end()
|
||||
}
|
||||
|
||||
private async sendFile (terminal, zsession, filePath, filesRemaining) {
|
||||
const stat = fs.statSync(filePath)
|
||||
private async sendFile (terminal, zsession, transfer: FileUpload, filesRemaining, sizeRemaining) {
|
||||
const offer = {
|
||||
name: path.basename(filePath),
|
||||
size: stat.size,
|
||||
mode: stat.mode,
|
||||
mtime: Math.floor(stat.mtimeMs / 1000),
|
||||
name: transfer.getName(),
|
||||
size: transfer.getSize(),
|
||||
mode: 0o755,
|
||||
files_remaining: filesRemaining,
|
||||
bytes_remaining: stat.size,
|
||||
bytes_remaining: sizeRemaining,
|
||||
}
|
||||
this.logger.info('offering', offer)
|
||||
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
|
||||
|
||||
const xfer = await zsession.send_offer(offer)
|
||||
if (xfer) {
|
||||
let bytesSent = 0
|
||||
let canceled = false
|
||||
const stream = fs.createReadStream(filePath)
|
||||
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
||||
if (terminal.hasFocus) {
|
||||
canceled = true
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (canceled) {
|
||||
stream.close()
|
||||
return
|
||||
while (true) {
|
||||
const chunk = await transfer.read()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (canceled || !chunk.length) {
|
||||
break
|
||||
}
|
||||
xfer.send(chunk)
|
||||
bytesSent += chunk.length
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
new Promise(resolve => stream.on('end', resolve)),
|
||||
this.cancelEvent.toPromise(),
|
||||
])
|
||||
await xfer.send(chunk)
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (canceled) {
|
||||
transfer.cancel()
|
||||
} else {
|
||||
transfer.close()
|
||||
}
|
||||
|
||||
await xfer.end()
|
||||
|
||||
@@ -226,9 +207,9 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
|
||||
}
|
||||
|
||||
stream.close()
|
||||
cancelSubscription.unsubscribe()
|
||||
} else {
|
||||
transfer.cancel()
|
||||
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)
|
||||
this.logger.warn('rejected by the other side')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user