diff --git a/package.json b/package.json index aecebac6fb..b595485dc3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "onCommand:vscode-docker.containers.attachShell", "onCommand:vscode-docker.containers.browse", "onCommand:vscode-docker.containers.configureExplorer", + "onCommand:vscode-docker.containers.downloadFile", "onCommand:vscode-docker.containers.inspect", "onCommand:vscode-docker.containers.openFile", "onCommand:vscode-docker.containers.prune", @@ -128,6 +129,10 @@ "contributes": { "menus": { "commandPalette": [ + { + "command": "vscode-docker.containers.downloadFile", + "when": "never" + }, { "command": "vscode-docker.containers.openFile", "when": "never" @@ -381,6 +386,10 @@ "when": "view == dockerContainers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i", "group": "containers_1_general@5" }, + { + "command": "vscode-docker.containers.downloadFile", + "when": "view == dockerContainers && viewItem == containerFile" + }, { "command": "vscode-docker.containers.openFile", "when": "view == dockerContainers && viewItem == containerFile" @@ -2212,6 +2221,11 @@ "dark": "resources/dark/settings.svg" } }, + { + "command": "vscode-docker.containers.downloadFile", + "title": "%vscode-docker.commands.containers.downloadFile%", + "category": "%vscode-docker.commands.category.dockerContainers%" + }, { "command": "vscode-docker.containers.inspect", "title": "%vscode-docker.commands.containers.inspect%", diff --git a/package.nls.json b/package.nls.json index 610a30c436..94bc0c1fe9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -193,6 +193,7 @@ "vscode-docker.commands.containers.attachShell": "Attach Shell", "vscode-docker.commands.containers.browse": "Open in Browser", "vscode-docker.commands.containers.configureExplorer": "Configure Explorer...", + "vscode-docker.commands.containers.downloadFile": "Download", "vscode-docker.commands.containers.inspect": "Inspect", "vscode-docker.commands.containers.openFile": "Open", "vscode-docker.commands.containers.prune": "Prune...", diff --git a/src/commands/containers/files/downloadContainerFile.ts b/src/commands/containers/files/downloadContainerFile.ts new file mode 100644 index 0000000000..35bc88155d --- /dev/null +++ b/src/commands/containers/files/downloadContainerFile.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IActionContext, UserCancelledError } from 'vscode-azureextensionui'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { FileTreeItem } from '../../../tree/containers/files/FileTreeItem'; +import { multiSelectNodes } from '../../../utils/multiSelectNodes'; + +async function fileExists(file: vscode.Uri): Promise { + try { + // NOTE: The expectation is that stat() throws when the file does not exist. + // Filed https://github.com/microsoft/vscode/issues/112107 to provide + // a better mechanism than trapping exceptions. + await vscode.workspace.fs.stat(file); + + return true; + } catch { + return false; + } +} + +const overwriteFile: vscode.MessageItem = { + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.overwriteFile', 'Overwrite File') +}; + +const skipFile: vscode.MessageItem = { + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.skipFile', 'Skip File') +}; + +const cancelDownload: vscode.MessageItem = { + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.cancelDownload', 'Cancel') +}; + +export async function downloadContainerFile(context: IActionContext, node?: FileTreeItem, nodes?: FileTreeItem[]): Promise { + nodes = await multiSelectNodes( + { ...context, noItemFoundErrorMessage: localize('vscode-docker.commands.containers.files.downloadContainerFile.noFiles', 'No files are available to download.') }, + ext.containersTree, + 'containerFile', + node, + nodes + ); + + const localFolderUris = await vscode.window.showOpenDialog( + { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: localize('vscode-docker.commands.containers.files.downloadContainerFile.openLabel', 'Select'), + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.openTitle', 'Select folder for download') + }); + + if (localFolderUris === undefined || localFolderUris.length === 0) { + throw new UserCancelledError(); + } + + const localFolderUri = localFolderUris[0]; + + const files = nodes.map(n => { + const containerFileUri = n.uri; + const fileName = path.posix.basename(containerFileUri.path); + + return { + containerUri: n.uri.uri, + fileName, + localUri: vscode.Uri.joinPath(localFolderUri, fileName) + }; + }); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.opening', 'Downloading File(s)...') + }, + async (_, token) => { + for (const file of files) { + if (token.isCancellationRequested) { + throw new UserCancelledError(); + } + + const localFileExists = await fileExists(file.localUri); + + if (localFileExists) { + const result = await vscode.window.showWarningMessage( + localize('vscode-docker.commands.containers.files.downloadContainerFile.existingFileWarning', 'The file \'{0}\' already exists in folder \'{1}\'.', file.fileName, localFolderUri.fsPath), + overwriteFile, + skipFile, + cancelDownload); + + if (result === skipFile) { + continue; + } else if (result !== overwriteFile) { + throw new UserCancelledError(); + } + } + + await vscode.workspace.fs.copy(file.containerUri, file.localUri, { overwrite: true }); + } + }); +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index df4b80477a..3f2a138c1b 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -14,6 +14,7 @@ import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainer } from "./containers/browseContainer"; import { composeGroupDown, composeGroupLogs, composeGroupRestart } from "./containers/composeGroup"; import { configureContainersExplorer } from "./containers/configureContainersExplorer"; +import { downloadContainerFile } from "./containers/files/downloadContainerFile"; import { openContainerFile } from "./containers/files/openContainerFile"; import { inspectContainer } from "./containers/inspectContainer"; import { pruneContainers } from "./containers/pruneContainers"; @@ -121,6 +122,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-docker.containers.attachShell', attachShellContainer); registerCommand('vscode-docker.containers.browse', browseContainer); + registerCommand('vscode-docker.containers.downloadFile', downloadContainerFile); registerCommand('vscode-docker.containers.inspect', inspectContainer); registerCommand('vscode-docker.containers.configureExplorer', configureContainersExplorer); registerCommand('vscode-docker.containers.openFile', openContainerFile);