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

Revert "improve download, allow download of big files, works on ff and chrome" #5708

Closed
wants to merge 2 commits into from
Closed
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
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ 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
2 changes: 2 additions & 0 deletions packages/filesystem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@theia/core": "^0.8.0",
"@types/body-parser": "^1.17.0",
"@types/fs-extra": "^4.0.2",
"@types/mime-types": "^2.1.0",
"@types/rimraf": "^2.0.2",
"@types/tar-fs": "^1.16.1",
"@types/touch": "0.0.1",
Expand All @@ -14,6 +15,7 @@
"drivelist": "^6.4.3",
"fs-extra": "^4.0.2",
"http-status-codes": "^1.3.0",
"mime-types": "^2.1.18",
"minimatch": "^3.0.4",
"mv": "^2.1.1",
"rimraf": "^2.6.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@

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 } from '@theia/core/lib/common/uri-command-handler';
import { UriAwareCommandHandler, UriCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { FileDownloadService } from './file-download-service';

@injectable()
Expand All @@ -33,26 +32,20 @@ export class FileDownloadCommandContribution implements CommandContribution {
protected readonly selectionService: SelectionService;

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

protected async executeDownload(uris: URI[], options?: { copyLink?: boolean }): Promise<void> {
this.downloadService.download(uris, options);
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 isDownloadEnabled(uris: URI[]): boolean {
Expand All @@ -73,10 +66,4 @@ export namespace FileDownloadCommands {
label: 'Download'
};

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

}
106 changes: 53 additions & 53 deletions packages/filesystem/src/browser/download/file-download-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar';
import { FileSystem } from '../../common/filesystem';
import { FileDownloadData } from '../../common/download/file-download-data';
import { MessageService } from '@theia/core/lib/common/message-service';
import { addClipboardListener } from '@theia/core/lib/browser/widgets';

@injectable()
export class FileDownloadService {

protected static PREPARING_DOWNLOAD_ID = 'theia-preparing-download';

protected anchor: HTMLAnchorElement | undefined;
protected downloadQueue: number[] = [];
protected downloadCounter: number = 0;

@inject(ILogger)
Expand All @@ -35,72 +38,60 @@ export class FileDownloadService {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;

@inject(StatusBar)
protected readonly statusBar: StatusBar;

@inject(MessageService)
protected readonly messageService: MessageService;

protected handleCopy(event: ClipboardEvent, downloadUrl: string): void {
if (downloadUrl && event.clipboardData) {
event.clipboardData.setData('text/plain', downloadUrl);
event.preventDefault();
this.messageService.info('Copied the download link to the clipboard.');
}
}

async cancelDownload(id: string): Promise<void> {
await fetch(`${this.endpoint()}/download/?id=${id}&cancel=true`);
}

async download(uris: URI[], options?: FileDownloadService.DownloadOptions): Promise<void> {
let cancel = false;
async download(uris: URI[]): Promise<void> {
if (uris.length === 0) {
return;
}
const copyLink = options && options.copyLink ? true : false;
let downloadId: number | undefined;
try {
const [progress, result] = await Promise.all([
this.messageService.showProgress({
text: `Preparing download${copyLink ? ' link' : ''}...`, options: { cancelable: true }
}, () => { cancel = true; }),
// 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 { response, jsonResponse } = result;
if (cancel) {
this.cancelDownload(jsonResponse.id);
return;
downloadId = this.downloadCounter++;
if (this.downloadQueue.length === 0) {
await this.statusBar.setElement(FileDownloadService.PREPARING_DOWNLOAD_ID, {
alignment: StatusBarAlignment.RIGHT,
text: '$(spinner~spin) Preparing download...',
tooltip: 'Preparing download...',
priority: 1
});
}
this.downloadQueue.push(downloadId);
const response = await fetch(this.request(uris));
await this.statusBar.removeElement(FileDownloadService.PREPARING_DOWNLOAD_ID);
const title = await this.title(response, uris);
const { status, statusText } = response;
if (status === 200) {
progress.cancel();
const downloadUrl = `${this.endpoint()}/download/?id=${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));
}
await this.forceDownload(response, decodeURIComponent(title));
} else {
throw new Error(`Received unexpected status code: ${status}. [${statusText}]`);
}
} catch (e) {
this.logger.error(`Error occurred when downloading: ${uris.map(u => u.toString(true))}.`, e);
} finally {
if (downloadId !== undefined) {
const indexOf = this.downloadQueue.indexOf(downloadId);
if (indexOf !== -1) {
this.downloadQueue.splice(indexOf, 1);
}
if (this.downloadQueue.length === 0) {
this.statusBar.removeElement(FileDownloadService.PREPARING_DOWNLOAD_ID);
}
}
}
}

protected async forceDownload(id: string, title: string): Promise<void> {
protected async forceDownload(response: Response, title: string): Promise<void> {
let url: string | undefined;
try {
const blob = await response.blob();
url = URL.createObjectURL(blob);
if (this.anchor === undefined) {
this.anchor = document.createElement('a');
}
const endpoint = this.endpoint();
url = `${endpoint}/download/?id=${id}`;
this.anchor.href = url;
this.anchor.style.display = 'none';
this.anchor.download = title;
Expand All @@ -117,6 +108,24 @@ export class FileDownloadService {
}
}

protected async title(response: Response, uris: URI[]): Promise<string> {
let title = (response.headers.get('Content-Disposition') || '').split('attachment; filename=').pop();
if (title) {
return title;
}
// tslint:disable-next-line:whitespace
const [uri,] = uris;
if (uris.length === 1) {
const stat = await this.fileSystem.getFileStat(uri.toString());
if (stat === undefined) {
throw new Error(`Unexpected error occurred when downloading file. Files does not exist. URI: ${uri.toString(true)}.`);
}
title = uri.path.base;
return stat.isDirectory ? `${title}.tar` : title;
}
return `${uri.parent.path.name}.tar`;
}

protected request(uris: URI[]): Request {
const url = this.url(uris);
const init = this.requestInit(uris);
Expand Down Expand Up @@ -164,12 +173,3 @@ 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 @@ -17,16 +17,13 @@
import { ContainerModule } from 'inversify';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { FileDownloadEndpoint } from './file-download-endpoint';
import { FileDownloadHandler, SingleFileDownloadHandler, MultiFileDownloadHandler, DownloadLinkHandler } from './file-download-handler';
import { FileDownloadHandler, SingleFileDownloadHandler, MultiFileDownloadHandler } from './file-download-handler';
import { DirectoryArchiver } from './directory-archiver';
import { FileDownloadCache } from './file-download-cache';

export default new ContainerModule(bind => {
bind(FileDownloadEndpoint).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(FileDownloadEndpoint);
bind(FileDownloadCache).toSelf().inSingletonScope();
bind(FileDownloadHandler).to(SingleFileDownloadHandler).inSingletonScope().whenTargetNamed(FileDownloadHandler.SINGLE);
bind(FileDownloadHandler).to(MultiFileDownloadHandler).inSingletonScope().whenTargetNamed(FileDownloadHandler.MULTI);
bind(FileDownloadHandler).to(DownloadLinkHandler).inSingletonScope().whenTargetNamed(FileDownloadHandler.DOWNLOAD_LINK);
bind(DirectoryArchiver).toSelf().inSingletonScope();
});
88 changes: 0 additions & 88 deletions packages/filesystem/src/node/download/file-download-cache.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@ export class FileDownloadEndpoint implements BackendApplicationContribution {
@named(FileDownloadHandler.MULTI)
protected readonly multiFileDownloadHandler: FileDownloadHandler;

@inject(FileDownloadHandler)
@named(FileDownloadHandler.DOWNLOAD_LINK)
protected readonly downloadLinkHandler: FileDownloadHandler;

configure(app: Application): void {
const router = Router();
router.get('/download', (request, response) => this.downloadLinkHandler.handle(request, response));
router.get('/', (request, response) => this.singleFileDownloadHandler.handle(request, response));
router.put('/', (request, response) => this.multiFileDownloadHandler.handle(request, response));
// Content-Type: application/json
Expand Down
Loading