Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Issue #4946 Basic folder upload for sftp electron. #9891

Merged
merged 12 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tabby-core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
73 changes: 62 additions & 11 deletions tabby-core/src/api/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ export interface FileUploadOptions {
multiple: boolean
}

export class DirectoryUpload {
private childrens: (FileUpload|DirectoryUpload)[] = []

constructor (private name = '') {
// Just set name for now.
}

getName (): string {
return this.name
}

getChildrens (): (FileUpload|DirectoryUpload)[] {
return this.childrens
}

pushChildren (item: FileUpload|DirectoryUpload): void {
this.childrens.push(item)
}
}

export type PlatformTheme = 'light'|'dark'

export abstract class PlatformService {
Expand All @@ -106,23 +126,54 @@ 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<DirectoryUpload> {
const result = new DirectoryUpload()

startUploadFromDragEvent (event: DragEvent, multiple = false): FileUpload[] {
const result: FileUpload[] = []
if (!event.dataTransfer) {
return []
return Promise.resolve(result)
}

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)
this.fileTransferStarted.next(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, childrenFolder)
}
resolve()
})
root.pushChildren(childrenFolder)
} else {
resolve()
}
})
}

const promises: Promise<void>[] = []

const items = event.dataTransfer.items
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files[i]
const transfer = new HTMLFileUpload(file)
this.fileTransferStarted.next(transfer)
result.push(transfer)
if (!multiple) {
break
for (let i = 0; i < items.length; i++) {
const item = items[i].webkitGetAsEntry()
if (item) {
promises.push(traverseFileTree(item))
if (!multiple) {
break
}
}
}
return result
return Promise.all(promises).then(() => result)
}

getConfigPath (): string|null {
Expand Down
10 changes: 4 additions & 6 deletions tabby-core/src/directives/dropZone.directive.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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 */
@Directive({
selector: '[dropZone]',
})
export class DropZoneDirective implements AfterViewInit {
@Output() transfer = new EventEmitter<FileUpload>()
@Output() transfer = new EventEmitter<DirectoryUpload>()
private dropHint?: HTMLElement

constructor (
Expand All @@ -27,11 +27,9 @@ export class DropZoneDirective implements AfterViewInit {
})
}
})
this.el.nativeElement.addEventListener('drop', (event: DragEvent) => {
this.el.nativeElement.addEventListener('drop', async (event: DragEvent) => {
this.removeHint()
for (const transfer of 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()
Expand Down
39 changes: 38 additions & 1 deletion tabby-electron/src/services/platform.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -48,6 +48,21 @@ export class ElectronPlatformService extends PlatformService {
})
}

async getAllFiles (dir: string, root: DirectoryUpload): Promise<DirectoryUpload> {
const items = await fs.readdir(dir, { withFileTypes: true })
for (const item of items) {
if (item.isDirectory()) {
root.pushChildren(await this.getAllFiles(path.join(dir, item.name), new DirectoryUpload(item.name)))
} else {
const file = new ElectronFileUpload(path.join(dir, item.name), this.electron)
root.pushChildren(file)
await wrapPromise(this.zone, file.open())
this.fileTransferStarted.next(file)
}
}
return root
}

readClipboard (): string {
return this.electron.clipboard.readText()
}
Expand Down Expand Up @@ -216,6 +231,28 @@ export class ElectronPlatformService extends PlatformService {
}))
}

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
}

const 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> {
if (!filePath) {
const result = await this.electron.dialog.showSaveDialog(
Expand Down
8 changes: 6 additions & 2 deletions tabby-ssh/src/components/sftpPanel.component.pug
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@

button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='upload()')
i.fas.fa-upload.me-1
div(translate) Upload
div(translate) Upload files

button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='uploadFolder()')
i.fas.fa-upload.me-1
div(translate) Upload folder

button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')}

.body(dropZone, (transfer)='uploadOne($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")'
Expand Down
26 changes: 25 additions & 1 deletion tabby-ssh/src/components/sftpPanel.component.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -180,6 +180,30 @@ export class SFTPPanelComponent {
await Promise.all(transfers.map(t => this.uploadOne(t)))
}

async uploadFolder (): Promise<void> {
const transfer = await this.platform.startUploadDirectory()
await this.uploadOneFolder(transfer)
}

async uploadOneFolder (transfer: DirectoryUpload, accumPath = ''): Promise<void> {
const savedPath = this.path
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 {
await this.sftp.upload(path.posix.join(this.path, accumPath, t.getName()), t)
}
}
if (this.path === savedPath) {
await this.navigate(this.path)
}
}

async uploadOne (transfer: FileUpload): Promise<void> {
const savedPath = this.path
await this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
Expand Down
6 changes: 5 additions & 1 deletion tabby-web/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
Expand Down
Loading