diff --git a/tabby-core/src/api/platform.ts b/tabby-core/src/api/platform.ts index be87aa17..f7968c56 100644 --- a/tabby-core/src/api/platform.ts +++ b/tabby-core/src/api/platform.ts @@ -33,8 +33,16 @@ export abstract class FileTransfer { return this.completedBytes } + getStatus (): string { + return this.status + } + + getTotalSize (): number { + return this.totalSize + } + isComplete (): boolean { - return this.completedBytes >= this.getSize() + return this.completed } isCancelled (): boolean { @@ -46,6 +54,18 @@ export abstract class FileTransfer { this.close() } + setStatus (status: string): void { + this.status = status + } + + setTotalSize (size: number): void { + this.totalSize = size + } + + setCompleted (completed: boolean): void { + this.completed = completed + } + protected increaseProgress (bytes: number): void { if (!bytes) { return @@ -56,9 +76,12 @@ export abstract class FileTransfer { } private completedBytes = 0 + private totalSize = 0 private lastChunkStartTime = Date.now() private lastChunkSpeed = 0 private cancelled = false + private completed = false + private status = '' } export abstract class FileDownload extends FileTransfer { diff --git a/tabby-core/src/components/transfersMenu.component.pug b/tabby-core/src/components/transfersMenu.component.pug index 9f1e9814..e2f425e6 100644 --- a/tabby-core/src/components/transfersMenu.component.pug +++ b/tabby-core/src/components/transfersMenu.component.pug @@ -5,7 +5,9 @@ .icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')} .icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')} .main - label.no-wrap([ngbTooltip]='transfer.getName()') {{transfer.getName()}} + label.no-wrap([ngbTooltip]='transfer.getName()') + | {{transfer.getName()}} + span.ms-2.text-muted(*ngIf='transfer.getStatus()') ({{transfer.getStatus()}}) ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)') .metadata .size {{transfer.getSize()|filesize}} diff --git a/tabby-electron/src/services/platform.service.ts b/tabby-electron/src/services/platform.service.ts index b447cb29..a25227d4 100644 --- a/tabby-electron/src/services/platform.service.ts +++ b/tabby-electron/src/services/platform.service.ts @@ -272,7 +272,7 @@ export class ElectronPlatformService extends PlatformService { } async startDownloadDirectory (name: string, estimatedSize?: number): Promise { - const selectedFolder = await this.pickDirectory(this.translate.instant('Select destination folder for {name}', { name })) + const selectedFolder = await this.pickDirectory(this.translate.instant('Select destination folder for {name}', { name }), this.translate.instant('Download here')) if (!selectedFolder) { return null } @@ -284,7 +284,7 @@ export class ElectronPlatformService extends PlatformService { counter++ } - const transfer = new ElectronDirectoryDownload(downloadPath, name, estimatedSize ?? 0, this.electron, this.zone, this) + const transfer = new ElectronDirectoryDownload(downloadPath, name, estimatedSize ?? 0, this.electron, this.zone) await wrapPromise(this.zone, transfer.open()) this.fileTransferStarted.next(transfer) return transfer @@ -300,11 +300,12 @@ export class ElectronPlatformService extends PlatformService { }) } - async pickDirectory (title?: string): Promise { + async pickDirectory (title?: string, buttonLabel?: string): Promise { const result = await this.electron.dialog.showOpenDialog( this.hostWindow.getWindow(), { title, + buttonLabel, properties: ['openDirectory', 'showHiddenFiles'], }, ) @@ -340,6 +341,7 @@ class ElectronFileUpload extends FileUpload { const stat = await fs.stat(this.filePath) this.size = stat.size this.mode = stat.mode + this.setTotalSize(this.size) this.file = await fs.open(this.filePath, 'r') } @@ -358,6 +360,9 @@ class ElectronFileUpload extends FileUpload { async read (): Promise { const result = await this.file.read(this.buffer, 0, this.buffer.length, null) this.increaseProgress(result.bytesRead) + if (this.getCompletedBytes() >= this.getSize()) { + this.setCompleted(true) + } return this.buffer.slice(0, result.bytesRead) } @@ -379,6 +384,7 @@ class ElectronFileDownload extends FileDownload { ) { super() this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension') + this.setTotalSize(size) } async open (): Promise { @@ -400,6 +406,9 @@ class ElectronFileDownload extends FileDownload { this.increaseProgress(result.bytesWritten) pos += result.bytesWritten } + if (this.getCompletedBytes() >= this.getSize()) { + this.setCompleted(true) + } } close (): void { @@ -414,13 +423,13 @@ class ElectronDirectoryDownload extends DirectoryDownload { constructor ( private basePath: string, private name: string, - private estimatedSize: number, + estimatedSize: number, private electron: ElectronService, private zone: NgZone, - private platformService: ElectronPlatformService, ) { super() this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension') + this.setTotalSize(estimatedSize) } async open (): Promise { @@ -432,7 +441,7 @@ class ElectronDirectoryDownload extends DirectoryDownload { } getSize (): number { - return this.estimatedSize + return this.getTotalSize() } async createDirectory (relativePath: string): Promise { @@ -446,7 +455,6 @@ class ElectronDirectoryDownload extends DirectoryDownload { const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron) await wrapPromise(this.zone, fileDownload.open()) - this.platformService._registerFileTransfer(fileDownload) return fileDownload } diff --git a/tabby-ssh/src/components/sftpPanel.component.ts b/tabby-ssh/src/components/sftpPanel.component.ts index dc5b0e9e..d15d6305 100644 --- a/tabby-ssh/src/components/sftpPanel.component.ts +++ b/tabby-ssh/src/components/sftpPanel.component.ts @@ -222,15 +222,19 @@ export class SFTPPanelComponent { async downloadFolder (folder: SFTPFile): Promise { try { - const estimatedSize = await this.calculateFolderSize(folder) - - const transfer = await this.platform.startDownloadDirectory(folder.name, estimatedSize) + const transfer = await this.platform.startDownloadDirectory(folder.name, 0) if (!transfer) { return } + // Start background size calculation and download simultaneously + const sizeCalculationPromise = this.calculateFolderSizeAndUpdate(folder, transfer) + const downloadPromise = this.downloadFolderRecursive(folder, transfer, '') + try { - await this.downloadFolderRecursive(folder, transfer, '') + await Promise.all([sizeCalculationPromise, downloadPromise]) + transfer.setStatus('') + transfer.setCompleted(true) } catch (error) { transfer.cancel() throw error @@ -243,15 +247,16 @@ export class SFTPPanelComponent { } } - private async calculateFolderSize (folder: SFTPFile): Promise { + private async calculateFolderSizeAndUpdate (folder: SFTPFile, transfer: DirectoryDownload) { let totalSize = 0 const items = await this.sftp.readdir(folder.fullPath) for (const item of items) { if (item.isDirectory) { - totalSize += await this.calculateFolderSize(item) + totalSize += await this.calculateFolderSizeAndUpdate(item, transfer) } else { totalSize += item.size } + transfer.setTotalSize(totalSize) } return totalSize } @@ -266,13 +271,12 @@ export class SFTPPanelComponent { const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name + transfer.setStatus(itemRelativePath) if (item.isDirectory) { await transfer.createDirectory(itemRelativePath) await this.downloadFolderRecursive(item, transfer, itemRelativePath) } else { - // Create file download for this individual file const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size) - await this.sftp.download(item.fullPath, fileDownload) } } diff --git a/tabby-ssh/src/sftpContextMenu.ts b/tabby-ssh/src/sftpContextMenu.ts index ab1092f2..72ad17e7 100644 --- a/tabby-ssh/src/sftpContextMenu.ts +++ b/tabby-ssh/src/sftpContextMenu.ts @@ -35,7 +35,7 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider { if (item.isDirectory && this.hostApp.platform !== Platform.Web) { items.push({ click: () => panel.downloadFolder(item), - label: this.translate.instant('Download folder'), + label: this.translate.instant('Download directory'), }) }