mirror of
https://github.com/Eugeny/tabby.git
synced 2024-11-29 17:37:21 +03:00
transfers wip
This commit is contained in:
parent
be767e7480
commit
0f2ba46d67
@ -65,9 +65,6 @@ rules:
|
|||||||
eqeqeq:
|
eqeqeq:
|
||||||
- error
|
- error
|
||||||
- smart
|
- smart
|
||||||
linebreak-style:
|
|
||||||
- error
|
|
||||||
- unix
|
|
||||||
max-depth:
|
max-depth:
|
||||||
- 1
|
- 1
|
||||||
- 5
|
- 5
|
||||||
|
@ -10,7 +10,7 @@ export { Theme } from './theme'
|
|||||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||||
export { SelectorOption } from './selector'
|
export { SelectorOption } from './selector'
|
||||||
export { CLIHandler, CLIEvent } from './cli'
|
export { CLIHandler, CLIEvent } from './cli'
|
||||||
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions } from './platform'
|
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer } from './platform'
|
||||||
export { MenuItemOptions } from './menu'
|
export { MenuItemOptions } from './menu'
|
||||||
export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
|
export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
|
||||||
export { HostWindowService } from './hostWindow'
|
export { HostWindowService } from './hostWindow'
|
||||||
|
@ -18,6 +18,44 @@ export interface MessageBoxResult {
|
|||||||
response: number
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class PlatformService {
|
export abstract class PlatformService {
|
||||||
supportsWindowControls = false
|
supportsWindowControls = false
|
||||||
|
|
||||||
@ -26,6 +64,9 @@ export abstract class PlatformService {
|
|||||||
abstract loadConfig (): Promise<string>
|
abstract loadConfig (): Promise<string>
|
||||||
abstract saveConfig (content: string): Promise<void>
|
abstract saveConfig (content: string): Promise<void>
|
||||||
|
|
||||||
|
abstract startDownload (name: string, size: number): Promise<FileDownload>
|
||||||
|
abstract startUpload (): Promise<FileUpload[]>
|
||||||
|
|
||||||
getConfigPath (): string|null {
|
getConfigPath (): string|null {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -83,12 +83,27 @@ title-bar(
|
|||||||
)
|
)
|
||||||
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
|
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
|
||||||
|
|
||||||
|
.d-flex(ngbDropdown)
|
||||||
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
|
title='File transfers',
|
||||||
|
ngbDropdownToggle
|
||||||
|
) !{require('../icons/download.svg')}
|
||||||
|
.transfers-dropdown-menu(ngbDropdownMenu)
|
||||||
|
.dropdown-header File transfers
|
||||||
|
.dropdown-item.transfer
|
||||||
|
.mr-3 !{require('../icons/download.svg')}
|
||||||
|
.main
|
||||||
|
label file.bin
|
||||||
|
.progress
|
||||||
|
.progress-bar.w-25
|
||||||
|
small 25%
|
||||||
|
button.btn.btn-link !{require('../icons/times.svg')}
|
||||||
|
|
||||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||||
*ngIf='updatesAvailable',
|
*ngIf='updatesAvailable',
|
||||||
title='Update available - Click to install',
|
title='Update available - Click to install',
|
||||||
(click)='updater.update()',
|
(click)='updater.update()'
|
||||||
[fastHtmlBind]='updateIcon'
|
) !{require('../icons/gift.svg')}
|
||||||
)
|
|
||||||
|
|
||||||
window-controls.background(
|
window-controls.background(
|
||||||
*ngIf='config.store.appearance.frame == "thin" \
|
*ngIf='config.store.appearance.frame == "thin" \
|
||||||
|
@ -178,3 +178,25 @@ hotkey-hint {
|
|||||||
::ng-deep .btn-update svg {
|
::ng-deep .btn-update svg {
|
||||||
fill: cyan;
|
fill: cyan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfers-dropdown-menu {
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
.transfer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0 5px 25px;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -59,7 +59,6 @@ export class AppRootComponent {
|
|||||||
@HostBinding('class.no-tabs') noTabs = true
|
@HostBinding('class.no-tabs') noTabs = true
|
||||||
tabsDragging = false
|
tabsDragging = false
|
||||||
unsortedTabs: BaseTabComponent[] = []
|
unsortedTabs: BaseTabComponent[] = []
|
||||||
updateIcon: string
|
|
||||||
updatesAvailable = false
|
updatesAvailable = false
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
|
|
||||||
@ -79,8 +78,6 @@ export class AppRootComponent {
|
|||||||
this.logger = log.create('main')
|
this.logger = log.create('main')
|
||||||
this.logger.info('v', platform.getAppVersion())
|
this.logger.info('v', platform.getAppVersion())
|
||||||
|
|
||||||
this.updateIcon = require('../icons/gift.svg')
|
|
||||||
|
|
||||||
this.hotkeys.matchedHotkey.subscribe((hotkey: string) => {
|
this.hotkeys.matchedHotkey.subscribe((hotkey: string) => {
|
||||||
if (hotkey.startsWith('tab-')) {
|
if (hotkey.startsWith('tab-')) {
|
||||||
const index = parseInt(hotkey.split('-')[1])
|
const index = parseInt(hotkey.split('-')[1])
|
||||||
|
1
terminus-core/src/icons/download.svg
Normal file
1
terminus-core/src/icons/download.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M528 288h-92.1l46.1-46.1c30.1-30.1 8.8-81.9-33.9-81.9h-64V48c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v112h-64c-42.6 0-64.2 51.7-33.9 81.9l46.1 46.1H48c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48zm-400-80h112V48h96v160h112L288 368 128 208zm400 256H48V336h140.1l65.9 65.9c18.8 18.8 49.1 18.7 67.9 0l65.9-65.9H528v128zm-88-64c0-13.3 10.7-24 24-24s24 10.7 24 24-10.7 24-24 24-24-10.7-24-24z"></path></svg>
|
After Width: | Height: | Size: 532 B |
1
terminus-core/src/icons/times.svg
Normal file
1
terminus-core/src/icons/times.svg
Normal file
@ -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 |
@ -190,3 +190,6 @@ $modal-header-border-width: 0;
|
|||||||
$modal-footer-border-color: #222;
|
$modal-footer-border-color: #222;
|
||||||
$modal-footer-border-width: 1px;
|
$modal-footer-border-width: 1px;
|
||||||
$modal-content-border-width: 0;
|
$modal-content-border-width: 0;
|
||||||
|
|
||||||
|
$progress-bar-bg: $table-bg;
|
||||||
|
$progress-height: 3px;
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as path from 'path'
|
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 * as os from 'os'
|
||||||
import promiseIpc from 'electron-promise-ipc'
|
import promiseIpc from 'electron-promise-ipc'
|
||||||
import { execFile } from 'mz/child_process'
|
import { execFile } from 'mz/child_process'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult } from 'terminus-core'
|
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload } from 'terminus-core'
|
||||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||||
|
|
||||||
/* eslint-disable block-scoped-var */
|
/* eslint-disable block-scoped-var */
|
||||||
@ -89,7 +90,7 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadConfig (): Promise<string> {
|
async loadConfig (): Promise<string> {
|
||||||
if (await fs.exists(this.configPath)) {
|
if (fsSync.existsSync(this.configPath)) {
|
||||||
return fs.readFile(this.configPath, 'utf8')
|
return fs.readFile(this.configPath, 'utf8')
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
@ -157,4 +158,58 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
quit (): void {
|
quit (): void {
|
||||||
this.electron.app.exit(0)
|
this.electron.app.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startUpload (): Promise<FileUpload[]> {
|
||||||
|
const result = await this.electron.dialog.showOpenDialog(
|
||||||
|
this.hostApp.getWindow(),
|
||||||
|
{
|
||||||
|
buttonLabel: 'Select',
|
||||||
|
properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (result.canceled) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(result.filePaths.map(async path => {
|
||||||
|
const t = new ElectronFileUpload(path)
|
||||||
|
await t.open()
|
||||||
|
return t
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
console.log(result)
|
||||||
|
return this.buffer.slice(0, result.bytesRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
close (): void {
|
||||||
|
this.file.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import * as ZModem from 'zmodem.js'
|
import * as ZModem from 'zmodem.js'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { filter } from 'rxjs/operators'
|
import { filter } from 'rxjs/operators'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { TerminalDecorator } from '../api/decorator'
|
import { TerminalDecorator } from '../api/decorator'
|
||||||
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
||||||
import { LogService, Logger, ElectronService, HostAppService, HotkeysService } from 'terminus-core'
|
import { LogService, Logger, ElectronService, HostAppService, HotkeysService, PlatformService, FileUpload } from 'terminus-core'
|
||||||
|
|
||||||
const SPACER = ' '
|
const SPACER = ' '
|
||||||
|
|
||||||
@ -23,6 +22,7 @@ export class ZModemDecorator extends TerminalDecorator {
|
|||||||
hotkeys: HotkeysService,
|
hotkeys: HotkeysService,
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
|
private platform: PlatformService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.logger = log.create('zmodem')
|
this.logger = log.create('zmodem')
|
||||||
@ -87,22 +87,13 @@ export class ZModemDecorator extends TerminalDecorator {
|
|||||||
this.logger.info('new session', zsession)
|
this.logger.info('new session', zsession)
|
||||||
|
|
||||||
if (zsession.type === 'send') {
|
if (zsession.type === 'send') {
|
||||||
const result = await this.electron.dialog.showOpenDialog(
|
const transfers = await this.platform.startUpload()
|
||||||
this.hostApp.getWindow(),
|
let filesRemaining = transfers.length
|
||||||
{
|
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
||||||
buttonLabel: 'Send',
|
for (const transfer of transfers) {
|
||||||
properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'],
|
await this.sendFile(terminal, zsession, transfer, filesRemaining, sizeRemaining)
|
||||||
},
|
|
||||||
)
|
|
||||||
if (result.canceled) {
|
|
||||||
zsession.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let filesRemaining = result.filePaths.length
|
|
||||||
for (const filePath of result.filePaths) {
|
|
||||||
await this.sendFile(terminal, zsession, filePath, filesRemaining)
|
|
||||||
filesRemaining--
|
filesRemaining--
|
||||||
|
sizeRemaining -= transfer.getSize()
|
||||||
}
|
}
|
||||||
this.activeSession = null
|
this.activeSession = null
|
||||||
await zsession.close()
|
await zsession.close()
|
||||||
@ -178,44 +169,41 @@ export class ZModemDecorator extends TerminalDecorator {
|
|||||||
stream.end()
|
stream.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendFile (terminal, zsession, filePath, filesRemaining) {
|
private async sendFile (terminal, zsession, transfer: FileUpload, filesRemaining, sizeRemaining) {
|
||||||
const stat = fs.statSync(filePath)
|
|
||||||
const offer = {
|
const offer = {
|
||||||
name: path.basename(filePath),
|
name: transfer.getName(),
|
||||||
size: stat.size,
|
size: transfer.getSize(),
|
||||||
mode: stat.mode,
|
mode: 0o755,
|
||||||
mtime: Math.floor(stat.mtimeMs / 1000),
|
|
||||||
files_remaining: filesRemaining,
|
files_remaining: filesRemaining,
|
||||||
bytes_remaining: stat.size,
|
bytes_remaining: sizeRemaining,
|
||||||
}
|
}
|
||||||
this.logger.info('offering', offer)
|
this.logger.info('offering', offer)
|
||||||
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
|
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
|
||||||
|
|
||||||
const xfer = await zsession.send_offer(offer)
|
const xfer = await zsession.send_offer(offer)
|
||||||
if (xfer) {
|
if (xfer) {
|
||||||
let bytesSent = 0
|
|
||||||
let canceled = false
|
let canceled = false
|
||||||
const stream = fs.createReadStream(filePath)
|
|
||||||
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
||||||
if (terminal.hasFocus) {
|
if (terminal.hasFocus) {
|
||||||
canceled = true
|
canceled = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.on('data', chunk => {
|
while (true) {
|
||||||
if (canceled) {
|
const chunk = await transfer.read()
|
||||||
stream.close()
|
if (canceled || !chunk.length) {
|
||||||
return
|
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([
|
await xfer.send(chunk)
|
||||||
new Promise(resolve => stream.on('end', resolve)),
|
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
|
||||||
this.cancelEvent.toPromise(),
|
}
|
||||||
])
|
|
||||||
|
if (canceled) {
|
||||||
|
transfer.cancel()
|
||||||
|
} else {
|
||||||
|
transfer.close()
|
||||||
|
}
|
||||||
|
|
||||||
await xfer.end()
|
await xfer.end()
|
||||||
|
|
||||||
@ -226,7 +214,6 @@ export class ZModemDecorator extends TerminalDecorator {
|
|||||||
this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
|
this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.close()
|
|
||||||
cancelSubscription.unsubscribe()
|
cancelSubscription.unsubscribe()
|
||||||
} else {
|
} else {
|
||||||
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)
|
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)
|
||||||
|
Loading…
Reference in New Issue
Block a user