-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GH-1905: Implemented the file download functionality.
Closes #1905. Signed-off-by: Akos Kitta <kittaakos@gmail.com>
- Loading branch information
Showing
19 changed files
with
904 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,4 +53,4 @@ | |
"devDependencies": { | ||
"@theia/cli": "^0.3.11" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,4 +52,4 @@ | |
"devDependencies": { | ||
"@theia/cli": "^0.3.11" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
packages/filesystem/src/browser/download/file-download-command-contribution.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* | ||
* Copyright (C) 2018 TypeFox and others. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
*/ | ||
|
||
import { inject, injectable } from 'inversify'; | ||
import URI from '@theia/core/lib/common/uri'; | ||
import { notEmpty } from '@theia/core/lib/common/objects'; | ||
import { UriSelection } from '@theia/core/lib/common/selection'; | ||
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 { FileDownloadService } from './file-download-service'; | ||
|
||
@injectable() | ||
export class FileDownloadCommandContribution implements CommandContribution { | ||
|
||
@inject(FileDownloadService) | ||
protected readonly downloadService: FileDownloadService; | ||
|
||
@inject(SelectionService) | ||
protected readonly selectionService: SelectionService; | ||
|
||
registerCommands(registry: CommandRegistry): void { | ||
const options = { | ||
multi: true, | ||
isValid: this.isValid.bind(this) | ||
}; | ||
const handler = new UriAwareCommandHandler<URI[]>(this.selectionService, this.downloadHandler(), options); | ||
registry.registerCommand(FileDownloadCommands.DOWNLOAD, handler); | ||
} | ||
|
||
protected downloadHandler(): UriCommandHandler<URI[]> { | ||
return { | ||
execute: uri => this.executeDownload(uri), | ||
isEnabled: (uri, args) => this.isDownloadEnabled(uri, args), | ||
isVisible: (uri, args) => this.isDownloadVisible(uri, args), | ||
}; | ||
} | ||
|
||
protected async executeDownload(uris: URI[]): Promise<void> { | ||
this.downloadService.download(uris); | ||
} | ||
|
||
// tslint:disable-next-line:no-any | ||
protected isDownloadEnabled(uri: Object | undefined, ...args: any[]): boolean { | ||
return this.getUris(uri).length > 0; | ||
} | ||
|
||
// tslint:disable-next-line:no-any | ||
protected isDownloadVisible(uri: Object | undefined, ...args: any[]): boolean { | ||
return this.isDownloadEnabled(uri, args); | ||
} | ||
|
||
protected isValid(uris: URI[]): boolean { | ||
return uris.every(u => u.scheme === 'file'); | ||
} | ||
|
||
protected getUris(uri: Object | undefined): URI[] { | ||
if (uri === undefined) { | ||
return []; | ||
} | ||
return (Array.isArray(uri) ? uri : [uri]).map(u => this.getUri(u)).filter(notEmpty); | ||
} | ||
|
||
protected getUri(uri: Object | undefined): URI | undefined { | ||
if (uri instanceof URI) { | ||
return uri; | ||
} | ||
if (UriSelection.is(uri)) { | ||
return uri.uri; | ||
} | ||
return undefined; | ||
} | ||
|
||
} | ||
|
||
export namespace FileDownloadCommands { | ||
|
||
export const DOWNLOAD: Command = { | ||
id: 'file.download' | ||
}; | ||
|
||
} |
16 changes: 16 additions & 0 deletions
16
packages/filesystem/src/browser/download/file-download-frontend-module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/* | ||
* Copyright (C) 2018 TypeFox and others. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
*/ | ||
|
||
import { ContainerModule } from 'inversify'; | ||
import { CommandContribution } from '@theia/core/lib/common/command'; | ||
import { FileDownloadService } from './file-download-service'; | ||
import { FileDownloadCommandContribution } from './file-download-command-contribution'; | ||
|
||
export default new ContainerModule(bind => { | ||
bind(FileDownloadService).toSelf().inSingletonScope(); | ||
bind(CommandContribution).to(FileDownloadCommandContribution).inSingletonScope(); | ||
}); |
123 changes: 123 additions & 0 deletions
123
packages/filesystem/src/browser/download/file-download-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* | ||
* Copyright (C) 2018 TypeFox and others. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
*/ | ||
|
||
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 { FileSystem } from '../../common/filesystem'; | ||
import { FileDownloadData } from '../../common/download/file-download-data'; | ||
|
||
@injectable() | ||
export class FileDownloadService { | ||
|
||
protected anchor: HTMLAnchorElement | undefined; | ||
|
||
@inject(ILogger) | ||
protected readonly logger: ILogger; | ||
|
||
@inject(FileSystem) | ||
protected readonly fileSystem: FileSystem; | ||
|
||
async download(uris: URI[]): Promise<void> { | ||
if (uris.length === 0) { | ||
return; | ||
} | ||
try { | ||
const response = await fetch(this.request(uris)); | ||
const title = await this.title(response, uris); | ||
const { status, statusText } = response; | ||
if (status === 200) { | ||
this.forceDownload(response, 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); | ||
} | ||
} | ||
|
||
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'); | ||
this.anchor.style.display = 'none'; | ||
} | ||
this.anchor.href = url; | ||
this.anchor.download = title; | ||
this.anchor.click(); | ||
} finally { | ||
if (url) { | ||
URL.revokeObjectURL(url); | ||
} | ||
} | ||
} | ||
|
||
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); | ||
return new Request(url, init); | ||
} | ||
|
||
protected requestInit(uris: URI[]): RequestInit { | ||
if (uris.length === 1) { | ||
return { | ||
body: undefined, | ||
method: 'GET' | ||
}; | ||
} | ||
return { | ||
method: 'PUT', | ||
body: JSON.stringify(this.body(uris)), | ||
headers: new Headers({ 'Content-Type': 'application/json' }), | ||
}; | ||
} | ||
|
||
protected body(uris: URI[]): FileDownloadData { | ||
return { | ||
uris: uris.map(u => u.toString(true)) | ||
}; | ||
} | ||
|
||
protected url(uris: URI[]): string { | ||
const endpoint = this.endpoint(); | ||
if (uris.length === 1) { | ||
// tslint:disable-next-line:whitespace | ||
const [uri,] = uris; | ||
return `${endpoint}/?uri=${uri.toString()}`; | ||
} | ||
return endpoint; | ||
|
||
} | ||
|
||
protected endpoint(): string { | ||
const url = new Endpoint({ path: 'files' }).getRestUrl().toString(); | ||
return url.endsWith('/') ? url.slice(0, -1) : url; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Theia - File Download | ||
|
||
Provides the file download contribution to the `Files` navigator. | ||
|
||
Supports single and multi file downloads. | ||
1. A single file will be downloaded as is. | ||
2. Folders will be downloaded az tar archives. | ||
3. When downloading multiple files, the name of the closest common parent directory will be used for the archive. | ||
4. When downloading multiple files from multiple disks (for instance: `C:\` and `D:\` on Windows), then we apply rule `3.` per disks and we tar the tars. | ||
|
||
### REST API | ||
|
||
- To download a single file or folder use the following endpoint: `GET /files/?uri=/encoded/file/uri/to/the/resource`. | ||
- Example: `curl -X GET http://localhost:3000/files/?uri=file:///Users/akos.kitta/git/theia/package.json`. | ||
|
||
- To download multiple files (from the same folder) use the `PUT /files/` endpoint with the `application/json` content type header and the following body format: | ||
```json | ||
{ | ||
"uri": [ | ||
"/encoded/file/uri/to/the/resource", | ||
"/another/encoded/file/uri/to/the/resource" | ||
] | ||
} | ||
``` | ||
``` | ||
curl -X PUT -H "Content-Type: application/json" -d '{ "uris": ["file:///Users/akos.kitta/git/theia/package.json", "file:///Users/akos.kitta/git/theia/README.md"] }' http://localhost:3000/files/ | ||
``` | ||
|
||
## License | ||
[Apache-2.0](https://github.com/theia-ide/theia/blob/master/LICENSE) |
16 changes: 16 additions & 0 deletions
16
packages/filesystem/src/common/download/file-download-data.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/* | ||
* Copyright (C) 2018 TypeFox and others. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
*/ | ||
|
||
export interface FileDownloadData { | ||
readonly uris: string[]; | ||
} | ||
|
||
export namespace FileDownloadData { | ||
export function is(arg: Object | undefined): arg is FileDownloadData { | ||
return !!arg && 'uris' in arg; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.