فهرست منبع

Support empty directory.

marko1616 1 سال پیش
والد
کامیت
d6c2c5de31

+ 1 - 1
tabby-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, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform'
+export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload } from './platform'
 export { MenuItemOptions } from './menu'
 export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
 export { HostWindowService } from './hostWindow'

+ 30 - 8
tabby-core/src/api/platform.ts

@@ -85,7 +85,26 @@ export abstract class FileUpload extends FileTransfer {
 
 export interface FileUploadOptions {
     multiple: boolean
-    directory: boolean
+}
+
+export class DirectoryUpload {
+    private childrens: (FileUpload|DirectoryUpload)[] = []
+
+    constructor(private name = '') {
+        // Just set name for now.
+    }
+
+    getName () {
+        return this.name
+    }
+
+    getChildrens () {
+        return this.childrens
+    }
+
+    pushChildren (item: FileUpload|DirectoryUpload) {
+        this.childrens.push(item)
+    }
 }
 
 export type PlatformTheme = 'light'|'dark'
@@ -108,31 +127,34 @@ export abstract class PlatformService {
 
     abstract startDownload (name: string, mode: number, size: number): Promise<FileDownload|null>
     abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
+    abstract startUploadDirectory (paths?: string[]): Promise<DirectoryUpload>
 
-    async startUploadFromDragEvent (event: DragEvent, multiple = false): Promise<FileUpload[]> {
-        const result: FileUpload[] = []
+    async startUploadFromDragEvent (event: DragEvent, multiple = false): Promise<DirectoryUpload> {
+        const result = new DirectoryUpload()
 
         if (!event.dataTransfer) {
-            return Promise.resolve([])
+            return Promise.resolve(result)
         }
 
-        const traverseFileTree = (item: any, path = ''): Promise<void> => {
+        const traverseFileTree = (item: any, root: DirectoryUpload = result): Promise<void> => {
             return new Promise((resolve) => {
                 if (item.isFile) {
                     item.file((file: File) => {
-                        const transfer = new HTMLFileUpload(file, `${path}/${item.name}`)
+                        const transfer = new HTMLFileUpload(file)
                         this.fileTransferStarted.next(transfer)
-                        result.push(transfer)
+                        root.pushChildren(transfer)
                         resolve()
                     })
                 } else if (item.isDirectory) {
                     const dirReader = item.createReader()
+                    const childrenFolder = new DirectoryUpload(item.name)
                     dirReader.readEntries(async (entries: any[]) => {
                         for (const entry of entries) {
-                            await traverseFileTree(entry, `${path}${item.name}/`)
+                            await traverseFileTree(entry, childrenFolder)
                         }
                         resolve()
                     })
+                    root.pushChildren(childrenFolder)
                 } else {
                     resolve()
                 }

+ 3 - 5
tabby-core/src/directives/dropZone.directive.ts

@@ -1,5 +1,5 @@
 import { Directive, Output, ElementRef, EventEmitter, AfterViewInit } from '@angular/core'
-import { FileUpload, PlatformService } from '../api/platform'
+import { DirectoryUpload, PlatformService } from '../api/platform'
 import './dropZone.directive.scss'
 
 /** @hidden */
@@ -7,7 +7,7 @@ import './dropZone.directive.scss'
     selector: '[dropZone]',
 })
 export class DropZoneDirective implements AfterViewInit {
-    @Output() transfer = new EventEmitter<FileUpload>()
+    @Output() transfer = new EventEmitter<DirectoryUpload>()
     private dropHint?: HTMLElement
 
     constructor (
@@ -29,9 +29,7 @@ export class DropZoneDirective implements AfterViewInit {
         })
         this.el.nativeElement.addEventListener('drop', async (event: DragEvent) => {
             this.removeHint()
-            for (const transfer of await this.platform.startUploadFromDragEvent(event, true)) {
-                this.transfer.emit(transfer)
-            }
+                this.transfer.emit(await this.platform.startUploadFromDragEvent(event, true))
         })
         this.el.nativeElement.addEventListener('dragleave', () => {
             this.removeHint()

+ 35 - 33
tabby-electron/src/services/platform.service.ts

@@ -5,7 +5,7 @@ import * as os from 'os'
 import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
 import { execFile } from 'mz/child_process'
 import { Injectable, NgZone } from '@angular/core'
-import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core'
+import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core'
 import { ElectronService } from '../services/electron.service'
 import { ElectronHostWindow } from './hostWindow.service'
 import { ShellIntegrationService } from './shellIntegration.service'
@@ -48,18 +48,19 @@ export class ElectronPlatformService extends PlatformService {
         })
     }
 
-    async getAllFiles (dir: string): Promise<string[]> {
-        let files: string[] = []
+    async getAllFiles (dir: string, root: DirectoryUpload): Promise<DirectoryUpload> {
         const items = await fs.readdir(dir, { withFileTypes: true })
         for (const item of items) {
-            const fullPath = path.posix.join(dir, item.name)
             if (item.isDirectory()) {
-                files = files.concat(await this.getAllFiles(fullPath))
+                root.pushChildren(await this.getAllFiles(path.join(dir, item.name), new DirectoryUpload(item.name)))
             } else {
-                files.push(fullPath)
+                let file = new ElectronFileUpload(path.join(dir, item.name), this.electron)
+                root.pushChildren(file)
+                await wrapPromise(this.zone, file.open())
+                this.fileTransferStarted.next(file)
             }
         }
-        return files
+        return root
     }
 
     readClipboard (): string {
@@ -201,15 +202,12 @@ export class ElectronPlatformService extends PlatformService {
     }
 
     async startUpload (options?: FileUploadOptions, paths?: string[]): Promise<FileUpload[]> {
-        options ??= { multiple: false, directory: false }
+        options ??= { multiple: false }
 
         const properties: any[] = ['openFile', 'treatPackageAsDirectory']
         if (options.multiple) {
             properties.push('multiSelections')
         }
-        if (options.directory) {
-            properties.push('openDirectory')
-        }
 
         if (!paths) {
             const result = await this.electron.dialog.showOpenDialog(
@@ -225,30 +223,34 @@ export class ElectronPlatformService extends PlatformService {
             paths = result.filePaths
         }
 
-        if(options.directory) {
-            let fileInfos: { fullPath: string, relativePath: string }[] = []
-            for (const folderPath of paths) {
-                const files = await this.getAllFiles(folderPath)
-                fileInfos = fileInfos.concat(files.map(file => ({
-                    fullPath: file,
-                    relativePath: path.posix.join(path.basename(folderPath), path.posix.relative(folderPath, file)),
-                })))
-            }
+        return Promise.all(paths.map(async p => {
+            const transfer = new ElectronFileUpload(p, this.electron)
+            await wrapPromise(this.zone, transfer.open())
+            this.fileTransferStarted.next(transfer)
+            return transfer
+        }))
+    }
 
-            return Promise.all(fileInfos.map(async (fileInfo) => {
-                const transfer = new ElectronFileUpload(fileInfo.fullPath, this.electron, fileInfo.relativePath)
-                await wrapPromise(this.zone, transfer.open())
-                this.fileTransferStarted.next(transfer)
-                return transfer
-            }))
-        } else {
-            return Promise.all(paths.map(async p => {
-                const transfer = new ElectronFileUpload(p, this.electron)
-                await wrapPromise(this.zone, transfer.open())
-                this.fileTransferStarted.next(transfer)
-                return transfer
-            }))
+    async startUploadDirectory (paths?: string[]): Promise<DirectoryUpload> {
+        const properties: any[] = ['openFile', 'treatPackageAsDirectory', 'openDirectory']
+
+        if (!paths) {
+            const result = await this.electron.dialog.showOpenDialog(
+                this.hostWindow.getWindow(),
+                {
+                    buttonLabel: this.translate.instant('Select'),
+                    properties,
+                },
+            )
+            if (result.canceled) {
+                return new DirectoryUpload()
+            }
+            paths = result.filePaths
         }
+
+        let root = new DirectoryUpload()
+        root.pushChildren(await this.getAllFiles(paths[0].split(path.sep).join(path.posix.sep),new DirectoryUpload(path.basename(paths[0]))))
+        return root
     }
 
     async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {

+ 1 - 1
tabby-electron/src/sftpContextMenu.ts

@@ -54,7 +54,7 @@ export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
             if (event === 'rename') {
                 watcher.close()
             }
-            const upload = await this.platform.startUpload({ multiple: false, directory: false }, [tempPath])
+            const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
             if (!upload.length) {
                 return
             }

+ 1 - 1
tabby-ssh/src/components/sftpPanel.component.pug

@@ -31,7 +31,7 @@
 
     button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')}
 
-.body(dropZone, (transfer)='uploadOneWithFolder($event)')
+.body(dropZone, (transfer)='uploadOneFolder($event)')
     a.alert.alert-info.d-flex.align-items-center(
         *ngIf='shouldShowCWDTip && !cwdDetectionAvailable',
         (click)='platform.openExternal("https://tabby.sh/go/cwd-detection")'

+ 13 - 24
tabby-ssh/src/components/sftpPanel.component.ts

@@ -1,7 +1,7 @@
 import * as C from 'constants'
 import { posix as path } from 'path'
 import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
-import { FileUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
+import { FileUpload, DirectoryUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
 import { SFTPSession, SFTPFile } from '../session/sftp'
 import { SSHSession } from '../session/ssh'
 import { SFTPContextMenuItemProvider } from '../api'
@@ -176,40 +176,29 @@ export class SFTPPanelComponent {
     }
 
     async upload (): Promise<void> {
-        const transfers = await this.platform.startUpload({ multiple: true, directory: false })
+        const transfers = await this.platform.startUpload({ multiple: true })
         await Promise.all(transfers.map(t => this.uploadOne(t)))
     }
 
     async uploadFolder (): Promise<void> {
-        const transfers = await this.platform.startUpload({ multiple: true, directory: true })
-        await Promise.all(transfers.map(t => this.uploadOneWithFolder(t)))
+        const transfer = await this.platform.startUploadDirectory()
+        await this.uploadOneFolder(transfer)
     }
 
-    async uploadOneWithFolder (transfer: FileUpload): Promise<void> {
+    async uploadOneFolder (transfer: DirectoryUpload, accumPath = ''): Promise<void> {
         const savedPath = this.path
-        const RelativePath = transfer.getRelativePath()
-        if (RelativePath == null) {
-            return
-        }
-
-        try {
-            await this.sftp.stat(path.join(this.path, RelativePath))
-        } catch (e) {
-            if (e instanceof Error && e.message.includes('No such file')) {
-                let accumPath = ''
-                for (const pathParts of path.posix.dirname(RelativePath).split(path.posix.sep)) {
-                    accumPath = path.posix.join(accumPath, pathParts)
-                    try {
-                        await this.sftp.mkdir(path.join(this.path, accumPath))
-                    } catch {
-                        // Intentionally ignoring errors from making duplicate dirs.
-                    }
+        for(const t of transfer.getChildrens()) {
+            if (t instanceof DirectoryUpload) {
+                try {
+                    await this.sftp.mkdir(path.posix.join(this.path, accumPath, t.getName()))
+                } catch {
+                    // Intentionally ignoring errors from making duplicate dirs.
                 }
+                await this.uploadOneFolder(t, path.posix.join(accumPath,t.getName()))
             } else {
-                throw e
+                await this.sftp.upload(path.posix.join(this.path, accumPath, t.getName()), t)
             }
         }
-        await this.sftp.upload(path.join(this.path, RelativePath), transfer)
         if (this.path === savedPath) {
             await this.navigate(this.path)
         }

+ 1 - 1
tabby-terminal/src/features/zmodem.ts

@@ -74,7 +74,7 @@ class ZModemMiddleware extends SessionMiddleware {
         this.logger.info('new session', zsession)
 
         if (zsession.type === 'send') {
-            const transfers = await this.platform.startUpload({ multiple: true, directory: false })
+            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) {

+ 5 - 1
tabby-web/src/platform.ts

@@ -2,7 +2,7 @@ import '@vaadin/vaadin-context-menu'
 import copyToClipboard from 'copy-text-to-clipboard'
 import { Injectable, Inject } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, HTMLFileUpload } from 'tabby-core'
+import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, HTMLFileUpload, DirectoryUpload } from 'tabby-core'
 
 // eslint-disable-next-line no-duplicate-imports
 import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu'
@@ -135,6 +135,10 @@ export class WebPlatformService extends PlatformService {
         })
     }
 
+    async startUploadDirectory (paths?: string[]): Promise<DirectoryUpload> {
+        return new DirectoryUpload()
+    }
+
     setErrorHandler (handler: (_: any) => void): void {
         window.addEventListener('error', handler)
     }