Skip to content

Commit d0509cb

Browse files
author
Benjamin Pasero
authored
Web: allow to download from the explorer (microsoft#83220)
* wip * finish
1 parent fcb807c commit d0509cb

3 files changed

Lines changed: 61 additions & 20 deletions

File tree

src/vs/base/browser/dom.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,3 +1212,18 @@ export function asCSSUrl(uri: URI): string {
12121212
}
12131213
return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`;
12141214
}
1215+
1216+
export function triggerDownload(uri: URI, name: string): void {
1217+
// In order to download from the browser, the only way seems
1218+
// to be creating a <a> element with download attribute that
1219+
// points to the file to download.
1220+
// See also https://developers.google.com/web/updates/2011/08/Downloading-resources-in-HTML5-a-download
1221+
const anchor = document.createElement('a');
1222+
document.body.appendChild(anchor);
1223+
anchor.download = name;
1224+
anchor.href = uri.toString(true);
1225+
anchor.click();
1226+
1227+
// Ensure to remove the element from DOM eventually
1228+
setTimeout(() => document.body.removeChild(anchor));
1229+
}

src/vs/workbench/contrib/files/browser/fileActions.contribution.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as nls from 'vs/nls';
77
import { Registry } from 'vs/platform/registry/common/platform';
8-
import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler } from 'vs/workbench/contrib/files/browser/fileActions';
8+
import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL } from 'vs/workbench/contrib/files/browser/fileActions';
99
import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler';
1010
import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions';
1111
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
@@ -14,7 +14,7 @@ import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID,
1414
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
1515
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
1616
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
17-
import { isMacintosh } from 'vs/base/common/platform';
17+
import { isMacintosh, isWeb } from 'vs/base/common/platform';
1818
import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash, ExplorerViewletVisibleContext } from 'vs/workbench/contrib/files/common/files';
1919
import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands';
2020
import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
@@ -23,7 +23,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources';
2323
import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService';
2424
import { URI } from 'vs/base/common/uri';
2525
import { Schemas } from 'vs/base/common/network';
26-
import { IsWebContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
26+
import { WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
2727
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
2828
import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions';
2929
import { ActiveEditorIsSaveableContext } from 'vs/workbench/common/editor';
@@ -218,7 +218,6 @@ export function appendToCommandPalette(id: string, title: ILocalizedString, cate
218218
});
219219
}
220220

221-
const downloadLabel = nls.localize('download', "Download");
222221
appendToCommandPalette(COPY_PATH_COMMAND_ID, { value: nls.localize('copyPathOfActive', "Copy Path of Active File"), original: 'Copy Path of Active File' }, category);
223222
appendToCommandPalette(COPY_RELATIVE_PATH_COMMAND_ID, { value: nls.localize('copyRelativePathOfActive', "Copy Relative Path of Active File"), original: 'Copy Relative Path of Active File' }, category);
224223
appendToCommandPalette(SAVE_FILE_COMMAND_ID, { value: SAVE_FILE_LABEL, original: 'Save' }, category);
@@ -231,7 +230,7 @@ appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, ori
231230
appendToCommandPalette(CLOSE_EDITOR_COMMAND_ID, { value: nls.localize('closeEditor', "Close Editor"), original: 'Close Editor' }, { value: nls.localize('view', "View"), original: 'View' });
232231
appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'New File' }, category, WorkspaceFolderCountContext.notEqualsTo('0'));
233232
appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'New Folder' }, category, WorkspaceFolderCountContext.notEqualsTo('0'));
234-
appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: downloadLabel, original: 'Download' }, category, ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)));
233+
appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file)));
235234

236235
// Menu registration - open editors
237236

@@ -465,15 +464,24 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
465464
when: ExplorerFolderContext
466465
});
467466

468-
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
469-
group: '5_cutcopypaste',
470-
order: 30,
471-
command: {
472-
id: DOWNLOAD_COMMAND_ID,
473-
title: downloadLabel,
474-
},
475-
when: ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file))
476-
});
467+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, (() => {
468+
const downloadMenuItem = {
469+
group: '5_cutcopypaste',
470+
order: 30,
471+
command: {
472+
id: DOWNLOAD_COMMAND_ID,
473+
title: DOWNLOAD_LABEL,
474+
},
475+
when: ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))
476+
};
477+
478+
// Web: currently not supporting download of folders
479+
if (isWeb) {
480+
downloadMenuItem.when = ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), ExplorerFolderContext.toNegated());
481+
}
482+
483+
return downloadMenuItem;
484+
})());
477485

478486
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
479487
group: '6_copypath',

src/vs/workbench/contrib/files/browser/fileActions.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import 'vs/css!./media/fileactions';
77
import * as nls from 'vs/nls';
88
import * as types from 'vs/base/common/types';
9-
import { isWindows } from 'vs/base/common/platform';
9+
import { isWindows, isWeb } from 'vs/base/common/platform';
1010
import * as extpath from 'vs/base/common/extpath';
1111
import { extname, basename } from 'vs/base/common/path';
1212
import * as resources from 'vs/base/common/resources';
@@ -45,6 +45,8 @@ import { coalesce } from 'vs/base/common/arrays';
4545
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
4646
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
4747
import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors';
48+
import { asDomUri, triggerDownload } from 'vs/base/browser/dom';
49+
import { mnemonicButtonLabel } from 'vs/base/common/labels';
4850

4951
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
5052
export const NEW_FILE_LABEL = nls.localize('newFile', "New File");
@@ -62,6 +64,8 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste");
6264

6365
export const FileCopiedContext = new RawContextKey<boolean>('fileCopied', false);
6466

67+
export const DOWNLOAD_LABEL = nls.localize('download', "Download");
68+
6569
const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';
6670

6771
function onError(notificationService: INotificationService, error: any): void {
@@ -1050,11 +1054,25 @@ const downloadFileHandler = (accessor: ServicesAccessor) => {
10501054
if (explorerContext.stat) {
10511055
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
10521056
stats.forEach(async s => {
1053-
const destination = await fileDialogService.showSaveDialog({
1054-
availableFileSystems: [Schemas.file]
1055-
});
1056-
if (destination) {
1057-
await fileService.copy(s.resource, destination);
1057+
if (isWeb) {
1058+
if (!s.isDirectory) {
1059+
triggerDownload(asDomUri(s.resource), s.name);
1060+
}
1061+
} else {
1062+
let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath();
1063+
if (defaultUri && !s.isDirectory) {
1064+
defaultUri = resources.joinPath(defaultUri, s.name);
1065+
}
1066+
1067+
const destination = await fileDialogService.showSaveDialog({
1068+
availableFileSystems: [Schemas.file],
1069+
saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")),
1070+
title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"),
1071+
defaultUri
1072+
});
1073+
if (destination) {
1074+
await fileService.copy(s.resource, destination);
1075+
}
10581076
}
10591077
});
10601078
}

0 commit comments

Comments
 (0)