This commit is contained in:
Eugene
2025-07-23 00:10:29 +02:00
parent 7925d93995
commit 7de2715ab9
5 changed files with 55 additions and 18 deletions

View File

@@ -33,8 +33,16 @@ export abstract class FileTransfer {
return this.completedBytes return this.completedBytes
} }
getStatus (): string {
return this.status
}
getTotalSize (): number {
return this.totalSize
}
isComplete (): boolean { isComplete (): boolean {
return this.completedBytes >= this.getSize() return this.completed
} }
isCancelled (): boolean { isCancelled (): boolean {
@@ -46,6 +54,18 @@ export abstract class FileTransfer {
this.close() 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 { protected increaseProgress (bytes: number): void {
if (!bytes) { if (!bytes) {
return return
@@ -56,9 +76,12 @@ export abstract class FileTransfer {
} }
private completedBytes = 0 private completedBytes = 0
private totalSize = 0
private lastChunkStartTime = Date.now() private lastChunkStartTime = Date.now()
private lastChunkSpeed = 0 private lastChunkSpeed = 0
private cancelled = false private cancelled = false
private completed = false
private status = ''
} }
export abstract class FileDownload extends FileTransfer { export abstract class FileDownload extends FileTransfer {

View File

@@ -5,7 +5,9 @@
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')} .icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')} .icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
.main .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)') ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)')
.metadata .metadata
.size {{transfer.getSize()|filesize}} .size {{transfer.getSize()|filesize}}

View File

@@ -272,7 +272,7 @@ export class ElectronPlatformService extends PlatformService {
} }
async startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null> { async startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null> {
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) { if (!selectedFolder) {
return null return null
} }
@@ -284,7 +284,7 @@ export class ElectronPlatformService extends PlatformService {
counter++ 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()) await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer) this.fileTransferStarted.next(transfer)
return transfer return transfer
@@ -300,11 +300,12 @@ export class ElectronPlatformService extends PlatformService {
}) })
} }
async pickDirectory (title?: string): Promise<string | null> { async pickDirectory (title?: string, buttonLabel?: string): Promise<string | null> {
const result = await this.electron.dialog.showOpenDialog( const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(), this.hostWindow.getWindow(),
{ {
title, title,
buttonLabel,
properties: ['openDirectory', 'showHiddenFiles'], properties: ['openDirectory', 'showHiddenFiles'],
}, },
) )
@@ -340,6 +341,7 @@ class ElectronFileUpload extends FileUpload {
const stat = await fs.stat(this.filePath) const stat = await fs.stat(this.filePath)
this.size = stat.size this.size = stat.size
this.mode = stat.mode this.mode = stat.mode
this.setTotalSize(this.size)
this.file = await fs.open(this.filePath, 'r') this.file = await fs.open(this.filePath, 'r')
} }
@@ -358,6 +360,9 @@ class ElectronFileUpload extends FileUpload {
async read (): Promise<Uint8Array> { async read (): Promise<Uint8Array> {
const result = await this.file.read(this.buffer, 0, this.buffer.length, null) const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
this.increaseProgress(result.bytesRead) this.increaseProgress(result.bytesRead)
if (this.getCompletedBytes() >= this.getSize()) {
this.setCompleted(true)
}
return this.buffer.slice(0, result.bytesRead) return this.buffer.slice(0, result.bytesRead)
} }
@@ -379,6 +384,7 @@ class ElectronFileDownload extends FileDownload {
) { ) {
super() super()
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension') this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
this.setTotalSize(size)
} }
async open (): Promise<void> { async open (): Promise<void> {
@@ -400,6 +406,9 @@ class ElectronFileDownload extends FileDownload {
this.increaseProgress(result.bytesWritten) this.increaseProgress(result.bytesWritten)
pos += result.bytesWritten pos += result.bytesWritten
} }
if (this.getCompletedBytes() >= this.getSize()) {
this.setCompleted(true)
}
} }
close (): void { close (): void {
@@ -414,13 +423,13 @@ class ElectronDirectoryDownload extends DirectoryDownload {
constructor ( constructor (
private basePath: string, private basePath: string,
private name: string, private name: string,
private estimatedSize: number, estimatedSize: number,
private electron: ElectronService, private electron: ElectronService,
private zone: NgZone, private zone: NgZone,
private platformService: ElectronPlatformService,
) { ) {
super() super()
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension') this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
this.setTotalSize(estimatedSize)
} }
async open (): Promise<void> { async open (): Promise<void> {
@@ -432,7 +441,7 @@ class ElectronDirectoryDownload extends DirectoryDownload {
} }
getSize (): number { getSize (): number {
return this.estimatedSize return this.getTotalSize()
} }
async createDirectory (relativePath: string): Promise<void> { async createDirectory (relativePath: string): Promise<void> {
@@ -446,7 +455,6 @@ class ElectronDirectoryDownload extends DirectoryDownload {
const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron) const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron)
await wrapPromise(this.zone, fileDownload.open()) await wrapPromise(this.zone, fileDownload.open())
this.platformService._registerFileTransfer(fileDownload)
return fileDownload return fileDownload
} }

View File

@@ -222,15 +222,19 @@ export class SFTPPanelComponent {
async downloadFolder (folder: SFTPFile): Promise<void> { async downloadFolder (folder: SFTPFile): Promise<void> {
try { try {
const estimatedSize = await this.calculateFolderSize(folder) const transfer = await this.platform.startDownloadDirectory(folder.name, 0)
const transfer = await this.platform.startDownloadDirectory(folder.name, estimatedSize)
if (!transfer) { if (!transfer) {
return return
} }
// Start background size calculation and download simultaneously
const sizeCalculationPromise = this.calculateFolderSizeAndUpdate(folder, transfer)
const downloadPromise = this.downloadFolderRecursive(folder, transfer, '')
try { try {
await this.downloadFolderRecursive(folder, transfer, '') await Promise.all([sizeCalculationPromise, downloadPromise])
transfer.setStatus('')
transfer.setCompleted(true)
} catch (error) { } catch (error) {
transfer.cancel() transfer.cancel()
throw error throw error
@@ -243,15 +247,16 @@ export class SFTPPanelComponent {
} }
} }
private async calculateFolderSize (folder: SFTPFile): Promise<number> { private async calculateFolderSizeAndUpdate (folder: SFTPFile, transfer: DirectoryDownload) {
let totalSize = 0 let totalSize = 0
const items = await this.sftp.readdir(folder.fullPath) const items = await this.sftp.readdir(folder.fullPath)
for (const item of items) { for (const item of items) {
if (item.isDirectory) { if (item.isDirectory) {
totalSize += await this.calculateFolderSize(item) totalSize += await this.calculateFolderSizeAndUpdate(item, transfer)
} else { } else {
totalSize += item.size totalSize += item.size
} }
transfer.setTotalSize(totalSize)
} }
return totalSize return totalSize
} }
@@ -266,13 +271,12 @@ export class SFTPPanelComponent {
const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
transfer.setStatus(itemRelativePath)
if (item.isDirectory) { if (item.isDirectory) {
await transfer.createDirectory(itemRelativePath) await transfer.createDirectory(itemRelativePath)
await this.downloadFolderRecursive(item, transfer, itemRelativePath) await this.downloadFolderRecursive(item, transfer, itemRelativePath)
} else { } else {
// Create file download for this individual file
const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size) const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size)
await this.sftp.download(item.fullPath, fileDownload) await this.sftp.download(item.fullPath, fileDownload)
} }
} }

View File

@@ -35,7 +35,7 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
if (item.isDirectory && this.hostApp.platform !== Platform.Web) { if (item.isDirectory && this.hostApp.platform !== Platform.Web) {
items.push({ items.push({
click: () => panel.downloadFolder(item), click: () => panel.downloadFolder(item),
label: this.translate.instant('Download folder'), label: this.translate.instant('Download directory'),
}) })
} }