Skip to content

Commit

Permalink
Split Download and Copy Download Link.
Browse files Browse the repository at this point in the history
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. #5466 (comment)

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
  • Loading branch information
Akos Kitta committed Jul 10, 2019
1 parent 3ecd008 commit 81fcfd2
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -32,20 +33,26 @@ export class FileDownloadCommandContribution implements CommandContribution {
protected readonly selectionService: SelectionService;

registerCommands(registry: CommandRegistry): void {
const handler = new UriAwareCommandHandler<URI[]>(this.selectionService, this.downloadHandler(), { multi: true });
registry.registerCommand(FileDownloadCommands.DOWNLOAD, handler);
registry.registerCommand(
FileDownloadCommands.DOWNLOAD,
new UriAwareCommandHandler<URI[]>(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<URI[]>(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<URI[]> {
return {
execute: uris => this.executeDownload(uris),
isEnabled: uris => this.isDownloadEnabled(uris),
isVisible: uris => this.isDownloadVisible(uris),
};
}

protected async executeDownload(uris: URI[]): Promise<void> {
this.downloadService.download(uris);
protected async executeDownload(uris: URI[], options?: { copyLink?: boolean }): Promise<void> {
this.downloadService.download(uris, options);
}

protected isDownloadEnabled(uris: URI[]): boolean {
Expand All @@ -66,4 +73,10 @@ export namespace FileDownloadCommands {
label: 'Download'
};

export const COPY_DOWNLOAD_LINK: Command = {
id: 'file.copyDownloadLink',
category: 'File',
label: 'Copy Download Link'
};

}
52 changes: 31 additions & 21 deletions packages/filesystem/src/browser/download/file-download-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await fetch(`${this.endpoint()}/download/?id=${id}&cancel=true`);
}

async download(uris: URI[]): Promise<void> {
async download(uris: URI[], options?: FileDownloadService.DownloadOptions): Promise<void> {
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;
Expand All @@ -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}]`);
}
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions packages/navigator/src/browser/navigator-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,16 @@ export class FileNavigatorContribution extends AbstractViewContribution<FileNavi
// });

registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: CommonCommands.COPY.id
commandId: CommonCommands.COPY.id,
order: 'a'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: CommonCommands.PASTE.id
commandId: CommonCommands.PASTE.id,
order: 'b'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: FileDownloadCommands.COPY_DOWNLOAD_LINK.id,
order: 'z'
});

registry.registerMenuAction(NavigatorContextMenu.MODIFICATION, {
Expand Down
11 changes: 11 additions & 0 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,18 @@ export class FileMenuContribution implements MenuContribution {
commandId: FileDownloadCommands.DOWNLOAD.id,
order: 'b'
});
}

}

@injectable()
export class EditMenuContribution implements MenuContribution {

registerMenus(registry: MenuModelRegistry) {
registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, {
commandId: FileDownloadCommands.COPY_DOWNLOAD_LINK.id,
order: '9999'
});
}

}
Expand Down
3 changes: 2 additions & 1 deletion packages/workspace/src/browser/workspace-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { VariableContribution } from '@theia/variable-resolver/lib/browser';
import { WorkspaceServer, workspacePath } from '../common';
import { WorkspaceFrontendContribution } from './workspace-frontend-contribution';
import { WorkspaceService } from './workspace-service';
import { WorkspaceCommandContribution, FileMenuContribution } from './workspace-commands';
import { WorkspaceCommandContribution, FileMenuContribution, EditMenuContribution } from './workspace-commands';
import { WorkspaceVariableContribution } from './workspace-variable-contribution';
import { WorkspaceStorageService } from './workspace-storage-service';
import { WorkspaceUriLabelProviderContribution } from './workspace-uri-contribution';
Expand Down Expand Up @@ -73,6 +73,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(WorkspaceCommandContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(WorkspaceCommandContribution);
bind(MenuContribution).to(FileMenuContribution).inSingletonScope();
bind(MenuContribution).to(EditMenuContribution).inSingletonScope();
bind(WorkspaceDeleteHandler).toSelf().inSingletonScope();
bind(WorkspaceDuplicateHandler).toSelf().inSingletonScope();
bind(WorkspaceCompareHandler).toSelf().inSingletonScope();
Expand Down

0 comments on commit 81fcfd2

Please sign in to comment.