From 81fcfd21d0f658dd9c924b06d9ba111e72fa091a Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 14 Jun 2019 11:46:55 +0200 Subject: [PATCH] Split `Download` and `Copy Download Link`. From now on, either the download link can be copied to the clipboard or the download can be triggered without any further user interaction. Note: `Copy Download Link` works in Chrome only. https://github.com/theia-ide/theia/pull/5466#issuecomment-509993140 Signed-off-by: Akos Kitta --- CHANGELOG.md | 1 + .../file-download-command-contribution.ts | 39 +++++++++----- .../browser/download/file-download-service.ts | 52 +++++++++++-------- .../node/download/file-download-handler.ts | 2 +- .../src/browser/navigator-contribution.ts | 10 +++- .../src/browser/workspace-commands.ts | 11 ++++ .../src/browser/workspace-frontend-module.ts | 3 +- 7 files changed, 80 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17671f35f4f2d..c5a6c0f411e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Breaking changes: - [preferences] renamed overridenPreferenceName to overriddenPreferenceName - [task] `cwd`, which used to be defined directly under `Task`, is moved into `Task.options` object - [workspace] `isMultiRootWorkspaceOpened()` is renamed into `isMultiRootWorkspaceEnabled()` +- [filesystem] Changed `FileDownloadService` API to support streaming download of huge files. ## v0.7.0 diff --git a/packages/filesystem/src/browser/download/file-download-command-contribution.ts b/packages/filesystem/src/browser/download/file-download-command-contribution.ts index 5ddaa66c34ecb..0ae76968c99f8 100644 --- a/packages/filesystem/src/browser/download/file-download-command-contribution.ts +++ b/packages/filesystem/src/browser/download/file-download-command-contribution.ts @@ -16,10 +16,11 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; +import { isChrome } from '@theia/core/lib/browser/browser'; import { environment } from '@theia/application-package/lib/environment'; import { SelectionService } from '@theia/core/lib/common/selection-service'; import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; -import { UriAwareCommandHandler, UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { FileDownloadService } from './file-download-service'; @injectable() @@ -32,20 +33,26 @@ export class FileDownloadCommandContribution implements CommandContribution { protected readonly selectionService: SelectionService; registerCommands(registry: CommandRegistry): void { - const handler = new UriAwareCommandHandler(this.selectionService, this.downloadHandler(), { multi: true }); - registry.registerCommand(FileDownloadCommands.DOWNLOAD, handler); + registry.registerCommand( + FileDownloadCommands.DOWNLOAD, + new UriAwareCommandHandler(this.selectionService, { + execute: uris => this.executeDownload(uris), + isEnabled: uris => this.isDownloadEnabled(uris), + isVisible: uris => this.isDownloadVisible(uris), + }, { multi: true }) + ); + registry.registerCommand( + FileDownloadCommands.COPY_DOWNLOAD_LINK, + new UriAwareCommandHandler(this.selectionService, { + execute: uris => this.executeDownload(uris, { copyLink: true }), + isEnabled: uris => isChrome && this.isDownloadEnabled(uris), + isVisible: uris => isChrome && this.isDownloadVisible(uris), + }, { multi: true }) + ); } - protected downloadHandler(): UriCommandHandler { - return { - execute: uris => this.executeDownload(uris), - isEnabled: uris => this.isDownloadEnabled(uris), - isVisible: uris => this.isDownloadVisible(uris), - }; - } - - protected async executeDownload(uris: URI[]): Promise { - this.downloadService.download(uris); + protected async executeDownload(uris: URI[], options?: { copyLink?: boolean }): Promise { + this.downloadService.download(uris, options); } protected isDownloadEnabled(uris: URI[]): boolean { @@ -66,4 +73,10 @@ export namespace FileDownloadCommands { label: 'Download' }; + export const COPY_DOWNLOAD_LINK: Command = { + id: 'file.copyDownloadLink', + category: 'File', + label: 'Copy Download Link' + }; + } diff --git a/packages/filesystem/src/browser/download/file-download-service.ts b/packages/filesystem/src/browser/download/file-download-service.ts index 818a0f5308eba..c6c594a611209 100644 --- a/packages/filesystem/src/browser/download/file-download-service.ts +++ b/packages/filesystem/src/browser/download/file-download-service.ts @@ -38,31 +38,37 @@ export class FileDownloadService { @inject(MessageService) protected readonly messageService: MessageService; - protected handleCopy(event: ClipboardEvent, downloadUrl: string) { - if (downloadUrl) { + protected handleCopy(event: ClipboardEvent, downloadUrl: string): void { + if (downloadUrl && event.clipboardData) { event.clipboardData.setData('text/plain', downloadUrl); event.preventDefault(); - this.messageService.info('Download link copied!'); + this.messageService.info('Copied the download link to the clipboard.'); } } - async cancelDownload(id: string) { + async cancelDownload(id: string): Promise { await fetch(`${this.endpoint()}/download/?id=${id}&cancel=true`); } - async download(uris: URI[]): Promise { + async download(uris: URI[], options?: FileDownloadService.DownloadOptions): Promise { let cancel = false; if (uris.length === 0) { return; } + const copyLink = options && options.copyLink ? true : false; try { - const [progress, response] = await Promise.all([ + const [progress, result] = await Promise.all([ this.messageService.showProgress({ - text: 'Preparing download link...', options: { cancelable: true } + text: `Preparing download${copyLink ? ' link' : ''}...`, options: { cancelable: true } }, () => { cancel = true; }), - fetch(this.request(uris)) + // tslint:disable-next-line:no-any + new Promise<{ response: Response, jsonResponse: any }>(async resolve => { + const resp = await fetch(this.request(uris)); + const jsonResp = await resp.json(); + resolve({ response: resp, jsonResponse: jsonResp }); + }) ]); - const jsonResponse = await response.json(); + const { response, jsonResponse } = result; if (cancel) { this.cancelDownload(jsonResponse.id); return; @@ -71,19 +77,14 @@ export class FileDownloadService { if (status === 200) { progress.cancel(); const downloadUrl = `${this.endpoint()}/download/?id=${jsonResponse.id}`; - this.messageService.info(downloadUrl, 'Download', 'Copy Download Link').then(action => { - if (action === 'Download') { - this.forceDownload(jsonResponse.id, decodeURIComponent(jsonResponse.name)); - this.messageService.info('Download started!'); - } else if (action === 'Copy Download Link') { - if (document.documentElement) { - addClipboardListener(document.documentElement, 'copy', e => this.handleCopy(e, downloadUrl)); - document.execCommand('copy'); - } - } else { - this.cancelDownload(jsonResponse.id); + if (copyLink) { + if (document.documentElement) { + addClipboardListener(document.documentElement, 'copy', e => this.handleCopy(e, downloadUrl)); + document.execCommand('copy'); } - }); + } else { + this.forceDownload(jsonResponse.id, decodeURIComponent(jsonResponse.name)); + } } else { throw new Error(`Received unexpected status code: ${status}. [${statusText}]`); } @@ -163,3 +164,12 @@ export class FileDownloadService { } } + +export namespace FileDownloadService { + export interface DownloadOptions { + /** + * `true` if the download link has to be copied to the clipboard. This will not trigger the actual download. Defaults to `false`. + */ + readonly copyLink?: boolean; + } +} diff --git a/packages/filesystem/src/node/download/file-download-handler.ts b/packages/filesystem/src/node/download/file-download-handler.ts index 123e953168651..d97d3b2efa60e 100644 --- a/packages/filesystem/src/node/download/file-download-handler.ts +++ b/packages/filesystem/src/node/download/file-download-handler.ts @@ -106,7 +106,7 @@ export abstract class FileDownloadHandler { /** * Streams the file and pipe it to the Response to avoid any OOM issues */ - protected streamDownload(status: number, response: Response, stream: fs.ReadStream, id: string) { + protected streamDownload(status: number, response: Response, stream: fs.ReadStream, id: string): void { response.status(status); stream.on('error', error => { this.fileDownloadCache.deleteDownload(id); diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index 3cbb5d1b08980..462993a540b30 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -208,10 +208,16 @@ export class FileNavigatorContribution extends AbstractViewContribution