Eugene Pankov 4 lat temu
rodzic
commit
0f2ba46d67

+ 0 - 3
.eslintrc.yml

@@ -65,9 +65,6 @@ rules:
   eqeqeq:
   - error
   - smart
-  linebreak-style:
-  - error
-  - unix
   max-depth:
   - 1
   - 5

+ 1 - 1
terminus-core/src/api/index.ts

@@ -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 } from './platform'
 export { MenuItemOptions } from './menu'
 export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
 export { HostWindowService } from './hostWindow'

+ 41 - 0
terminus-core/src/api/platform.ts

@@ -18,6 +18,44 @@ 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>
+}
+
 export abstract class PlatformService {
     supportsWindowControls = false
 
@@ -26,6 +64,9 @@ export abstract class PlatformService {
     abstract loadConfig (): Promise<string>
     abstract saveConfig (content: string): Promise<void>
 
+    abstract startDownload (name: string, size: number): Promise<FileDownload>
+    abstract startUpload (): Promise<FileUpload[]>
+
     getConfigPath (): string|null {
         return null
     }

+ 18 - 3
terminus-core/src/components/appRoot.component.pug

@@ -83,12 +83,27 @@ title-bar(
                         )
                         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(
                 *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" \

+ 22 - 0
terminus-core/src/components/appRoot.component.scss

@@ -178,3 +178,25 @@ hotkey-hint {
 ::ng-deep .btn-update svg {
     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;
+        }
+    }
+}

+ 0 - 3
terminus-core/src/components/appRoot.component.ts

@@ -59,7 +59,6 @@ export class AppRootComponent {
     @HostBinding('class.no-tabs') noTabs = true
     tabsDragging = false
     unsortedTabs: BaseTabComponent[] = []
-    updateIcon: string
     updatesAvailable = false
     private logger: Logger
 
@@ -79,8 +78,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])

+ 1 - 0
terminus-core/src/icons/download.svg

@@ -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>

+ 1 - 0
terminus-core/src/icons/times.svg

@@ -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>

+ 3 - 0
terminus-core/src/theme.vars.scss

@@ -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-bar-bg: $table-bg;
+$progress-height: 3px;

+ 58 - 3
terminus-electron/src/services/platform.service.ts

@@ -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 { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload } from 'terminus-core'
 const fontManager = require('fontmanager-redux') // eslint-disable-line
 
 /* eslint-disable block-scoped-var */
@@ -89,7 +90,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 +158,58 @@ export class ElectronPlatformService extends PlatformService {
     quit (): void {
         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()
+    }
 }

+ 26 - 39
terminus-terminal/src/features/zmodem.ts

@@ -1,13 +1,12 @@
 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 { 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, ElectronService, HostAppService, HotkeysService, PlatformService, FileUpload } from 'terminus-core'
 
 const SPACER = '            '
 
@@ -23,6 +22,7 @@ export class ZModemDecorator extends TerminalDecorator {
         hotkeys: HotkeysService,
         private electron: ElectronService,
         private hostApp: HostAppService,
+        private platform: PlatformService,
     ) {
         super()
         this.logger = log.create('zmodem')
@@ -87,22 +87,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()
+            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()
@@ -178,44 +169,41 @@ export class ZModemDecorator extends TerminalDecorator {
         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()
+                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)
+            }
+
+            if (canceled) {
+                transfer.cancel()
+            } else {
+                transfer.close()
+            }
 
             await xfer.end()
 
@@ -226,7 +214,6 @@ export class ZModemDecorator extends TerminalDecorator {
                 this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
             }
 
-            stream.close()
             cancelSubscription.unsubscribe()
         } else {
             this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)