From 8d09d2c8c6b8eba0d40d9362d55d19ae46b81d42 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 8 Dec 2020 11:12:40 -0800 Subject: [PATCH 1/3] Sketch download of container files. --- package.json | 14 +++++ package.nls.json | 1 + .../containers/files/downloadContainerFile.ts | 51 +++++++++++++++++++ src/commands/registerCommands.ts | 2 + 4 files changed, 68 insertions(+) create mode 100644 src/commands/containers/files/downloadContainerFile.ts 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..f2bce2d667 --- /dev/null +++ b/src/commands/containers/files/downloadContainerFile.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from 'vscode-azureextensionui'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { FileTreeItem } from '../../../tree/containers/files/FileTreeItem'; +import { multiSelectNodes } from '../../../utils/multiSelectNodes'; + +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') + }); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: localize('vscode-docker.commands.containers.files.downloadContainerFile.opening', 'Downloading File(s)...') + }, + async () => { + await Promise.all( + nodes.map( + async n => { + const containerFileUri = node.uri; + const filePath = containerFileUri.path; + const fileName = path.posix.basename(filePath); + const localFileUri = vscode.Uri.joinPath(localFolderUris[0], fileName); + + const content = await vscode.workspace.fs.readFile(containerFileUri.uri); + + await vscode.workspace.fs.writeFile(localFileUri, content); + })); + }); +} 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); From 426c9259a55ff92186683fe0155d5161203bc4e9 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 8 Dec 2020 14:57:52 -0800 Subject: [PATCH 2/3] Manage existing files during download. --- .../containers/files/downloadContainerFile.ts | 79 +++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/commands/containers/files/downloadContainerFile.ts b/src/commands/containers/files/downloadContainerFile.ts index f2bce2d667..37b0e41517 100644 --- a/src/commands/containers/files/downloadContainerFile.ts +++ b/src/commands/containers/files/downloadContainerFile.ts @@ -5,12 +5,25 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { IActionContext } from 'vscode-azureextensionui'; +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; + } +} + 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.') }, @@ -29,23 +42,61 @@ export async function downloadContainerFile(context: IActionContext, node?: File title: localize('vscode-docker.commands.containers.files.downloadContainerFile.openTitle', 'Select folder for download') }); + 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 () => { - await Promise.all( - nodes.map( - async n => { - const containerFileUri = node.uri; - const filePath = containerFileUri.path; - const fileName = path.posix.basename(filePath); - const localFileUri = vscode.Uri.joinPath(localFolderUris[0], fileName); - - const content = await vscode.workspace.fs.readFile(containerFileUri.uri); - - await vscode.workspace.fs.writeFile(localFileUri, content); - })); + async (_, token) => { + for (const file of files) { + if (token.isCancellationRequested) { + throw new UserCancelledError(); + } + + const localFileExists = await fileExists(file.localUri); + + if (localFileExists) { + 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') + }; + + 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(); + } + } + + const content = await vscode.workspace.fs.readFile(file.containerUri); + + await vscode.workspace.fs.writeFile(file.localUri, content); + } }); } From 65d9665809c8f129b16f455cb9a7fb3d2e37f940 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 9 Dec 2020 09:57:10 -0800 Subject: [PATCH 3/3] Updates per PR feedback. --- .../containers/files/downloadContainerFile.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/commands/containers/files/downloadContainerFile.ts b/src/commands/containers/files/downloadContainerFile.ts index 37b0e41517..35bc88155d 100644 --- a/src/commands/containers/files/downloadContainerFile.ts +++ b/src/commands/containers/files/downloadContainerFile.ts @@ -24,6 +24,18 @@ async function fileExists(file: vscode.Uri): Promise { } } +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.') }, @@ -42,6 +54,10 @@ export async function downloadContainerFile(context: IActionContext, node?: File 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 => { @@ -69,18 +85,6 @@ export async function downloadContainerFile(context: IActionContext, node?: File const localFileExists = await fileExists(file.localUri); if (localFileExists) { - 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') - }; - 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, @@ -94,9 +98,7 @@ export async function downloadContainerFile(context: IActionContext, node?: File } } - const content = await vscode.workspace.fs.readFile(file.containerUri); - - await vscode.workspace.fs.writeFile(file.localUri, content); + await vscode.workspace.fs.copy(file.containerUri, file.localUri, { overwrite: true }); } }); }