├── .nvmrc ├── package.nls.json ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── locker.yml │ ├── bump-version-pr.yaml │ ├── info-needed-closer.yml │ ├── main.yml │ └── feature-request.yml ├── .eslintignore ├── .eslintrc.js ├── resources ├── SignIn.gif ├── storageTree.png ├── readme │ ├── deploy.png │ ├── activityLog.png │ ├── createAdvanced.png │ ├── createResource.png │ └── createStorageAccount.png ├── storage-deploy.png ├── storageAccount.png ├── SelectSubscriptions.gif ├── dark │ ├── BrandAzureStaticWebsites.svg │ ├── AzureQueue.svg │ ├── AzureBlobContainer.svg │ ├── AzureFileShare.svg │ ├── AzureTable.svg │ └── AzureStorageAccount.svg └── light │ ├── BrandAzureStaticWebsites.svg │ ├── AzureQueue.svg │ ├── AzureBlobContainer.svg │ ├── AzureFileShare.svg │ ├── AzureTable.svg │ └── AzureStorageAccount.svg ├── .vscode ├── extensions.json ├── tasks.json └── settings.json ├── test ├── test.code-workspace ├── .eslintrc.js ├── nightly │ ├── testFolder │ │ └── html-hello-world │ │ │ └── index.html │ └── global.resource.test.ts ├── assertThrowsAsync.ts ├── global.test.ts ├── runTest.ts └── index.ts ├── .azure-pipelines ├── compliance │ ├── CredScanSuppressions.json │ ├── tsaoptions.json │ ├── PoliCheckExclusions.xml │ └── compliance.yml ├── common │ ├── lint.yml │ ├── build.yml │ ├── sbom.yml │ └── test.yml ├── release.yml ├── SignExtension.signproj ├── linux │ └── xvfb.init ├── main.yml └── 1esmain.yml ├── SUPPORT.md ├── src ├── getApiExport.ts ├── utils │ ├── localize.ts │ ├── delay.ts │ ├── stringUtils.ts │ ├── azureUtils.ts │ ├── openUrl.ts │ ├── workspaceUtils.ts │ ├── getPropertyFromConnectionString.ts │ ├── copyAndShowToast.ts │ ├── getCoreNodeModule.ts │ ├── settingsUtils.ts │ ├── fs.ts │ ├── launcher.ts │ ├── nonNull.ts │ ├── errorUtils.ts │ ├── storageWrappers.ts │ ├── askOpenInStorageExplorer.ts │ ├── treeUtils.ts │ ├── azuriteUtils.ts │ ├── azureClients.ts │ ├── activityUtils.ts │ ├── progress.ts │ ├── checkCanOverwrite.ts │ ├── blobPathUtils.ts │ ├── validateNames.ts │ └── quickPickUtils.ts ├── tree │ ├── ICopyUrl.ts │ ├── IStorageTreeItem.ts │ ├── fileShare │ │ ├── createFileShare │ │ │ ├── IFileShareWizardContext.ts │ │ │ ├── StorageQuotaPromptStep.ts │ │ │ └── FileShareNameStep.ts │ │ └── FileTreeItem.ts │ ├── ITransferSrcOrDstTreeItem.ts │ ├── createWizard │ │ ├── IStaticWebsiteConfigWizardContext.ts │ │ ├── StaticWebsiteIndexDocumentStep.ts │ │ ├── StaticWebsiteErrorDocument404Step.ts │ │ ├── StorageAccountTreeItemCreateStep.ts │ │ ├── StaticWebsiteEnableStep.ts │ │ ├── storageAccountNameStep.ts │ │ └── StaticWebsiteConfigureStep.ts │ ├── refreshTreeItem.ts │ ├── SubscriptionTreeItem.ts │ ├── IStorageRoot.ts │ ├── table │ │ └── TableTreeItem.ts │ ├── blob │ │ └── createBlobContainer │ │ │ └── BlobContainerNameStep.ts │ └── queue │ │ └── QueueTreeItem.ts ├── storageExplorerLauncher │ ├── ResourceType.ts │ ├── IStorageExplorerLauncher.ts │ └── storageExplorerLauncher.ts ├── vscode-azurestorage.api.d.ts ├── commands │ ├── transfers │ │ └── azCopy │ │ │ ├── IAzCopyResolution.ts │ │ │ ├── credentialStore.ts │ │ │ └── azCopyLocations.ts │ ├── downloadFiles │ │ ├── ISasDownloadContext.ts │ │ ├── IDownloadWizardContext.ts │ │ ├── DestinationPromptStep.ts │ │ ├── DownloadFilesStep.ts │ │ └── SasUrlPromptStep.ts │ ├── uploadFiles │ │ ├── IExistingFileContext.ts │ │ ├── IUploadFilesWizardContext.ts │ │ ├── GetFileDestinationDirectoryStep.ts │ │ └── uploadFiles.ts │ ├── deleteBlob │ │ ├── IDeleteBlobWizardContext.ts │ │ └── DeleteBlobStep.ts │ ├── fileShare │ │ ├── fileActionHandlers.ts │ │ ├── fileShareGroupActionHandlers.ts │ │ ├── directoryActionHandlers.ts │ │ └── fileShareActionHandlers.ts │ ├── deleteBlobDirectory │ │ ├── IDeleteBlobDirectoryWizardContext.ts │ │ └── DeleteBlobDirectoryStep.ts │ ├── deleteStorageAccount │ │ ├── DeleteStorageAccountWizardContext.ts │ │ └── DeleteStorageAccountStep.ts │ ├── openInFileExplorer │ │ ├── IOpenInFileExplorerWizardContext.ts │ │ ├── OpenBehaviorStep.ts │ │ └── OpenTreeItemStep.ts │ ├── table │ │ ├── tableGroupActionHandlers.ts │ │ └── tableActionHandlers.ts │ ├── generateSasUrl.ts │ ├── api │ │ └── revealTreeItem.ts │ ├── uploadFolder │ │ ├── IUploadFolderWizardContext.ts │ │ ├── GetFolderDestinationDirectoryStep.ts │ │ ├── uploadFolder.ts │ │ └── UploadFolderStep.ts │ ├── detachStorageAccount.ts │ ├── queue │ │ ├── queueGroupActionHandlers.ts │ │ └── queueActionHandlers.ts │ ├── blob │ │ └── blobContainerGroupActionHandlers.ts │ ├── commonTreeCommands.ts │ ├── startEmulator.ts │ ├── deleteFilesAndDirectories.ts │ ├── downloadFile.ts │ └── StartingResourcesLogStep.ts ├── vscode.proposed.authLearnMore.d.ts ├── polyfill.worker.ts ├── AttachedAccountRoot.ts ├── StorageWorkspaceProvider.ts ├── extensionVariables.ts ├── constants.ts └── TransferProgress.ts ├── CODE_OF_CONDUCT.md ├── .vscodeignore ├── tsconfig.json ├── main.js ├── LICENSE.md ├── extension.bundle.ts └── SECURITY.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.1 2 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | NOTICE.html linguist-vendored=true 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Microsoft/vscodeazuretoolsdev 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | gulpfile.ts 2 | .eslintrc.js 3 | webpack.config.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "@microsoft/eslint-config-azuretools" 3 | }; 4 | -------------------------------------------------------------------------------- /resources/SignIn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/SignIn.gif -------------------------------------------------------------------------------- /resources/storageTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/storageTree.png -------------------------------------------------------------------------------- /resources/readme/deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/readme/deploy.png -------------------------------------------------------------------------------- /resources/storage-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/storage-deploy.png -------------------------------------------------------------------------------- /resources/storageAccount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/storageAccount.png -------------------------------------------------------------------------------- /resources/SelectSubscriptions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/SelectSubscriptions.gif -------------------------------------------------------------------------------- /resources/readme/activityLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/readme/activityLog.png -------------------------------------------------------------------------------- /resources/readme/createAdvanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/readme/createAdvanced.png -------------------------------------------------------------------------------- /resources/readme/createResource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/readme/createResource.png -------------------------------------------------------------------------------- /resources/readme/createStorageAccount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-azurestorage/HEAD/resources/readme/createStorageAccount.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "ms-azuretools.vscode-azureresourcegroups" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/test.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "./nightly/testFolder/html-hello-world" 5 | } 6 | ], 7 | "settings": {} 8 | } 9 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "@microsoft/eslint-config-azuretools", 4 | "@microsoft/eslint-config-azuretools/test" 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/nightly/testFolder/html-hello-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello World!

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.azure-pipelines/compliance/CredScanSuppressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "tool": "Credential Scanner", 3 | "suppressions": [ 4 | { 5 | "folder": "node_modules\\", 6 | "_justification": "No need to scan external node modules." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.azure-pipelines/common/lint.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Npm@1 3 | displayName: 'Lint' 4 | inputs: 5 | command: custom 6 | customCommand: run lint 7 | 8 | - task: ComponentGovernanceComponentDetection@0 9 | displayName: 'Component Detection' 10 | condition: ne(variables['System.PullRequest.IsFork'], 'True') 11 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please see https://aka.ms/azCodeIssueReporting for our issue reporting guidelines. 6 | 7 | ## Microsoft Support Policy 8 | 9 | Support for this project is limited to the resources listed above. 10 | -------------------------------------------------------------------------------- /.azure-pipelines/common/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: NodeTool@0 3 | displayName: 'Use Node' 4 | inputs: 5 | versionSource: fromFile 6 | versionFilePath: .nvmrc 7 | 8 | - task: Npm@1 9 | displayName: 'npm ci' 10 | inputs: 11 | command: ci 12 | 13 | - task: Npm@1 14 | displayName: 'Build' 15 | inputs: 16 | command: custom 17 | customCommand: run build 18 | -------------------------------------------------------------------------------- /src/getApiExport.ts: -------------------------------------------------------------------------------- 1 | import { Extension, extensions } from "vscode"; 2 | 3 | export async function getApiExport(extensionId: string): Promise { 4 | const extension: Extension | undefined = extensions.getExtension(extensionId); 5 | if (extension) { 6 | if (!extension.isActive) { 7 | await extension.activate(); 8 | } 9 | 10 | return extension.exports; 11 | } 12 | 13 | return undefined; 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /src/utils/localize.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as nls from 'vscode-nls'; 7 | 8 | export const localize: nls.LocalizeFunc = nls.loadMessageBundle(); 9 | -------------------------------------------------------------------------------- /resources/dark/BrandAzureStaticWebsites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/tree/ICopyUrl.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | export interface ICopyUrl { 9 | getUrl?(): string; 10 | copyUrl(): void; 11 | } 12 | -------------------------------------------------------------------------------- /resources/light/BrandAzureStaticWebsites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/storageExplorerLauncher/ResourceType.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export type ResourceType = 'Azure.BlobContainer' | 'Azure.FileShare' | 'Azure.Queue' | 'Azure.Table'; 7 | -------------------------------------------------------------------------------- /src/tree/IStorageTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IStorageRoot } from "./IStorageRoot"; 7 | 8 | export interface IStorageTreeItem { 9 | root: IStorageRoot; 10 | } 11 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # These files are excluded during vsce package 2 | .vscode/** 3 | .vscode-test/** 4 | out 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | .github/** 10 | test-results.xml 11 | .azure-pipelines/** 12 | *.cmd 13 | **/*.map 14 | out/test/** 15 | README.dev.md 16 | test/** 17 | tsd.json 18 | typings/** 19 | build 20 | out/** 21 | tslint.json 22 | node_modules/** 23 | **/*.map 24 | dist/test/** 25 | gulp* 26 | webpack.config* 27 | stats.json 28 | *.vsix 29 | *.zip 30 | testWorkspace/** 31 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export async function delay(ms: number): Promise { 7 | await new Promise((resolve: () => void): NodeJS.Timer => setTimeout(resolve, ms)); 8 | } 9 | -------------------------------------------------------------------------------- /src/vscode-azurestorage.api.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface AzureStorageExtensionApi { 7 | apiVersion: string; 8 | 9 | revealTreeItem(resourceId: string): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/transfers/azCopy/IAzCopyResolution.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IParsedError } from "@microsoft/vscode-azext-utils"; 7 | 8 | export interface IAzCopyResolution { 9 | errors: IParsedError[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/downloadFiles/ISasDownloadContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | 8 | export interface ISasDownloadContext extends IActionContext { 9 | sasUrl?: string; 10 | } 11 | -------------------------------------------------------------------------------- /.azure-pipelines/compliance/tsaoptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsaVersion": "TsaV2", 3 | "codeBase": "NewOrUpdate", 4 | "codeBaseName": "vscode-azurestorage", 5 | "tsaStamp": "DevDiv", 6 | "notificationAliases": [ 7 | "AzCode@microsoft.com" 8 | ], 9 | "codebaseAdmins": [ 10 | "REDMOND\\jinglou", 11 | "REDMOND\\AzCode" 12 | ], 13 | "instanceUrl": "https://devdiv.visualstudio.com", 14 | "projectName": "DevDiv", 15 | "areaPath": "DevDiv\\VS Azure Tools\\AzCode Extensions", 16 | "iterationPath": "DevDiv", 17 | "allTools": true 18 | } 19 | -------------------------------------------------------------------------------- /.azure-pipelines/common/sbom.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 3 | displayName: "SBoM Generation Task" 4 | inputs: 5 | BuildDropPath: "$(Build.ArtifactStagingDirectory)" 6 | condition: ne(variables['System.PullRequest.IsFork'], 'True') 7 | 8 | - task: PublishBuildArtifacts@1 9 | displayName: "Publish artifacts: sbom" 10 | inputs: 11 | PathtoPublish: "$(Build.ArtifactStagingDirectory)/_manifest" 12 | ArtifactName: _manifest 13 | condition: ne(variables['System.PullRequest.IsFork'], 'True') 14 | -------------------------------------------------------------------------------- /src/commands/uploadFiles/IExistingFileContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | 8 | export interface IExistingFileContext extends IActionContext { 9 | localFilePath: string; 10 | remoteFilePath: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/tree/fileShare/createFileShare/IFileShareWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | 8 | export interface IFileShareWizardContext extends IActionContext { 9 | name?: string; 10 | quota?: number; 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "isBackground": true, 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "lint", 20 | "problemMatcher": "$eslint-stylish" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/stringUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as crypto from "crypto"; 7 | 8 | export function getRandomHexString(length: number = 10): string { 9 | const buffer: Buffer = crypto.randomBytes(Math.ceil(length / 2)); 10 | return buffer.toString('hex').slice(0, length); 11 | } 12 | -------------------------------------------------------------------------------- /src/tree/ITransferSrcOrDstTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IStorageTreeItem } from "./IStorageTreeItem"; 7 | 8 | export interface ITransferSrcOrDstTreeItem extends IStorageTreeItem { 9 | remoteFilePath: string; 10 | resourceUri: string; 11 | transferSasToken: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/azureUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function getAppResourceIdFromId(id: string): string | undefined { 7 | const matches: RegExpMatchArray | null = id.match(/\/subscriptions\/.*\/resourceGroups\/.*\/providers\/Microsoft.Storage\/storageAccounts\/[^\/]*/); 8 | 9 | return matches?.[0]; 10 | } 11 | -------------------------------------------------------------------------------- /src/storageExplorerLauncher/IStorageExplorerLauncher.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ResourceType } from "./ResourceType"; 7 | 8 | export interface IStorageExplorerLauncher { 9 | openResource(accountId: string, subscriptionid: string, resourceType?: ResourceType, resourceName?: string): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/openUrl.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as open from 'open'; 7 | 8 | export async function openUrl(url: string): Promise { 9 | // Using this functionality is blocked by https://github.com/Microsoft/vscode/issues/85930 10 | // await vscode.env.openExternal(vscode.Uri.parse(url)); 11 | 12 | await open(url); 13 | } 14 | -------------------------------------------------------------------------------- /resources/dark/AzureQueue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/AzureQueue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tree/createWizard/IStaticWebsiteConfigWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IStorageAccountWizardContext } from "@microsoft/vscode-azext-azureutils"; 7 | 8 | export interface IStaticWebsiteConfigWizardContext extends IStorageAccountWizardContext { 9 | enableStaticWebsite: boolean; 10 | indexDocument: string; 11 | errorDocument404Path: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/deleteBlob/IDeleteBlobWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { BlobTreeItem } from "../../tree/blob/BlobTreeItem"; 8 | 9 | export interface IDeleteBlobWizardContext extends IActionContext, ExecuteActivityContext { 10 | blobName?: string; 11 | blob?: BlobTreeItem; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/workspaceUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export function getSingleRootWorkspace(): vscode.WorkspaceFolder | undefined { 9 | // if this is a multi-root workspace, return undefined 10 | return vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length === 1 ? vscode.workspace.workspaceFolders[0] : undefined; 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/fileShare/fileActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { deleteFilesAndDirectories } from '../deleteFilesAndDirectories'; 8 | 9 | export function registerFileActionHandlers(): void { 10 | registerCommandWithTreeNodeUnwrapping("azureStorage.deleteFile", deleteFilesAndDirectories); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/getPropertyFromConnectionString.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function getPropertyFromConnectionString(connectionString: string, property: string): string | undefined { 7 | const regexp: RegExp = new RegExp(`(?:^|;)\\s*${property}=([^;]+)(?:;|$)`, 'i'); 8 | const match: RegExpMatchArray | undefined = connectionString.match(regexp) || undefined; 9 | return match && match[1]; 10 | } 11 | -------------------------------------------------------------------------------- /.azure-pipelines/compliance/PoliCheckExclusions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | NODE_MODULES 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/commands/deleteBlobDirectory/IDeleteBlobDirectoryWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { BlobDirectoryTreeItem } from "../../tree/blob/BlobDirectoryTreeItem"; 8 | 9 | export interface IDeleteBlobDirectoryWizardContext extends IActionContext, ExecuteActivityContext { 10 | dirName?: string; 11 | blobDirectory?: BlobDirectoryTreeItem; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/copyAndShowToast.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { env } from "vscode"; 7 | import { ext } from "../extensionVariables"; 8 | import { localize } from "./localize"; 9 | 10 | export async function copyAndShowToast(text: string, name?: string): Promise { 11 | await env.clipboard.writeText(text); 12 | ext.outputChannel.show(); 13 | ext.outputChannel.appendLog(localize('copiedClipboard', '{1} copied to clipboard: {0}', text, name)); 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/deleteStorageAccount/DeleteStorageAccountWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; 7 | import { StorageAccountWrapper } from "../../utils/storageWrappers"; 8 | 9 | export interface DeleteStorageAccountWizardContext extends IActionContext, ExecuteActivityContext { 10 | resourceGroupName?: string; 11 | storageAccount?: StorageAccountWrapper; 12 | subscription: ISubscriptionContext; 13 | } 14 | -------------------------------------------------------------------------------- /src/vscode.proposed.authLearnMore.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | 8 | // https://github.com/microsoft/vscode/issues/206587 9 | 10 | export interface AuthenticationGetSessionPresentationOptions { 11 | /** 12 | * An optional Uri to open in the browser to learn more about this authentication request. 13 | */ 14 | learnMore?: Uri; 15 | } 16 | } 17 | 18 | // this proposed api is only included because vscode.proposed.authenticationChallenges.d.ts depends on it 19 | -------------------------------------------------------------------------------- /resources/dark/AzureBlobContainer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/AzureBlobContainer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/AzureFileShare.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/AzureFileShare.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "out", 6 | "lib": [ 7 | "es2021", 8 | "dom", 9 | "es6", 10 | "esnext.asynciterable" 11 | ], 12 | "sourceMap": true, 13 | "rootDir": ".", 14 | "noUnusedLocals": true, 15 | "noImplicitThis": true, 16 | "noImplicitReturns": true, 17 | "strictNullChecks": true, 18 | "noUnusedParameters": true, 19 | "baseUrl": "./", 20 | "paths": { 21 | "*": [ 22 | "node_modules/@types/*", 23 | "*" 24 | ] 25 | }, 26 | "skipLibCheck": false 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | ".vscode-test", 31 | "gulpfile.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.organizeImports": "explicit" 5 | }, 6 | "editor.detectIndentation": false, 7 | "editor.formatOnSave": true, 8 | "editor.insertSpaces": true, 9 | "editor.tabSize": 4, 10 | "files.insertFinalNewline": true, 11 | "files.trimTrailingWhitespace": true, 12 | "search.exclude": { 13 | "out": true, 14 | "**/node_modules": true, 15 | ".vscode-test": true 16 | }, 17 | "typescript.preferences.importModuleSpecifier": "relative", 18 | "typescript.tsdk": "node_modules/typescript/lib", 19 | "cSpell.words": [ 20 | "azureextensionui", 21 | "azurestorage", 22 | "azuretools", 23 | "mailto", 24 | "norequired", 25 | "opencode", 26 | "repo" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/assertThrowsAsync.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as assert from 'assert'; 7 | 8 | /** 9 | * Same as assert.throws except for async functions 10 | */ 11 | export async function assertThrowsAsync(block: () => Promise, error: RegExp | Function, message?: string): Promise { 12 | let blockSync = (): void => { /* ignore */ }; 13 | try { 14 | await block(); 15 | } catch (e) { 16 | blockSync = (): void => { throw e; }; 17 | } finally { 18 | assert.throws(blockSync, error, message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/locker.yml: -------------------------------------------------------------------------------- 1 | name: Locker 2 | on: 3 | schedule: 4 | - cron: 0 5 * * * # 10:00pm PT 5 | workflow_dispatch: 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Actions 12 | uses: actions/checkout@v2 13 | with: 14 | repository: "microsoft/vscode-github-triage-actions" 15 | path: ./actions 16 | ref: stable 17 | - name: Install Actions 18 | run: npm install --production --prefix ./actions 19 | - name: Run Locker 20 | uses: ./actions/locker 21 | with: 22 | app_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_ID }} 23 | app_installation_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_INSTALLATION_ID }} 24 | app_private_key: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_PRIVATE_KEY }} 25 | daysSinceClose: 45 26 | daysSinceUpdate: 7 27 | -------------------------------------------------------------------------------- /.azure-pipelines/common/test.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | sudo cp .azure-pipelines/linux/xvfb.init /etc/init.d/xvfb 4 | sudo chmod +x /etc/init.d/xvfb 5 | sudo update-rc.d xvfb defaults 6 | sudo service xvfb start 7 | displayName: 'Start X Virtual Frame Buffer' 8 | condition: eq(variables['Agent.OS'], 'Linux') 9 | 10 | - task: Npm@1 11 | displayName: 'Test' 12 | inputs: 13 | command: custom 14 | customCommand: test 15 | env: 16 | SERVICE_PRINCIPAL_CLIENT_ID: $(SERVICE_PRINCIPAL_CLIENT_ID) 17 | SERVICE_PRINCIPAL_SECRET: $(SERVICE_PRINCIPAL_SECRET) 18 | SERVICE_PRINCIPAL_DOMAIN: $(SERVICE_PRINCIPAL_DOMAIN) 19 | DISPLAY: :10 # Only necessary for linux tests 20 | 21 | - task: PublishTestResults@2 22 | displayName: 'Publish Test Results' 23 | inputs: 24 | testResultsFiles: '*-results.xml' 25 | testRunTitle: '$(Agent.OS)' 26 | condition: succeededOrFailed() 27 | -------------------------------------------------------------------------------- /src/polyfill.worker.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { JSDOM } from "jsdom"; 7 | 8 | const { window } = new JSDOM(); 9 | 10 | export function polyfill(): void { 11 | if (!globalThis.document) { 12 | globalThis.document = window.document; 13 | } 14 | 15 | if (!globalThis.DOMParser) { 16 | globalThis.DOMParser = window.DOMParser; 17 | } 18 | 19 | if (!globalThis.XMLSerializer) { 20 | globalThis.XMLSerializer = window.XMLSerializer; 21 | } 22 | 23 | if (!globalThis.Node) { 24 | globalThis.Node = window.Node; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/downloadFiles/IDownloadWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { ITransferSrcOrDstTreeItem } from "../../tree/ITransferSrcOrDstTreeItem"; 8 | import { DownloadItem } from "../transfers/transfers"; 9 | 10 | export interface IDownloadWizardContext extends IActionContext, ExecuteActivityContext { 11 | destinationFolder?: string; 12 | sasUrl?: string; 13 | treeItems?: ITransferSrcOrDstTreeItem[]; 14 | 15 | allFileDownloads?: DownloadItem[]; 16 | allFolderDownloads?: DownloadItem[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/openInFileExplorer/IOpenInFileExplorerWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { BlobContainerTreeItem } from '../../tree/blob/BlobContainerTreeItem'; 8 | import { FileShareTreeItem } from '../../tree/fileShare/FileShareTreeItem'; 9 | 10 | export interface IOpenInFileExplorerWizardContext extends IActionContext { 11 | treeItem: BlobContainerTreeItem | FileShareTreeItem; 12 | openBehavior?: OpenBehavior; 13 | } 14 | 15 | export type OpenBehavior = 'AddToWorkspace' | 'OpenInNewWindow' | 'OpenInCurrentWindow' | 'AlreadyOpen' | 'DontOpen'; 16 | -------------------------------------------------------------------------------- /src/tree/refreshTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { AttachedAccountRoot } from "../AttachedAccountRoot"; 8 | import { ext } from "../extensionVariables"; 9 | import { IStorageTreeItem } from "./IStorageTreeItem"; 10 | 11 | export async function refreshTreeItem(actionContext: IActionContext, node: (AzExtTreeItem & IStorageTreeItem) | undefined): Promise { 12 | node && node.root instanceof AttachedAccountRoot ? 13 | await ext.rgApi.workspaceResourceTree.refresh(actionContext, node) : 14 | await ext.rgApi.appResourceTree.refresh(actionContext, node); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/table/tableGroupActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { TableGroupTreeItem } from '../../tree/table/TableGroupTreeItem'; 8 | import { createChildNode } from '../commonTreeCommands'; 9 | 10 | export function registerTableGroupActionHandlers(): void { 11 | registerCommandWithTreeNodeUnwrapping("azureStorage.createTable", createTable); 12 | } 13 | 14 | export async function createTable(context: IActionContext, treeItem?: TableGroupTreeItem): Promise { 15 | await createChildNode(context, TableGroupTreeItem.contextValue, treeItem); 16 | } 17 | -------------------------------------------------------------------------------- /src/storageExplorerLauncher/storageExplorerLauncher.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as os from "os"; 7 | import { IStorageExplorerLauncher } from "./IStorageExplorerLauncher"; 8 | import { MacOSStorageExplorerLauncher } from "./macOSStorageExplorerLauncher"; 9 | import { WindowsStorageExplorerLauncher } from "./windowsStorageExplorerLauncher"; 10 | 11 | let storageExplorerLauncher: IStorageExplorerLauncher; 12 | 13 | if (os.platform() === "win32") { 14 | storageExplorerLauncher = new WindowsStorageExplorerLauncher(); 15 | } else { 16 | // assume Mac Os for now. 17 | storageExplorerLauncher = new MacOSStorageExplorerLauncher(); 18 | } 19 | 20 | export { storageExplorerLauncher }; 21 | -------------------------------------------------------------------------------- /.github/workflows/bump-version-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Bump version after release 2 | 3 | # Run when release is published, or manually triggered 4 | on: 5 | release: 6 | types: [published] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | bump: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Bump version 16 | id: bump 17 | uses: alexweininger/bump-prerelease-version@v0.1.1 18 | 19 | - name: Create pull request 20 | uses: peter-evans/create-pull-request@v4 21 | with: 22 | title: ${{ env.MESSAGE }} 23 | body: Automatically created by ${{ env.RUN_LINK }} 24 | commit-message: ${{ env.MESSAGE }} 25 | branch: bot/bump-${{ steps.bump.outputs.new-version }} 26 | base: main 27 | author: GitHub 28 | env: 29 | RUN_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 30 | MESSAGE: Bump version after release 31 | -------------------------------------------------------------------------------- /src/commands/fileShare/fileShareGroupActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { FileShareGroupTreeItem } from '../../tree/fileShare/FileShareGroupTreeItem'; 8 | import { createChildNode } from '../commonTreeCommands'; 9 | 10 | export function registerFileShareGroupActionHandlers(): void { 11 | registerCommandWithTreeNodeUnwrapping("azureStorage.createFileShare", createFileShare); 12 | } 13 | 14 | export async function createFileShare(context: IActionContext, treeItem?: FileShareGroupTreeItem): Promise { 15 | await createChildNode(context, FileShareGroupTreeItem.contextValue, treeItem); 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/generateSasUrl.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from '@microsoft/vscode-azext-utils'; 7 | import { posix } from 'path'; 8 | import { ITransferSrcOrDstTreeItem } from '../tree/ITransferSrcOrDstTreeItem'; 9 | import { copyAndShowToast } from '../utils/copyAndShowToast'; 10 | 11 | export async function generateSasUrl(_context: IActionContext, treeItem: ITransferSrcOrDstTreeItem): Promise { 12 | const resourceUri = treeItem.resourceUri; 13 | const sasToken = treeItem.transferSasToken; 14 | const sasUrl: string = `${resourceUri}${posix.sep}${treeItem.remoteFilePath}?${sasToken}`; 15 | await copyAndShowToast(sasUrl, 'SAS Token and URL'); 16 | return sasUrl; 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/api/revealTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, callWithTelemetryAndErrorHandling, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { ext } from "../../extensionVariables"; 8 | 9 | export async function revealTreeItem(resourceId: string): Promise { 10 | return await callWithTelemetryAndErrorHandling('api.revealTreeItem', async (context: IActionContext) => { 11 | const node: AzExtTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem(resourceId, { ...context, loadAll: true }); 12 | if (node) { 13 | await ext.rgApi.appResourceTreeView.reveal(node, { select: true, focus: true, expand: true }); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/uploadFiles/IUploadFilesWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { Uri } from "vscode"; 8 | import { BlobContainerTreeItem } from "../../tree/blob/BlobContainerTreeItem"; 9 | import { FileShareTreeItem } from "../../tree/fileShare/FileShareTreeItem"; 10 | import { IAzCopyResolution } from "../transfers/azCopy/IAzCopyResolution"; 11 | 12 | export interface IUploadFilesWizardContext extends IActionContext, ExecuteActivityContext { 13 | calledFromUploadToAzureStorage: boolean; 14 | destinationDirectory?: string; 15 | resolution?: IAzCopyResolution; 16 | treeItem?: BlobContainerTreeItem | FileShareTreeItem; 17 | uris?: Uri[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/uploadFolder/IUploadFolderWizardContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExecuteActivityContext, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { Uri } from "vscode"; 8 | import { BlobContainerTreeItem } from "../../tree/blob/BlobContainerTreeItem"; 9 | import { FileShareTreeItem } from "../../tree/fileShare/FileShareTreeItem"; 10 | import { IAzCopyResolution } from "../transfers/azCopy/IAzCopyResolution"; 11 | 12 | export interface IUploadFolderWizardContext extends IActionContext, ExecuteActivityContext { 13 | calledFromUploadToAzureStorage: boolean; 14 | destinationDirectory?: string; 15 | resolution?: IAzCopyResolution; 16 | treeItem?: BlobContainerTreeItem | FileShareTreeItem; 17 | uri?: Uri; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/getCoreNodeModule.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | /** 9 | * Returns a node module installed with VSCode, or undefined if it fails. 10 | */ 11 | export function getCoreNodeModule(moduleName: string): T | undefined { 12 | try { 13 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 14 | return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`); 15 | } catch (err) { 16 | // ignore 17 | } 18 | 19 | try { 20 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 21 | return require(`${vscode.env.appRoot}/node_modules/${moduleName}`); 22 | } catch (err) { 23 | // ignore 24 | } 25 | return undefined; 26 | } 27 | -------------------------------------------------------------------------------- /resources/dark/AzureTable.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/AzureTable.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/detachStorageAccount.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { ext } from "../extensionVariables"; 8 | import { AttachedStorageAccountTreeItem } from "../tree/AttachedStorageAccountTreeItem"; 9 | 10 | export async function detachStorageAccount(actionContext: IActionContext, treeItem?: AttachedStorageAccountTreeItem): Promise { 11 | if (!treeItem) { 12 | treeItem = await ext.rgApi.workspaceResourceTree.showTreeItemPicker(AttachedStorageAccountTreeItem.baseContextValue, actionContext); 13 | } 14 | 15 | await ext.attachedStorageAccountsTreeItem.detach(treeItem); 16 | await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedStorageAccountsTreeItem); 17 | } 18 | -------------------------------------------------------------------------------- /test/nightly/global.resource.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { StorageManagementClient } from '@azure/arm-storage'; 7 | import { longRunningTestsEnabled } from '../global.test'; 8 | 9 | export let webSiteClient: StorageManagementClient; 10 | export const resourceGroupsToDelete: string[] = []; 11 | 12 | suiteSetup(async function (this: Mocha.Context): Promise { 13 | this.skip(); 14 | if (longRunningTestsEnabled) { 15 | this.timeout(2 * 60 * 1000); 16 | } 17 | }); 18 | 19 | suiteTeardown(async function (this: Mocha.Context): Promise { 20 | if (longRunningTestsEnabled) { 21 | this.timeout(10 * 60 * 1000); 22 | // await Promise.all(resourceGroupsToDelete.map(async resource => { 23 | // await beginDeleteResourceGroup(resource); 24 | // })); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | "use strict"; 7 | 8 | // This is the extension entrypoint module, which imports extension.bundle.js, the actual extension code. 9 | // 10 | // This is in a separate file so we can properly measure extension.bundle.js load time. 11 | 12 | let perfStats = { 13 | loadStartTime: Date.now(), 14 | loadEndTime: undefined 15 | }; 16 | 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | 19 | const extension = require('./out/src/extension'); 20 | 21 | async function activate(ctx) { 22 | return await extension.activate(ctx, perfStats, true); 23 | } 24 | 25 | async function deactivate(ctx) { 26 | return await extension.deactivate(ctx, perfStats); 27 | } 28 | 29 | // Export as entrypoints for vscode 30 | exports.activate = activate; 31 | exports.deactivate = deactivate; 32 | 33 | perfStats.loadEndTime = Date.now(); 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/commands/queue/queueGroupActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { QueueGroupTreeItem } from '../../tree/queue/QueueGroupTreeItem'; 8 | import { isAzuriteInstalled, warnAzuriteNotInstalled } from '../../utils/azuriteUtils'; 9 | import { createChildNode } from '../commonTreeCommands'; 10 | 11 | export function registerQueueGroupActionHandlers(): void { 12 | registerCommandWithTreeNodeUnwrapping("azureStorage.createQueue", createQueue); 13 | } 14 | 15 | export async function createQueue(context: IActionContext, treeItem?: QueueGroupTreeItem): Promise { 16 | if (treeItem?.root.isEmulated && !(await isAzuriteInstalled())) { 17 | warnAzuriteNotInstalled(context); 18 | } 19 | await createChildNode(context, QueueGroupTreeItem.contextValue, treeItem); 20 | } 21 | -------------------------------------------------------------------------------- /.azure-pipelines/release.yml: -------------------------------------------------------------------------------- 1 | trigger: none # Only run this pipeline when manually triggered 2 | 3 | parameters: 4 | - name: publishVersion 5 | displayName: Version to publish 6 | type: string 7 | - name: dryRun 8 | displayName: Dry run 9 | type: boolean 10 | default: false 11 | 12 | resources: 13 | pipelines: 14 | - pipeline: build # identifier to use in pipeline resource variables 15 | source: \Azure Tools\VSCode\Extensions\vscode-azurestorage # name of the pipeline that produces the artifacts 16 | repositories: 17 | - repository: azExtTemplates 18 | type: github 19 | name: microsoft/vscode-azuretools 20 | ref: main 21 | endpoint: GitHub-AzureTools # The service connection to use when accessing this repository 22 | 23 | variables: 24 | # Required by MicroBuild template 25 | - name: TeamName 26 | value: "Azure Tools for VS Code" 27 | 28 | # Use those templates 29 | extends: 30 | template: azure-pipelines/release-extension.yml@azExtTemplates 31 | parameters: 32 | pipelineID: $(resources.pipeline.build.pipelineID) 33 | runID: $(resources.pipeline.build.runID) 34 | publishVersion: ${{ parameters.publishVersion }} 35 | dryRun: ${{ parameters.dryRun }} 36 | environmentName: AzCodeDeploy 37 | -------------------------------------------------------------------------------- /src/utils/settingsUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Uri, workspace, WorkspaceConfiguration } from "vscode"; 7 | import { ext } from "../extensionVariables"; 8 | 9 | /** 10 | * Uses ext.prefix unless otherwise specified 11 | */ 12 | export async function updateWorkspaceSetting(section: string, value: T, fsPath: string, prefix: string = ext.prefix): Promise { 13 | const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, Uri.file(fsPath)); 14 | await projectConfiguration.update(section, value); 15 | } 16 | 17 | /** 18 | * Uses ext.prefix unless otherwise specified 19 | */ 20 | export function getWorkspaceSetting(key: string, fsPath?: string, prefix: string = ext.prefix): T | undefined { 21 | const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, fsPath ? Uri.file(fsPath) : undefined); 22 | return projectConfiguration.get(key); 23 | } 24 | -------------------------------------------------------------------------------- /resources/dark/AzureStorageAccount.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Icon-storage-86 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/light/AzureStorageAccount.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Icon-storage-86 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/commands/blob/blobContainerGroupActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { BlobContainerGroupTreeItem } from '../../tree/blob/BlobContainerGroupTreeItem'; 8 | import { isAzuriteInstalled, warnAzuriteNotInstalled } from '../../utils/azuriteUtils'; 9 | import { createChildNode } from '../commonTreeCommands'; 10 | 11 | export function registerBlobContainerGroupActionHandlers(): void { 12 | registerCommandWithTreeNodeUnwrapping("azureStorage.createBlobContainer", createBlobContainer); 13 | } 14 | 15 | export async function createBlobContainer(context: IActionContext, treeItem?: BlobContainerGroupTreeItem): Promise { 16 | if (treeItem?.root.isEmulated && !(await isAzuriteInstalled())) { 17 | warnAzuriteNotInstalled(context); 18 | } 19 | await createChildNode(context, BlobContainerGroupTreeItem.contextValue, treeItem); 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/fileShare/directoryActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtParentTreeItem, IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { DirectoryTreeItem } from '../../tree/fileShare/DirectoryTreeItem'; 8 | import { IFileShareCreateChildContext } from '../../tree/fileShare/FileShareTreeItem'; 9 | import { deleteFilesAndDirectories } from '../deleteFilesAndDirectories'; 10 | 11 | export function registerDirectoryActionHandlers(): void { 12 | registerCommandWithTreeNodeUnwrapping("azureStorage.deleteDirectory", deleteFilesAndDirectories); 13 | registerCommandWithTreeNodeUnwrapping("azureStorage.createSubdirectory", async (context: IActionContext, treeItem: AzExtParentTreeItem) => await treeItem.createChild({ ...context, childType: DirectoryTreeItem.contextValue })); 14 | 15 | // Note: azureStorage.createTextFile is registered in fileShareActionHandlers 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from 'path'; 7 | import * as vscode from 'vscode'; 8 | import { Uri, workspace } from 'vscode'; 9 | 10 | export function isPathEqual(fsPath1: string, fsPath2: string): boolean { 11 | const relativePath: string = path.relative(fsPath1, fsPath2); 12 | return relativePath === ''; 13 | } 14 | 15 | export function isSubpath(expectedParent: string, expectedChild: string): boolean { 16 | const relativePath: string = path.relative(expectedParent, expectedChild); 17 | return relativePath !== '' && !relativePath.startsWith('..') && relativePath !== expectedChild; 18 | } 19 | 20 | export async function isEmptyDirectory(pathOrUri: Uri | string): Promise { 21 | if (typeof pathOrUri === 'string') { 22 | pathOrUri = vscode.Uri.file(pathOrUri) 23 | } 24 | 25 | const files = await workspace.fs.readDirectory(pathOrUri); 26 | if (files.length === 0) { 27 | return true; 28 | } 29 | return false; 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/downloadFiles/DestinationPromptStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { configurationSettingsKeys } from "../../constants"; 8 | import { localize } from "../../utils/localize"; 9 | import { showWorkspaceFoldersQuickPick } from "../../utils/quickPickUtils"; 10 | import { IDownloadWizardContext } from "./IDownloadWizardContext"; 11 | 12 | export class DestinationPromptStep extends AzureWizardPromptStep { 13 | public async prompt(context: IDownloadWizardContext): Promise { 14 | const placeHolderString: string = localize('selectFolderForDownload', 'Select destination folder for download'); 15 | context.destinationFolder = await showWorkspaceFoldersQuickPick(placeHolderString, context, configurationSettingsKeys.deployPath); 16 | } 17 | 18 | public shouldPrompt(context: IDownloadWizardContext): boolean { 19 | return !context.destinationFolder; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/launcher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | **/ 5 | 6 | import { spawn } from "child_process"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 9 | export class Launcher { 10 | public static async launch(command: string, ...args: string[]): Promise { 11 | await new Promise((resolve, _reject) => { 12 | const spawnEnv = <{ [key: string]: string }>JSON.parse(JSON.stringify(process.env)); 13 | // remove those env vars 14 | delete spawnEnv.ATOM_SHELL_INTERNAL_RUN_AS_NODE; 15 | delete spawnEnv.ELECTRON_RUN_AS_NODE; 16 | 17 | const childProcess = spawn( 18 | command, 19 | args, 20 | { 21 | env: spawnEnv 22 | } 23 | ); 24 | 25 | childProcess.stdout.on("data", (chunk) => { 26 | resolve(); 27 | console.log(`child process message: ${chunk}`); 28 | }); 29 | 30 | childProcess.stderr.on("data", (chunk) => { 31 | console.log(`child process message: ${chunk}`); 32 | }); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/nonNull.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Retrieves a property by name from an object and checks that it's not null and not undefined. It is strongly typed 8 | * for the property and will give a compile error if the given name is not a property of the source. 9 | */ 10 | export function nonNullProp(source: TSource, name: TKey): NonNullable { 11 | const value: NonNullable = >source[name]; 12 | return nonNullValue(value, name); 13 | } 14 | 15 | /** 16 | * Validates that a given value is not null and not undefined. 17 | */ 18 | export function nonNullValue(value: T | undefined | null, propertyNameOrMessage?: string): T { 19 | if (value === null || value === undefined) { 20 | throw new Error( 21 | 'Internal error: Expected value to be neither null nor undefined' 22 | + (propertyNameOrMessage ? `: ${propertyNameOrMessage}` : '')); 23 | } 24 | 25 | return value; 26 | } 27 | -------------------------------------------------------------------------------- /.azure-pipelines/SignExtension.signproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 9 | 10 | 11 | VSCodePublisher 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/info-needed-closer.yml: -------------------------------------------------------------------------------- 1 | name: Info Needed Closer 2 | on: 3 | schedule: 4 | - cron: 30 5 * * * # 10:30pm PT 5 | workflow_dispatch: 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Actions 12 | uses: actions/checkout@v2 13 | with: 14 | repository: "microsoft/vscode-github-triage-actions" 15 | path: ./actions 16 | ref: stable 17 | - name: Install Actions 18 | run: npm install --production --prefix ./actions 19 | - name: Run Info Needed Closer 20 | uses: ./actions/needs-more-info-closer 21 | with: 22 | app_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_ID }} 23 | app_installation_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_INSTALLATION_ID }} 24 | app_private_key: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_PRIVATE_KEY }} 25 | label: info-needed 26 | closeDays: 14 27 | closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity. See also our [issue reporting](https://aka.ms/azcodeissuereporting) guidelines.\n\nHappy Coding!" 28 | pingDays: 80 29 | pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." 30 | -------------------------------------------------------------------------------- /src/utils/errorUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IParsedError, TelemetryProperties, UserCancelledError } from '@microsoft/vscode-azext-utils'; 7 | import * as vscode from 'vscode'; 8 | import { ext } from '../extensionVariables'; 9 | import { localize } from './localize'; 10 | 11 | export const multipleAzCopyErrorsMessage: string = localize('multipleAzCopyErrors', 'Multiple AzCopy errors occurred while uploading. Check the [output window](command:{0}) for more details.', `${ext.prefix}.showOutputChannel`); 12 | 13 | export function throwIfCanceled(cancellationToken: vscode.CancellationToken | undefined, properties: TelemetryProperties | undefined, cancelStep: string): void { 14 | if (cancellationToken && cancellationToken.isCancellationRequested) { 15 | if (properties && cancelStep) { 16 | properties.cancelStep = cancelStep; 17 | } 18 | throw new UserCancelledError(); 19 | } 20 | } 21 | 22 | export function isAzCopyError(parsedError: IParsedError): boolean { 23 | return /azcopy/i.test(parsedError.message); 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/downloadFiles/DownloadFilesStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; 7 | import { CancellationToken } from "vscode"; 8 | import { NotificationProgress } from "../../constants"; 9 | import { downloadFoldersAndFiles } from "../transfers/transfers"; 10 | import { IDownloadWizardContext } from "./IDownloadWizardContext"; 11 | 12 | export class DownloadFilesStep extends AzureWizardExecuteStep { 13 | public priority: number = 300; 14 | 15 | public constructor(private readonly cancellationToken?: CancellationToken) { 16 | super(); 17 | } 18 | 19 | public async execute(context: IDownloadWizardContext, notificationProgress: NotificationProgress): Promise { 20 | await downloadFoldersAndFiles(context, context.allFolderDownloads ?? [], context.allFileDownloads ?? [], notificationProgress, this.cancellationToken); 21 | } 22 | 23 | public shouldExecute(wizardContext: IDownloadWizardContext): boolean { 24 | return !!wizardContext.destinationFolder; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/global.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { TestUserInput } from '@microsoft/vscode-azext-dev'; 7 | import * as vscode from 'vscode'; 8 | import { registerOnActionStartHandler } from "../extension.bundle"; 9 | 10 | const longRunningLocalTestsEnabled: boolean = !/^(false|0)?$/i.test(process.env.AzCode_EnableLongRunningTestsLocal || ''); 11 | const longRunningRemoteTestsEnabled: boolean = !/^(false|0)?$/i.test(process.env.AzCode_UseAzureFederatedCredentials || ''); 12 | 13 | export const longRunningTestsEnabled: boolean = longRunningLocalTestsEnabled || longRunningRemoteTestsEnabled; 14 | 15 | // Runs before all tests 16 | suiteSetup(async function (this: Mocha.Context): Promise { 17 | this.timeout(120 * 1000); 18 | await vscode.commands.executeCommand('azureStorage.refresh'); // activate the extension before tests begin 19 | 20 | registerOnActionStartHandler(context => { 21 | // Use `TestUserInput` by default so we get an error if an unexpected call to `context.ui` occurs, rather than timing out 22 | context.ui = new TestUserInput(vscode); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.azure-pipelines/linux/xvfb.init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # COPIED FROM https://github.com/microsoft/vscode/blob/01e9903967417ba243cec705445eef9ecbfebfea/build/azure-pipelines/linux/xvfb.init 4 | # 5 | # 6 | # /etc/rc.d/init.d/xvfbd 7 | # 8 | # chkconfig: 345 95 28 9 | # description: Starts/Stops X Virtual Framebuffer server 10 | # processname: Xvfb 11 | # 12 | ### BEGIN INIT INFO 13 | # Provides: xvfb 14 | # Required-Start: $remote_fs $syslog 15 | # Required-Stop: $remote_fs $syslog 16 | # Default-Start: 2 3 4 5 17 | # Default-Stop: 0 1 6 18 | # Short-Description: Start xvfb at boot time 19 | # Description: Enable xvfb provided by daemon. 20 | ### END INIT INFO 21 | 22 | [ "${NETWORKING}" = "no" ] && exit 0 23 | 24 | PROG="/usr/bin/Xvfb" 25 | PROG_OPTIONS=":10 -ac -screen 0 1024x768x24" 26 | PROG_OUTPUT="/tmp/Xvfb.out" 27 | 28 | case "$1" in 29 | start) 30 | echo "Starting : X Virtual Frame Buffer " 31 | $PROG $PROG_OPTIONS>>$PROG_OUTPUT 2>&1 & 32 | disown -ar 33 | ;; 34 | stop) 35 | echo "Shutting down : X Virtual Frame Buffer" 36 | killproc $PROG 37 | RETVAL=$? 38 | [ $RETVAL -eq 0 ] && /bin/rm -f /var/lock/subsys/Xvfb 39 | /var/run/Xvfb.pid 40 | echo 41 | ;; 42 | restart|reload) 43 | $0 stop 44 | $0 start 45 | RETVAL=$? 46 | ;; 47 | status) 48 | status Xvfb 49 | RETVAL=$? 50 | ;; 51 | *) 52 | echo $"Usage: $0 (start|stop|restart|reload|status)" 53 | exit 1 54 | esac 55 | 56 | exit $RETVAL 57 | -------------------------------------------------------------------------------- /src/tree/SubscriptionTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { SubscriptionTreeItemBase } from '@microsoft/vscode-azext-azureutils'; 7 | import { AzExtTreeItem, IActionContext } from '@microsoft/vscode-azext-utils'; 8 | import { AttachedStorageAccountTreeItem } from './AttachedStorageAccountTreeItem'; 9 | 10 | export class SubscriptionTreeItem extends SubscriptionTreeItemBase { 11 | public childTypeLabel: string = "Storage Account"; 12 | public supportsAdvancedCreation: boolean = true; 13 | 14 | private _nextLink: string | undefined; 15 | 16 | async loadMoreChildrenImpl(clearCache: boolean, _context: IActionContext): Promise { 17 | if (clearCache) { 18 | this._nextLink = undefined; 19 | } 20 | return []; 21 | } 22 | 23 | public hasMoreChildrenImpl(): boolean { 24 | return !!this._nextLink; 25 | } 26 | 27 | public isAncestorOfImpl(contextValue: string): boolean { 28 | return contextValue !== AttachedStorageAccountTreeItem.baseContextValue && contextValue !== AttachedStorageAccountTreeItem.emulatedContextValue; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/commonTreeCommands.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from '@microsoft/vscode-azext-utils'; 7 | import { storageFilter } from '../constants'; 8 | import { ext } from '../extensionVariables'; 9 | 10 | export async function deleteNode(context: IActionContext, expectedContextValue?: string | RegExp, node?: AzExtTreeItem): Promise { 11 | if (!node) { 12 | node = await ext.rgApi.pickAppResource({ ...context, suppressCreatePick: true }, { 13 | filter: storageFilter, 14 | expectedChildContextValue: expectedContextValue 15 | }); 16 | } 17 | 18 | await node.deleteTreeItem(context); 19 | } 20 | 21 | export async function createChildNode(context: IActionContext, expectedContextValue: string, node?: AzExtParentTreeItem): Promise { 22 | if (!node) { 23 | node = await ext.rgApi.pickAppResource(context, { 24 | filter: storageFilter, 25 | expectedChildContextValue: expectedContextValue 26 | }); 27 | } 28 | 29 | await node.createChild(context); 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/storageWrappers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Endpoints, StorageAccount, StorageAccountKey } from '@azure/arm-storage'; 7 | import { nonNullProp } from '@microsoft/vscode-azext-utils'; 8 | 9 | /** 10 | * Wrappers around StorageAccountWrapper that guarantees certain properties are not undefined/null 11 | */ 12 | export class StorageAccountWrapper { 13 | constructor(private readonly _account: StorageAccount) { } 14 | readonly id: string = nonNullProp(this._account, 'id'); 15 | readonly name: string = nonNullProp(this._account, 'name'); 16 | readonly type: string = nonNullProp(this._account, 'type'); 17 | readonly primaryEndpoints: Endpoints = nonNullProp(this._account, 'primaryEndpoints'); 18 | readonly location: string = nonNullProp(this._account, 'location'); 19 | } 20 | 21 | /** 22 | * Wrappers around StorageAccountKeyWrapper that guarantees certain properties are not undefined/null 23 | */ 24 | export class StorageAccountKeyWrapper { 25 | constructor(private readonly _key: StorageAccountKey) { } 26 | readonly value = nonNullProp(this._key, 'value'); 27 | readonly keyName = nonNullProp(this._key, 'keyName'); 28 | } 29 | -------------------------------------------------------------------------------- /.azure-pipelines/main.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | WEB_BUILDS_STG_ACCT: vscodedotdev 3 | WEB_BUILDS_CONTAINER: web-builds 4 | ${{ if eq(variables['Build.Reason'], 'Schedule') }}: 5 | ENABLE_LONG_RUNNING_TESTS: true 6 | ENABLE_COMPLIANCE: true 7 | 8 | jobs: 9 | - job: Windows 10 | pool: 11 | vmImage: windows-latest 12 | steps: 13 | - template: common/build.yml 14 | - template: common/package.yml 15 | - template: common/lint.yml 16 | - template: compliance/compliance.yml # Only works on Windows 17 | - template: common/test.yml 18 | 19 | - job: Linux 20 | pool: 21 | vmImage: ubuntu-latest 22 | steps: 23 | - template: common/build.yml 24 | - template: common/package.yml 25 | - template: common/sbom.yml # only generate on linux 26 | - template: common/lint.yml 27 | - template: common/test.yml 28 | variables: 29 | Codeql.Enabled: $[in(variables['Build.Reason'], 'Schedule')] # Enable CodeQL only on scheduled builds because it is slow 30 | 31 | - job: macOS 32 | pool: 33 | vmImage: macOS-latest 34 | steps: 35 | - template: common/build.yml 36 | - template: common/package.yml 37 | - template: common/lint.yml 38 | - template: common/test.yml 39 | 40 | trigger: 41 | branches: 42 | include: 43 | - '*' 44 | 45 | pr: 46 | branches: 47 | include: 48 | - '*' 49 | 50 | schedules: 51 | - cron: "30 8 * * *" 52 | displayName: Nightly at 1:30 PT 53 | always: true # Run even when there are no code changes 54 | branches: 55 | include: 56 | - main 57 | -------------------------------------------------------------------------------- /src/utils/askOpenInStorageExplorer.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, UserCancelledError } from "@microsoft/vscode-azext-utils"; 7 | import { platform } from "os"; 8 | import { window } from "vscode"; 9 | import { ResourceType } from "../storageExplorerLauncher/ResourceType"; 10 | import { storageExplorerLauncher } from "../storageExplorerLauncher/storageExplorerLauncher"; 11 | import { localize } from "./localize"; 12 | 13 | export function askOpenInStorageExplorer(context: IActionContext, errorMessage: string, resourceId: string, subscriptionId: string, resourceType: ResourceType, resourceName: string): void { 14 | const message: string = localize("openInSE", "Open resource in Storage Explorer"); 15 | const items: string[] = platform() === 'linux' ? [] : [message]; 16 | void window.showErrorMessage(errorMessage, ...items).then(async result => { 17 | if (result === message) { 18 | context.telemetry.properties.openInStorageExplorer = 'true'; 19 | await storageExplorerLauncher.openResource(resourceId, subscriptionId, resourceType, resourceName); 20 | } 21 | }); 22 | 23 | // Either way, throw canceled error 24 | throw new UserCancelledError(message); 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/queue/queueActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { storageExplorerLauncher } from '../../storageExplorerLauncher/storageExplorerLauncher'; 8 | import { QueueTreeItem } from '../../tree/queue/QueueTreeItem'; 9 | import { deleteNode } from '../commonTreeCommands'; 10 | 11 | export function registerQueueActionHandlers(): void { 12 | registerCommandWithTreeNodeUnwrapping("azureStorage.openQueue", openQueueInStorageExplorer); 13 | registerCommandWithTreeNodeUnwrapping("azureStorage.deleteQueue", deleteQueue); 14 | } 15 | 16 | async function openQueueInStorageExplorer(_context: IActionContext, treeItem: QueueTreeItem): Promise { 17 | const accountId = treeItem.root.storageAccountId; 18 | const resourceType = "Azure.Queue"; 19 | const resourceName = treeItem.queue.name; 20 | 21 | await storageExplorerLauncher.openResource(accountId, treeItem.subscription.subscriptionId, resourceType, resourceName); 22 | } 23 | 24 | export async function deleteQueue(context: IActionContext, treeItem?: QueueTreeItem): Promise { 25 | await deleteNode(context, QueueTreeItem.contextValue, treeItem); 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/table/tableActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 7 | import { storageExplorerLauncher } from '../../storageExplorerLauncher/storageExplorerLauncher'; 8 | import { TableTreeItem } from '../../tree/table/TableTreeItem'; 9 | import { deleteNode } from '../commonTreeCommands'; 10 | 11 | export function registerTableActionHandlers(): void { 12 | registerCommandWithTreeNodeUnwrapping("azureStorage.openTable", openTableInStorageExplorer); 13 | registerCommandWithTreeNodeUnwrapping("azureStorage.deleteTable", deleteTable); 14 | } 15 | 16 | async function openTableInStorageExplorer(_context: IActionContext, treeItem: TableTreeItem): Promise { 17 | const accountId = treeItem.root.storageAccountId; 18 | const resourceType = "Azure.Table"; 19 | const resourceName = treeItem.tableName; 20 | 21 | await storageExplorerLauncher.openResource(accountId, treeItem.subscription.subscriptionId, resourceType, resourceName); 22 | } 23 | 24 | export async function deleteTable(context: IActionContext, treeItem?: TableTreeItem): Promise { 25 | await deleteNode(context, TableTreeItem.contextValue, treeItem); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/treeUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, NoResourceFoundError } from "@microsoft/vscode-azext-utils"; 7 | 8 | export namespace treeUtils { 9 | export function findNearestParent(node: AzExtTreeItem, parentContextValues: string | RegExp | (string | RegExp)[]): T { 10 | parentContextValues = Array.isArray(parentContextValues) ? parentContextValues : [parentContextValues]; 11 | if (!parentContextValues.length) throw new NoResourceFoundError(); 12 | 13 | let currentNode: AzExtTreeItem = node; 14 | let foundParent: boolean = false; 15 | while (currentNode.parent) { 16 | for (const contextValue of parentContextValues) { 17 | const parentRegex: RegExp = contextValue instanceof RegExp ? contextValue : new RegExp(contextValue); 18 | if (parentRegex.test(currentNode.contextValue)) { 19 | foundParent = true; 20 | break; 21 | } 22 | } 23 | if (foundParent) break; 24 | currentNode = currentNode.parent; 25 | } 26 | if (!foundParent) throw new NoResourceFoundError(); 27 | return currentNode as T; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AttachedAccountRoot.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ISubscriptionContext } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "./utils/localize"; 8 | 9 | export class AttachedAccountRoot implements ISubscriptionContext { 10 | private _error: Error = new Error(localize('cannotRetrieveAzureSubscriptionInfoForAttachedAccount', 'Cannot retrieve Azure subscription information for an attached account.')); 11 | 12 | public get credentials(): never { 13 | throw this._error; 14 | } 15 | 16 | public get createCredentialsForScopes(): never { 17 | throw this._error; 18 | } 19 | 20 | public get subscriptionDisplayName(): never { 21 | throw this._error; 22 | } 23 | 24 | public get subscriptionId(): never { 25 | throw this._error; 26 | } 27 | 28 | public get subscriptionPath(): never { 29 | throw this._error; 30 | } 31 | 32 | public get tenantId(): never { 33 | throw this._error; 34 | } 35 | 36 | public get userId(): never { 37 | throw this._error; 38 | } 39 | 40 | public get environment(): never { 41 | throw this._error; 42 | } 43 | 44 | public get isCustomCloud(): never { 45 | throw this._error; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/StorageWorkspaceProvider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtParentTreeItem, AzExtTreeItem, callWithTelemetryAndErrorHandling, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { WorkspaceResourceProvider } from "@microsoft/vscode-azext-utils/hostapi"; 8 | import { Disposable } from "vscode"; 9 | import { ext } from "./extensionVariables"; 10 | import { AttachedStorageAccountsTreeItem } from "./tree/AttachedStorageAccountsTreeItem"; 11 | 12 | 13 | export class StorageWorkspaceProvider implements WorkspaceResourceProvider { 14 | 15 | public disposables: Disposable[] = []; 16 | 17 | constructor(parent: AzExtParentTreeItem) { 18 | ext.attachedStorageAccountsTreeItem = new AttachedStorageAccountsTreeItem(parent); 19 | } 20 | 21 | public async provideResources(): Promise { 22 | 23 | return await callWithTelemetryAndErrorHandling('StorageWorkspaceProvider.provideResources', async (_context: IActionContext) => { 24 | return [ext.attachedStorageAccountsTreeItem]; 25 | }); 26 | } 27 | private _projectDisposables: Disposable[] = []; 28 | 29 | public dispose(): void { 30 | Disposable.from(...this._projectDisposables).dispose(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.azure-pipelines/1esmain.yml: -------------------------------------------------------------------------------- 1 | # Trigger the build whenever `main` or `rel/*` is updated 2 | trigger: 3 | - main 4 | - rel/* 5 | 6 | # Disable PR trigger 7 | pr: none 8 | 9 | # Scheduled nightly build of `main` 10 | schedules: 11 | - cron: "0 0 * * *" 12 | displayName: Nightly scheduled build 13 | always: false # Don't rebuild if there haven't been changes 14 | branches: 15 | include: 16 | - main 17 | 18 | # `resources` specifies the location of templates to pick up, use it to get AzExt templates 19 | resources: 20 | repositories: 21 | - repository: azExtTemplates 22 | type: github 23 | name: microsoft/vscode-azuretools 24 | ref: main 25 | endpoint: GitHub-AzureTools # The service connection to use when accessing this repository 26 | 27 | parameters: 28 | - name: enableLongRunningTests 29 | displayName: Enable Long Running Tests 30 | type: boolean 31 | default: true 32 | 33 | variables: 34 | # Required by MicroBuild template 35 | - name: TeamName 36 | value: "Azure Tools for VS Code" 37 | 38 | # Use those templates 39 | extends: 40 | template: azure-pipelines/1esmain.yml@azExtTemplates 41 | parameters: 42 | useAzureFederatedCredentials: ${{ parameters.enableLongRunningTests }} 43 | additionalSetupSteps: 44 | - script: npm install --force @azure-tools/azcopy-darwin @azure-tools/azcopy-linux @azure-tools/azcopy-win32 @azure-tools/azcopy-win64 45 | displayName: Install AzCopy 46 | name: Install_azcopy 47 | - script: npm run setazcopypermissions 48 | displayName: Set AzCopy Permissions 49 | name: Set_Azcopy_Permissions 50 | -------------------------------------------------------------------------------- /src/commands/transfers/azCopy/credentialStore.ts: -------------------------------------------------------------------------------- 1 | /*!--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *----------------------------------------------------------*/ 4 | 5 | import { ICredentialStore } from "@azure-tools/azcopy-node"; 6 | import { ext } from "../../../extensionVariables"; 7 | 8 | export class CredentialStore implements ICredentialStore { 9 | private credentialMap: Map = new Map(); 10 | public async setEntry(service: string, account: string, value: string): Promise { 11 | try { 12 | this.credentialMap.set(`${service}-${account}`, value); 13 | } catch (error) { 14 | ext.outputChannel.appendLog(`Unable to set password. service:${service} account:${account}`, error); 15 | } 16 | } 17 | 18 | public async getEntry(service: string, account: string): Promise { 19 | try { 20 | return this.credentialMap.get(`${service}-${account}`) ?? null; 21 | } catch (error) { 22 | ext.outputChannel.appendLog(`Unable to get password. service:${service} account:${account}`, error); 23 | return null; 24 | } 25 | } 26 | 27 | public async deleteEntry(service: string, account: string): Promise { 28 | try { 29 | return this.credentialMap.delete(`${service}-${account}`); 30 | } catch (error) { 31 | ext.outputChannel.appendLog(`Unable to delete password. service:${service} account:${account}`, error); 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/azuriteUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { extensions } from "vscode"; 8 | import { azuriteExtensionId } from "../constants"; 9 | import { cpUtils } from "./cpUtils"; 10 | import { localize } from "./localize"; 11 | 12 | export async function isAzuriteInstalled(): Promise { 13 | return isAzuriteExtensionInstalled() || await isAzuriteCliInstalled(); 14 | } 15 | 16 | export function isAzuriteExtensionInstalled(): boolean { 17 | return !!extensions.getExtension(azuriteExtensionId); 18 | } 19 | 20 | export async function isAzuriteCliInstalled(): Promise { 21 | try { 22 | await cpUtils.executeCommand(undefined, undefined, 'azurite -v'); 23 | return true; 24 | } catch { 25 | return false; 26 | } 27 | } 28 | 29 | export function warnAzuriteNotInstalled(context: IActionContext): void { 30 | void context.ui.showWarningMessage(localize('mustInstallAzurite', 'You must install the [Azurite extension](command:azureStorage.showAzuriteExtension) to perform this operation.')); 31 | context.telemetry.properties.cancelStep = 'installAzuriteExtension'; 32 | context.errorHandling.suppressDisplay = true; 33 | throw new Error(`"${azuriteExtensionId}" extension is not installed.`); 34 | } 35 | -------------------------------------------------------------------------------- /src/tree/createWizard/StaticWebsiteIndexDocumentStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "../../utils/localize"; 8 | import { DocumentType, validateDocumentPath } from "../../utils/validateNames"; 9 | import { IStaticWebsiteConfigWizardContext } from "./IStaticWebsiteConfigWizardContext"; 10 | 11 | export class StaticWebsiteIndexDocumentStep extends AzureWizardPromptStep { 12 | static readonly defaultIndexDocument: string = 'index.html'; 13 | private oldIndexDocument: string | undefined; 14 | 15 | public constructor(oldIndexDocument?: string) { 16 | super(); 17 | this.oldIndexDocument = oldIndexDocument; 18 | } 19 | 20 | public async prompt(context: IStaticWebsiteConfigWizardContext): Promise { 21 | context.indexDocument = await context.ui.showInputBox({ 22 | prompt: localize('enterTheIndexDocumentName', 'Enter the index document name'), 23 | value: this.oldIndexDocument || StaticWebsiteIndexDocumentStep.defaultIndexDocument, 24 | validateInput: (value) => { return validateDocumentPath(value, DocumentType.index); } 25 | }); 26 | } 27 | 28 | public shouldPrompt(): boolean { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.azure-pipelines/compliance/compliance.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@1 3 | displayName: 'Run PoliCheck' 4 | inputs: 5 | targetType: F # search files and folders 6 | optionsUEPATH: '$(Build.SourcesDirectory)/.azure-pipelines/compliance/PoliCheckExclusions.xml' 7 | continueOnError: true 8 | condition: eq(variables['ENABLE_COMPLIANCE'], 'true') 9 | 10 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 11 | displayName: 'Run CredScan' 12 | inputs: 13 | toolMajorVersion: V2 14 | suppressionsFile: '$(Build.SourcesDirectory)/.azure-pipelines/compliance/CredScanSuppressions.json' 15 | debugMode: true # Needed to suppress folders 16 | folderSuppression: true 17 | continueOnError: true 18 | condition: eq(variables['ENABLE_COMPLIANCE'], 'true') 19 | 20 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-antimalware.AntiMalware@3 21 | displayName: 'AntiMalware Scanner' 22 | inputs: 23 | FileDirPath: '$(Build.SourcesDirectory)' 24 | EnableServices: true 25 | continueOnError: true 26 | condition: eq(variables['ENABLE_COMPLIANCE'], 'true') 27 | 28 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2 29 | displayName: 'Publish Security Analysis Logs' 30 | condition: eq(variables['ENABLE_COMPLIANCE'], 'true') 31 | 32 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1 33 | displayName: 'Post Analysis' 34 | inputs: 35 | AllTools: true 36 | condition: eq(variables['ENABLE_COMPLIANCE'], 'true') 37 | -------------------------------------------------------------------------------- /src/utils/azureClients.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { type ResourceGraphClient } from '@azure/arm-resourcegraph'; 7 | import { StorageManagementClient } from '@azure/arm-storage'; 8 | import { AzExtClientContext, createAzureClient, createAzureSubscriptionClient, parseClientContext } from '@microsoft/vscode-azext-azureutils'; 9 | 10 | // Lazy-load @azure packages to improve startup performance. 11 | // NOTE: The client is the only import that matters, the rest of the types disappear when compiled to JavaScript 12 | 13 | export async function createStorageClient(context: AzExtClientContext): Promise { 14 | if (parseClientContext(context).isCustomCloud) { 15 | // set API version for Azure Stack 16 | process.env.AZCOPY_DEFAULT_SERVICE_API_VERSION = "2019-02-02"; 17 | return createAzureClient(context, (await import('@azure/arm-storage-profile-2020-09-01-hybrid')).StorageManagementClient); 18 | } else { 19 | return createAzureClient(context, (await import('@azure/arm-storage')).StorageManagementClient); 20 | } 21 | } 22 | 23 | export async function createResourceGraphClient(context: AzExtClientContext): Promise { 24 | return createAzureSubscriptionClient(context, (await import('@azure/arm-resourcegraph')).ResourceGraphClient); 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/openInFileExplorer/OpenBehaviorStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; 7 | import { localize } from '../../utils/localize'; 8 | import { IOpenInFileExplorerWizardContext, OpenBehavior } from './IOpenInFileExplorerWizardContext'; 9 | 10 | export class OpenBehaviorStep extends AzureWizardPromptStep { 11 | public async prompt(context: IOpenInFileExplorerWizardContext): Promise { 12 | const picks: IAzureQuickPickItem[] = [ 13 | { label: localize('OpenInCurrentWindow', 'Open in current window'), data: 'OpenInCurrentWindow' }, 14 | { label: localize('OpenInNewWindow', 'Open in new window'), data: 'OpenInNewWindow' }, 15 | { label: localize('AddToWorkspace', 'Add to workspace'), data: 'AddToWorkspace' } 16 | ]; 17 | 18 | const placeHolder: string = localize('selectOpenBehavior', 'Select how you would like to open this resource'); 19 | context.openBehavior = (await context.ui.showQuickPick(picks, { placeHolder })).data; 20 | } 21 | 22 | public shouldPrompt(context: IOpenInFileExplorerWizardContext): boolean { 23 | return !context.openBehavior && context.openBehavior !== 'AlreadyOpen' && context.openBehavior !== 'DontOpen'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/tree/IStorageRoot.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import type { TableServiceClient } from "@azure/data-tables"; 7 | import type { AccountSASSignatureValues as AccountSASSignatureValuesBlob, BlobServiceClient } from "@azure/storage-blob"; 8 | import type { AccountSASSignatureValues as AccountSASSignatureValuesFileShare, ShareServiceClient } from "@azure/storage-file-share"; 9 | import type { QueueServiceClient } from "@azure/storage-queue"; 10 | 11 | import { type Endpoints, type StorageManagementClient } from "@azure/arm-storage"; 12 | import { type IActionContext } from "@microsoft/vscode-azext-utils"; 13 | 14 | export interface IStorageRoot { 15 | storageAccountName: string; 16 | storageAccountId: string; 17 | tenantId: string; 18 | isEmulated: boolean; 19 | allowSharedKeyAccess: boolean; 20 | primaryEndpoints?: Endpoints; 21 | getAccessToken(): Promise; 22 | generateSasToken(accountSASSignatureValues: AccountSASSignatureValuesBlob | AccountSASSignatureValuesFileShare): string; 23 | getStorageManagementClient(context: IActionContext): Promise; 24 | createBlobServiceClient(): Promise; 25 | createShareServiceClient(): Promise; 26 | createQueueServiceClient(): Promise; 27 | createTableServiceClient(): Promise; 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node PR Lint, Build and Test 2 | 3 | on: 4 | # Trigger when manually run 5 | workflow_dispatch: 6 | 7 | # Trigger on pushes to `main` or `rel/*` 8 | push: 9 | branches: 10 | - main 11 | - rel/* 12 | 13 | # Trigger on pull requests to `main` or `rel/*` 14 | pull_request: 15 | branches: 16 | - main 17 | - rel/* 18 | 19 | # not using shared template (https://github.com/microsoft/vscode-azuretools/tree/main/.github/workflows) so that we can install azcopy 20 | jobs: 21 | Build: 22 | runs-on: ubuntu-latest 23 | 24 | defaults: 25 | run: 26 | working-directory: "." 27 | 28 | steps: 29 | # Setup 30 | - uses: actions/checkout@v3 31 | - name: Using Node.js 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version-file: .nvmrc 35 | - run: npm ci --no-optional 36 | 37 | # Install AzCopy 38 | - run: npm install --force @azure-tools/azcopy-darwin @azure-tools/azcopy-linux @azure-tools/azcopy-win32 @azure-tools/azcopy-win64 39 | - run: npm run setazcopypermissions 40 | 41 | # Set AzCopy permissions 42 | # comment out in main because I think it is effecting other branches 43 | # - run: npm run postinstall 44 | 45 | # Lint 46 | - run: npm run lint 47 | 48 | # Build 49 | - run: npm run build 50 | 51 | # Package 52 | - run: npm run package 53 | - name: Upload Artifacts 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: Artifacts 57 | path: | 58 | **/*.vsix 59 | **/*.tgz 60 | !**/node_modules 61 | 62 | # Test 63 | - run: xvfb-run -a npm test 64 | -------------------------------------------------------------------------------- /src/tree/createWizard/StaticWebsiteErrorDocument404Step.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "../../utils/localize"; 8 | import { DocumentType, validateDocumentPath } from "../../utils/validateNames"; 9 | import { IStaticWebsiteConfigWizardContext } from "./IStaticWebsiteConfigWizardContext"; 10 | 11 | export class StaticWebsiteErrorDocument404Step extends AzureWizardPromptStep { 12 | static readonly defaultErrorDocument404Path: string = 'index.html'; 13 | private oldErrorDocument404Path: string | undefined; 14 | 15 | public constructor(oldErrorDocument404Path?: string) { 16 | super(); 17 | this.oldErrorDocument404Path = oldErrorDocument404Path; 18 | } 19 | 20 | public async prompt(context: IStaticWebsiteConfigWizardContext): Promise { 21 | context.errorDocument404Path = await context.ui.showInputBox({ 22 | prompt: localize('enterThe404ErrorDocumentPath', 'Enter the 404 error document path'), 23 | value: this.oldErrorDocument404Path || StaticWebsiteErrorDocument404Step.defaultErrorDocument404Path, 24 | validateInput: (value) => { return validateDocumentPath(value, DocumentType.error); } 25 | }); 26 | } 27 | 28 | public shouldPrompt(): boolean { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/extensionVariables.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IAzExtOutputChannel } from "@microsoft/vscode-azext-utils"; 7 | import { AzureHostExtensionApi } from "@microsoft/vscode-azext-utils/hostapi"; 8 | import { ExtensionContext, UIKind, Uri, env } from "vscode"; 9 | import { AzureStorageFS } from "./AzureStorageFS"; 10 | import { AttachedStorageAccountsTreeItem } from "./tree/AttachedStorageAccountsTreeItem"; 11 | 12 | /** 13 | * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts 14 | */ 15 | export namespace ext { 16 | export let context: ExtensionContext; 17 | export let outputChannel: IAzExtOutputChannel; 18 | export let ignoreBundle: boolean | undefined; 19 | 20 | export let attachedStorageAccountsTreeItem: AttachedStorageAccountsTreeItem; 21 | export let azureStorageFS: AzureStorageFS; 22 | export let azureStorageWorkspaceFS: AzureStorageFS; 23 | export const azCopyExePath: string = 'azcopy'; 24 | export const prefix: string = 'azureStorage'; 25 | 26 | export let rgApi: AzureHostExtensionApi; 27 | export let lastUriUpload: Uri | undefined; 28 | 29 | // When debugging thru VS Code as a web environment, the UIKind is Desktop. However, if you sideload it into the browser, you must 30 | // change this to UIKind.Web and then webpack it again 31 | export const isWeb: boolean = env.uiKind === UIKind.Web; 32 | } 33 | -------------------------------------------------------------------------------- /src/tree/fileShare/createFileShare/StorageQuotaPromptStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "../../../utils/localize"; 8 | import { IFileShareWizardContext } from "./IFileShareWizardContext"; 9 | 10 | const minQuotaGB = 1; 11 | const maxQuotaGB = 5120; 12 | 13 | export class StorageQuotaPromptStep extends AzureWizardPromptStep { 14 | public async prompt(context: IFileShareWizardContext): Promise { 15 | context.quota = Number(await context.ui.showInputBox({ 16 | prompt: localize('quotaPrompt', 'Specify quota (in GB, between {0} and {1}), to limit total storage size', minQuotaGB, maxQuotaGB), 17 | value: maxQuotaGB.toString(), 18 | validateInput: this.validateQuota 19 | })); 20 | } 21 | 22 | public shouldPrompt(context: IFileShareWizardContext): boolean { 23 | return !context.quota; 24 | } 25 | 26 | private validateQuota(input: string): string | undefined { 27 | const value = Number(input); 28 | if (isNaN(value)) { 29 | return localize('quotaNum', 'Value must be a number'); 30 | } else if (value < minQuotaGB || value > maxQuotaGB) { 31 | return localize('quotaBetween', 'Value must be between {0} and {1}', minQuotaGB, maxQuotaGB); 32 | } 33 | 34 | return undefined; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; 7 | import * as cp from 'child_process'; 8 | import * as path from 'path'; 9 | 10 | async function main(): Promise { 11 | try { 12 | const vscodeExecutablePath = await downloadAndUnzipVSCode('stable'); 13 | const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); 14 | 15 | cp.spawnSync( 16 | cli, 17 | [ 18 | ...args, 19 | '--install-extension', 'ms-azuretools.vscode-azureresourcegroups', 20 | ], 21 | { 22 | encoding: 'utf-8', 23 | stdio: 'inherit' 24 | }); 25 | 26 | const repoRoot: string = path.resolve(__dirname, '..', '..'); 27 | await runTests({ 28 | vscodeExecutablePath, 29 | extensionDevelopmentPath: repoRoot, 30 | launchArgs: [ 31 | path.resolve(repoRoot, 'test', 'test.code-workspace'), 32 | '--disable-workspace-trust' 33 | ], 34 | extensionTestsPath: path.resolve(repoRoot, 'dist', 'test', 'index'), 35 | extensionTestsEnv: { 36 | DEBUGTELEMETRY: 'v', 37 | MOCHA_timeout: '20000' 38 | } 39 | }); 40 | } catch (err) { 41 | console.error('Failed to run tests'); 42 | process.exit(1); 43 | } 44 | } 45 | 46 | void main(); 47 | -------------------------------------------------------------------------------- /src/utils/activityUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ActivityChildItemBase, ActivityChildType, ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; 7 | import { ext } from "../extensionVariables"; 8 | import { getWorkspaceSetting } from "./settingsUtils"; 9 | 10 | export async function createActivityContext(options?: { withChildren?: boolean }): Promise { 11 | return { 12 | registerActivity: async (activity) => ext.rgApi.registerActivity(activity), 13 | suppressNotification: await getWorkspaceSetting('suppressActivityNotifications', undefined, 'azureResourceGroups'), 14 | activityChildren: options?.withChildren ? [] : undefined, 15 | }; 16 | } 17 | 18 | /** 19 | * Adds a new activity child after the last info child in the `activityChildren` array. 20 | * If no info child already exists, the new child is prepended to the front of the array. 21 | * (This utility function is useful for keeping the info children grouped at the front of the list) 22 | */ 23 | export type ActivityInfoChild = ActivityChildItemBase & { activityType: ActivityChildType.Info }; 24 | export function prependOrInsertAfterLastInfoChild(context: Partial, infoChild: ActivityInfoChild): void { 25 | if (!context.activityChildren) { 26 | return; 27 | } 28 | 29 | const idx: number = context.activityChildren 30 | .map(child => child.activityType) 31 | .lastIndexOf(ActivityChildType.Info); 32 | 33 | if (idx === -1) { 34 | context.activityChildren.unshift(infoChild); 35 | } else { 36 | context.activityChildren.splice(idx + 1, 0, infoChild); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/deleteBlob/DeleteBlobStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import type { BlobClient } from "@azure/storage-blob"; 7 | 8 | import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; 9 | import { Progress, window } from "vscode"; 10 | import { ext } from "../../extensionVariables"; 11 | import { createBlobClient } from "../../utils/blobUtils"; 12 | import { localize } from "../../utils/localize"; 13 | import { IDeleteBlobWizardContext } from "./IDeleteBlobWizardContext"; 14 | 15 | export class DeleteBlobStep extends AzureWizardExecuteStep { 16 | public priority: number = 100; 17 | 18 | public async execute(wizardContext: IDeleteBlobWizardContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise { 19 | const blobName = nonNullProp(wizardContext, 'blobName'); 20 | const blob = nonNullProp(wizardContext, 'blob'); 21 | const deletingBlob: string = localize('deletingBlob', 'Deleting blob "{0}"...', blobName); 22 | ext.outputChannel.appendLog(deletingBlob); 23 | progress.report({ message: deletingBlob }); 24 | const blobClient: BlobClient = await createBlobClient(blob.root, blob.container.name, blob.blobPath); 25 | await blobClient.delete(); 26 | const deleteSuccessful: string = localize('successfullyDeletedBlob', 'Successfully deleted blob "{0}".', blobName); 27 | ext.outputChannel.appendLog(deleteSuccessful); 28 | if (!wizardContext.suppressNotification) { 29 | void window.showInformationMessage(deleteSuccessful); 30 | } 31 | } 32 | 33 | public shouldExecute(): boolean { 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Progress } from 'vscode'; 7 | import { ext } from './extensionVariables'; 8 | 9 | export const staticWebsiteContainerName = '$web'; 10 | 11 | export const maxPageSize = 50; 12 | 13 | export enum configurationSettingsKeys { 14 | deployPath = 'deployPath', 15 | preDeployTask = 'preDeployTask', 16 | deleteBeforeDeploy = 'deleteBeforeDeploy' 17 | } 18 | 19 | export const extensionPrefix: string = 'azureStorage'; 20 | 21 | export const azuriteExtensionId: string = 'Azurite.azurite'; 22 | export const emulatorTimeoutMS: number = 3 * 1000; 23 | export const emulatorAccountName: string = 'devstoreaccount1'; 24 | export const emulatorConnectionString: string = 'UseDevelopmentStorage=true;'; 25 | export const emulatorKey: string = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='; 26 | export const azuriteKey: string = 'azurite' 27 | export const defaultEmulatorHost: string = '127.0.0.1'; 28 | 29 | export const maxRemoteFileEditSizeMB: number = 50; 30 | export const maxRemoteFileEditSizeBytes: number = maxRemoteFileEditSizeMB * 1024 * 1024; 31 | 32 | export const storageExplorerDownloadUrl: string = 'https://go.microsoft.com/fwlink/?LinkId=723579'; 33 | 34 | export function getResourcesPath(): string { 35 | return ext.context.asAbsolutePath('resources'); 36 | } 37 | 38 | export type NotificationProgress = Progress<{ 39 | message?: string | undefined; 40 | increment?: number | undefined; 41 | }>; 42 | 43 | export const storageProvider: string = 'Microsoft.Storage'; 44 | 45 | export const storageFilter = { 46 | type: 'Microsoft.Storage/storageAccounts', 47 | } 48 | 49 | export const threeDaysInMS: number = 1000 * 60 * 60 * 24 * 3; 50 | -------------------------------------------------------------------------------- /src/commands/startEmulator.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IActionContext } from '@microsoft/vscode-azext-utils'; 7 | import * as vscode from 'vscode'; 8 | import { emulatorTimeoutMS } from '../constants'; 9 | import { ext } from "../extensionVariables"; 10 | import { isAzuriteCliInstalled, isAzuriteExtensionInstalled, warnAzuriteNotInstalled } from '../utils/azuriteUtils'; 11 | import { cpUtils } from '../utils/cpUtils'; 12 | 13 | export async function startEmulator(context: IActionContext, emulatorType: EmulatorType): Promise { 14 | if (isAzuriteExtensionInstalled()) { 15 | // Use the Azurite extension 16 | await vscode.commands.executeCommand(`azurite.start_${emulatorType}`); 17 | await ext.rgApi.workspaceResourceTree.refresh(context, ext.attachedStorageAccountsTreeItem); 18 | } else if (await isAzuriteCliInstalled()) { 19 | // Use the Azurite CLI 20 | 21 | // This task will remain active as long as the user keeps the emulator running. Only show an error if it happens in the first three seconds 22 | const emulatorTask: Promise = cpUtils.executeCommand(ext.outputChannel, undefined, `azurite-${emulatorType}`); 23 | ext.outputChannel.show(); 24 | await new Promise((resolve: (value: unknown) => void, reject: (error: unknown) => void) => { 25 | emulatorTask.catch(reject); 26 | setTimeout(resolve, emulatorTimeoutMS); 27 | }); 28 | 29 | await ext.rgApi.workspaceResourceTree.refresh(context, ext.attachedStorageAccountsTreeItem); 30 | } else { 31 | warnAzuriteNotInstalled(context); 32 | } 33 | } 34 | 35 | export enum EmulatorType { 36 | blob = 'blob', 37 | queue = 'queue', 38 | table = 'table' 39 | } 40 | -------------------------------------------------------------------------------- /src/tree/createWizard/StorageAccountTreeItemCreateStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IStorageAccountWizardContext } from "@microsoft/vscode-azext-azureutils"; 7 | import { AzureWizardExecuteStep, ExecuteActivityContext, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; 8 | import { AppResource } from "@microsoft/vscode-azext-utils/hostapi"; 9 | import { nonNullProp } from '../../utils/nonNull'; 10 | import { StorageAccountTreeItem } from "../StorageAccountTreeItem"; 11 | 12 | export interface IStorageAccountTreeItemCreateContext extends IStorageAccountWizardContext, ExecuteActivityContext { 13 | accountTreeItem: StorageAccountTreeItem; 14 | } 15 | 16 | export class StorageAccountTreeItemCreateStep extends AzureWizardExecuteStep { 17 | public priority: number = 170; 18 | public subscription: ISubscriptionContext; 19 | 20 | public constructor(subscription: ISubscriptionContext) { 21 | super(); 22 | this.subscription = subscription; 23 | } 24 | 25 | public async execute(wizardContext: IStorageAccountTreeItemCreateContext): Promise { 26 | wizardContext.accountTreeItem = new StorageAccountTreeItem(this.subscription, nonNullProp(wizardContext, 'storageAccount')); 27 | await wizardContext.accountTreeItem.initStorageAccount(wizardContext); 28 | 29 | const appResource: AppResource = { 30 | id: wizardContext.accountTreeItem.storageAccount.id, 31 | name: wizardContext.accountTreeItem.storageAccount.name, 32 | type: wizardContext.accountTreeItem.storageAccount.type, 33 | }; 34 | wizardContext.activityResult = appResource; 35 | } 36 | 37 | public shouldExecute(): boolean { 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/uploadFolder/GetFolderDestinationDirectoryStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; 7 | import * as vscode from "vscode"; 8 | import { storageFilter } from '../../constants'; 9 | import { ext } from '../../extensionVariables'; 10 | import { BlobContainerTreeItem } from '../../tree/blob/BlobContainerTreeItem'; 11 | import { FileShareTreeItem } from '../../tree/fileShare/FileShareTreeItem'; 12 | import { getDestinationDirectory, upload } from '../../utils/uploadUtils'; 13 | import { IUploadFolderWizardContext } from './IUploadFolderWizardContext'; 14 | 15 | export class GetFolderDestinationDirectoryStep extends AzureWizardPromptStep { 16 | public async prompt(context: IUploadFolderWizardContext): Promise { 17 | if (context.uri === undefined) { 18 | context.uri = (await context.ui.showOpenDialog({ 19 | canSelectFiles: false, 20 | canSelectFolders: true, 21 | canSelectMany: false, 22 | defaultUri: vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri : undefined, 23 | openLabel: upload 24 | }))[0]; 25 | } 26 | context.treeItem = context.treeItem || await ext.rgApi.pickAppResource(context, { 27 | filter: storageFilter, 28 | expectedChildContextValue: [BlobContainerTreeItem.contextValue, FileShareTreeItem.contextValue] 29 | }); 30 | context.destinationDirectory = await getDestinationDirectory(context, context.destinationDirectory); 31 | } 32 | 33 | public shouldPrompt(): boolean { 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/uploadFiles/GetFileDestinationDirectoryStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; 7 | import { storageFilter } from '../../constants'; 8 | import { ext } from '../../extensionVariables'; 9 | import { BlobContainerTreeItem } from '../../tree/blob/BlobContainerTreeItem'; 10 | import { FileShareTreeItem } from '../../tree/fileShare/FileShareTreeItem'; 11 | import { getDestinationDirectory, upload } from '../../utils/uploadUtils'; 12 | import { IUploadFilesWizardContext } from './IUploadFilesWizardContext'; 13 | 14 | export class GetFileDestinationDirectoryStep extends AzureWizardPromptStep { 15 | public async prompt(context: IUploadFilesWizardContext): Promise { 16 | context.destinationDirectory = await getDestinationDirectory(context, context.destinationDirectory); 17 | if (context.uris === undefined) { 18 | context.uris = await context.ui.showOpenDialog( 19 | { 20 | canSelectFiles: true, 21 | canSelectFolders: false, 22 | canSelectMany: true, 23 | defaultUri: ext.lastUriUpload, 24 | filters: { 25 | "All files": ['*'] 26 | }, 27 | openLabel: upload 28 | } 29 | ); 30 | } 31 | context.treeItem = context.treeItem || await ext.rgApi.pickAppResource(context, { 32 | filter: storageFilter, 33 | expectedChildContextValue: [BlobContainerTreeItem.contextValue, FileShareTreeItem.contextValue] 34 | }); 35 | } 36 | 37 | public shouldPrompt(): boolean { 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/tree/table/TableTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, DialogResponses, IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; 7 | import * as path from 'path'; 8 | import { getResourcesPath } from '../../constants'; 9 | import { IStorageRoot } from '../IStorageRoot'; 10 | import { IStorageTreeItem } from '../IStorageTreeItem'; 11 | import { TableGroupTreeItem } from './TableGroupTreeItem'; 12 | 13 | export class TableTreeItem extends AzExtTreeItem implements IStorageTreeItem { 14 | public parent: TableGroupTreeItem; 15 | constructor( 16 | parent: TableGroupTreeItem, 17 | public readonly tableName: string) { 18 | super(parent); 19 | this.iconPath = { 20 | light: path.join(getResourcesPath(), 'light', 'AzureTable.svg'), 21 | dark: path.join(getResourcesPath(), 'dark', 'AzureTable.svg') 22 | }; 23 | } 24 | 25 | public get root(): IStorageRoot { 26 | return this.parent.root; 27 | } 28 | 29 | public label: string = this.tableName; 30 | public static contextValue: string = 'azureTable'; 31 | public contextValue: string = TableTreeItem.contextValue; 32 | 33 | public async deleteTreeItemImpl(context: IActionContext): Promise { 34 | const message: string = `Are you sure you want to delete table '${this.label}' and all its contents?`; 35 | const result = await context.ui.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); 36 | if (result === DialogResponses.deleteResponse) { 37 | const tableServiceClient = await this.root.createTableServiceClient(); 38 | await tableServiceClient.deleteTable(this.tableName); 39 | } else { 40 | throw new UserCancelledError(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tree/blob/createBlobContainer/BlobContainerNameStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { IBlobContainerCreateChildContext } from "../../../utils/blobUtils"; 8 | import { localize } from "../../../utils/localize"; 9 | 10 | export class BlobContainerNameStep extends AzureWizardPromptStep { 11 | public async prompt(context: IActionContext & { name?: string }): Promise { 12 | context.name = await context.ui.showInputBox({ 13 | placeHolder: localize('enterBlobContainerName', 'Enter a name for the new blob container'), 14 | validateInput: this.validateBlobContainerName 15 | }); 16 | } 17 | 18 | public shouldPrompt(context: IBlobContainerCreateChildContext): boolean { 19 | return !context.childName; 20 | } 21 | 22 | private validateBlobContainerName(name: string): string | undefined | null { 23 | const validLength = { min: 3, max: 63 }; 24 | 25 | if (!name) { 26 | return "Container name cannot be empty"; 27 | } else if (/\s/.test(name)) { 28 | return "Container name cannot contain spaces"; 29 | } else if (name.length < validLength.min || name.length > validLength.max) { 30 | return `Container name must contain between ${validLength.min} and ${validLength.max} characters`; 31 | } else if (!/^[a-z0-9-]+$/.test(name)) { 32 | return 'Container name can only contain lowercase letters, numbers and hyphens'; 33 | } else if (/--/.test(name)) { 34 | return 'Container name cannot contain two hyphens in a row'; 35 | } else if (/(^-)|(-$)/.test(name)) { 36 | return 'Container name cannot begin or end with a hyphen'; 37 | } 38 | 39 | return undefined; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/tree/createWizard/StaticWebsiteEnableStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep, DialogResponses, IWizardOptions } from "@microsoft/vscode-azext-utils"; 7 | import { QuickPickItem } from 'vscode'; 8 | import { localize } from "../../utils/localize"; 9 | import { IStaticWebsiteConfigWizardContext } from "./IStaticWebsiteConfigWizardContext"; 10 | import { StaticWebsiteErrorDocument404Step } from "./StaticWebsiteErrorDocument404Step"; 11 | import { StaticWebsiteIndexDocumentStep } from "./StaticWebsiteIndexDocumentStep"; 12 | 13 | export class StaticWebsiteEnableStep extends AzureWizardPromptStep { 14 | public async prompt(context: IStaticWebsiteConfigWizardContext): Promise { 15 | if (!context.isCustomCloud) { 16 | const placeHolder: string = localize('wouldYouLikeToEnableStaticWebsiteHosting', 'Would you like to enable static website hosting?'); 17 | const yes: QuickPickItem = { label: DialogResponses.yes.title }; 18 | const no: QuickPickItem = { label: DialogResponses.no.title }; 19 | 20 | context.enableStaticWebsite = await context.ui.showQuickPick([yes, no], { placeHolder }) === yes; 21 | } else { 22 | context.enableStaticWebsite = false; 23 | } 24 | } 25 | 26 | // eslint-disable-next-line @typescript-eslint/require-await 27 | public async getSubWizard(wizardContext: IStaticWebsiteConfigWizardContext): Promise | undefined> { 28 | if (wizardContext.enableStaticWebsite) { 29 | return { 30 | promptSteps: [new StaticWebsiteIndexDocumentStep(), new StaticWebsiteErrorDocument404Step()] 31 | }; 32 | } 33 | 34 | return undefined; 35 | } 36 | 37 | public shouldPrompt(): boolean { 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/tree/queue/QueueTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import type { QueueItem } from '@azure/storage-queue'; 7 | 8 | import { AzExtTreeItem, DialogResponses, IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; 9 | import * as path from 'path'; 10 | import { getResourcesPath } from "../../constants"; 11 | import { IStorageRoot } from "../IStorageRoot"; 12 | import { IStorageTreeItem } from '../IStorageTreeItem'; 13 | import { QueueGroupTreeItem } from "./QueueGroupTreeItem"; 14 | 15 | export class QueueTreeItem extends AzExtTreeItem implements IStorageTreeItem { 16 | public parent: QueueGroupTreeItem; 17 | constructor( 18 | parent: QueueGroupTreeItem, 19 | public readonly queue: QueueItem) { 20 | super(parent); 21 | this.iconPath = { 22 | light: path.join(getResourcesPath(), 'light', 'AzureQueue.svg'), 23 | dark: path.join(getResourcesPath(), 'dark', 'AzureQueue.svg') 24 | }; 25 | } 26 | 27 | public get root(): IStorageRoot { 28 | return this.parent.root; 29 | } 30 | 31 | public label: string = this.queue.name; 32 | public static contextValue: string = 'azureQueue'; 33 | public contextValue: string = QueueTreeItem.contextValue; 34 | 35 | public async deleteTreeItemImpl(context: IActionContext): Promise { 36 | const message: string = `Are you sure you want to delete queue '${this.label}' and all its contents?`; 37 | const result = await context.ui.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); 38 | if (result === DialogResponses.deleteResponse) { 39 | const queueServiceClient = await this.root.createQueueServiceClient(); 40 | await queueServiceClient.deleteQueue(this.queue.name); 41 | } else { 42 | throw new UserCancelledError(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/deleteFilesAndDirectories.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { ext } from "../extensionVariables"; 8 | import { BlobContainerGroupTreeItem } from "../tree/blob/BlobContainerGroupTreeItem"; 9 | import { FileShareGroupTreeItem } from "../tree/fileShare/FileShareGroupTreeItem"; 10 | import { isTreeItemDirectory } from "../utils/directoryUtils"; 11 | import { isSubpath } from "../utils/fs"; 12 | import { treeUtils } from "../utils/treeUtils"; 13 | 14 | export async function deleteFilesAndDirectories(context: IActionContext, treeItem: AzExtTreeItem, selection: AzExtTreeItem[] = []): Promise { 15 | const parentContainer = treeUtils.findNearestParent(treeItem, [ 16 | BlobContainerGroupTreeItem.contextValue, 17 | FileShareGroupTreeItem.contextValue 18 | ]); 19 | 20 | // Covers both single selection and an edge case where the node you delete from isn't part of the selection 21 | if (!selection.some(s => s === treeItem)) { 22 | await ext.rgApi.appResourceTreeView.reveal(treeItem); 23 | await treeItem.deleteTreeItem(context); 24 | await parentContainer.refresh(context); 25 | return; 26 | } 27 | 28 | const dirPaths: string[] = []; 29 | let shouldSkip; 30 | for (const node of selection) { 31 | shouldSkip = false; 32 | 33 | // Check to see if it's a resource we've already deleted 34 | for (const dirPath of dirPaths) { 35 | if (isSubpath(dirPath, node.fullId)) { 36 | shouldSkip = true; 37 | break; 38 | } 39 | } 40 | if (shouldSkip) continue; 41 | if (isTreeItemDirectory(node)) { 42 | dirPaths.push(node.fullId); 43 | } 44 | await node.deleteTreeItem(context); 45 | } 46 | await parentContainer.refresh(context); 47 | } 48 | -------------------------------------------------------------------------------- /src/tree/fileShare/createFileShare/FileShareNameStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "../../../utils/localize"; 8 | import { IFileShareWizardContext } from "./IFileShareWizardContext"; 9 | 10 | export class FileShareNameStep extends AzureWizardPromptStep { 11 | public async prompt(context: IFileShareWizardContext): Promise { 12 | context.name = await context.ui.showInputBox({ 13 | placeHolder: localize('enterFileShareName', 'Enter a name for the new file share'), 14 | validateInput: this.validateFileShareName 15 | }); 16 | } 17 | 18 | public shouldPrompt(context: IFileShareWizardContext): boolean { 19 | return !context.name; 20 | } 21 | 22 | private validateFileShareName(name: string): string | undefined | null { 23 | const validLength = { min: 3, max: 63 }; 24 | 25 | if (!name) { 26 | return localize('shareNameEmpty', "Share name cannot be empty"); 27 | } else if (/\s/.test(name)) { 28 | return localize('shareNameSpaces', "Share name cannot contain spaces"); 29 | } else if (name.length < validLength.min || name.length > validLength.max) { 30 | return localize('shareNameBetween', 'Share name must contain between {0} and {1} characters', validLength.min, validLength.max); 31 | } else if (!/^[a-z0-9-]+$/.test(name)) { 32 | return localize('shareNameInvalidChar', 'Share name can only contain lowercase letters, numbers and hyphens'); 33 | } else if (/--/.test(name)) { 34 | return localize('shareNameDoubleHyphen', 'Share name cannot contain two hyphens in a row'); 35 | } else if (/(^-)|(-$)/.test(name)) { 36 | return localize('shareNameHyphen', 'Share name cannot begin or end with a hyphen'); 37 | } 38 | 39 | return undefined; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as glob from 'glob'; 7 | import * as Mocha from 'mocha'; 8 | import * as path from 'path'; 9 | 10 | export async function run(): Promise { 11 | const options: Mocha.MochaOptions = { 12 | ui: 'tdd', 13 | color: true, 14 | reporter: 'mocha-multi-reporters', 15 | reporterOptions: { 16 | reporterEnabled: 'spec, mocha-junit-reporter', 17 | mochaJunitReporterReporterOptions: { 18 | mochaFile: path.resolve(__dirname, '..', '..', 'test-results.xml') 19 | } 20 | } 21 | }; 22 | 23 | addEnvVarsToMochaOptions(options); 24 | console.log(`Mocha options: ${JSON.stringify(options, undefined, 2)}`); 25 | 26 | const mocha = new Mocha(options); 27 | 28 | const files: string[] = await new Promise((resolve, reject) => { 29 | glob('**/**.test.js', { cwd: __dirname }, (err, result) => { 30 | err ? reject(err) : resolve(result); 31 | }); 32 | }); 33 | 34 | files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); 35 | 36 | const failures = await new Promise(resolve => mocha.run(resolve)); 37 | if (failures > 0) { 38 | throw new Error(`${failures} tests failed.`); 39 | } 40 | } 41 | 42 | function addEnvVarsToMochaOptions(options: Mocha.MochaOptions): void { 43 | for (const envVar of Object.keys(process.env)) { 44 | const match: RegExpMatchArray | null = envVar.match(/^mocha_(.+)/i); 45 | if (match) { 46 | const [, option] = match; 47 | let value: string | number = process.env[envVar] || ''; 48 | if (typeof value === 'string' && !isNaN(parseInt(value))) { 49 | value = parseInt(value); 50 | } 51 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any 52 | (options)[option] = value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/deleteStorageAccount/DeleteStorageAccountStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; 7 | import { Progress, window } from "vscode"; 8 | import { ext } from "../../extensionVariables"; 9 | import { StorageAccountTreeItem } from "../../tree/StorageAccountTreeItem"; 10 | import { createStorageClient } from "../../utils/azureClients"; 11 | import { localize } from "../../utils/localize"; 12 | import { DeleteStorageAccountWizardContext } from "./DeleteStorageAccountWizardContext"; 13 | 14 | export class DeleteStorageAccountStep extends AzureWizardExecuteStep { 15 | public priority: number = 100; 16 | 17 | public async execute(wizardContext: DeleteStorageAccountWizardContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise { 18 | 19 | const storageAccount = nonNullProp(wizardContext, 'storageAccount'); 20 | 21 | const deletingStorageAccount: string = localize('deletingStorageAccount', 'Deleting storage account "{0}"...', storageAccount.name); 22 | const storageManagementClient = await createStorageClient([wizardContext, wizardContext.subscription]); 23 | const parsedId = StorageAccountTreeItem.parseAzureResourceId(storageAccount.id); 24 | const resourceGroupName = parsedId.resourceGroups; 25 | 26 | ext.outputChannel.appendLog(deletingStorageAccount); 27 | progress.report({ message: deletingStorageAccount }); 28 | await storageManagementClient.storageAccounts.delete(resourceGroupName, storageAccount.name); 29 | 30 | const deleteSuccessful: string = localize('successfullyDeletedStorageAccount', 'Successfully deleted storage account "{0}".', storageAccount.name); 31 | ext.outputChannel.appendLog(deleteSuccessful); 32 | void window.showInformationMessage(deleteSuccessful); 33 | } 34 | 35 | public shouldExecute(): boolean { 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/progress.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Progress, ProgressLocation, ProgressOptions, window } from "vscode"; 7 | import { ext } from "../extensionVariables"; 8 | 9 | type StatusBarProgress = Progress<{ message: string }>; 10 | 11 | /** 12 | * Shows progress in both the output window and the status bar 13 | */ 14 | export async function awaitWithProgress(title: string, promise: Promise, getProgress: () => string): Promise { 15 | const uiIntervalMs = 1 * 500; 16 | const uiUpdatesPerChannelUpdate = 5000 / uiIntervalMs; 17 | let nextChannelUpdate = uiUpdatesPerChannelUpdate; 18 | 19 | let thisProgress: StatusBarProgress; 20 | 21 | void window.withProgress( 22 | { 23 | location: ProgressLocation.Window, 24 | title: title 25 | }, 26 | async (progress: StatusBarProgress): Promise => { 27 | thisProgress = progress; 28 | return promise; 29 | }); 30 | 31 | pollDuringPromise(uiIntervalMs, promise, () => { 32 | const msg = getProgress(); 33 | 34 | nextChannelUpdate -= 1; 35 | if (nextChannelUpdate <= 0) { 36 | nextChannelUpdate = uiUpdatesPerChannelUpdate; 37 | ext.outputChannel.appendLog(`${title}: ${msg}`); 38 | } 39 | 40 | thisProgress.report({ message: msg }); 41 | }); 42 | 43 | return await promise; 44 | } 45 | 46 | /** 47 | * Runs the given poll function repeatedly until the given promise is resolved or rejected 48 | */ 49 | function pollDuringPromise(intervalMs: number, promise: Promise, poll: () => void): void { 50 | let inProgress = true; 51 | promise.then( 52 | () => { 53 | inProgress = false; 54 | }, 55 | () => { 56 | inProgress = false; 57 | } 58 | ); 59 | 60 | const pollFunction = () => { 61 | if (inProgress) { 62 | poll(); 63 | setTimeout(pollFunction, intervalMs); 64 | } 65 | }; 66 | 67 | pollFunction(); 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/downloadFiles/SasUrlPromptStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardPromptStep, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { env } from "vscode"; 8 | import { localize } from "../../utils/localize"; 9 | import { ISasDownloadContext } from "./ISasDownloadContext"; 10 | 11 | export class SasUrlPromptStep extends AzureWizardPromptStep { 12 | public async prompt(context: ISasDownloadContext): Promise { 13 | let value: string | undefined = await env.clipboard.readText(); 14 | if (await this.validateInput(context, value)) { 15 | // if there is a string value here, then the clipboard does not contain a valid SAS url 16 | value = undefined; 17 | } 18 | 19 | context.sasUrl = await context.ui.showInputBox({ 20 | prompt: localize('enterSasUrl', 'Enter a Azure Storage SAS URL'), 21 | validateInput: async (s): Promise => await this.validateInput(context, s), 22 | value 23 | }); 24 | } 25 | 26 | public shouldPrompt(): boolean { 27 | return true; 28 | } 29 | 30 | public async validateInput(_wizardContext: ISasDownloadContext, value: string | undefined): Promise { 31 | if (!value) { 32 | return localize('emptySasUrl', 'A SAS token and URL cannot be empty.') 33 | } 34 | 35 | try { 36 | const url = new URL(value); 37 | // not a comprehensive list of required params 38 | if ( 39 | !url.searchParams.get('sp') || 40 | !url.searchParams.get('se') || 41 | !url.searchParams.get('sig') 42 | ) { 43 | return localize('enterValidToken', 'The SAS token is missing a parameter. Enter a valid SAS token.',) 44 | } 45 | } catch (err) { 46 | return localize('enterValidSasUrl', 'The URL "{0}" is not valid. Enter a valid URL', value); 47 | } 48 | 49 | return undefined; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TransferProgress.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { NotificationProgress } from './constants'; 7 | import { ext } from './extensionVariables'; 8 | 9 | export class TransferProgress { 10 | private message: string = ''; 11 | private percentage: number = 0; 12 | private lastPercentage: number = 0; 13 | private lastUpdated: number = Date.now(); 14 | 15 | constructor( 16 | private readonly units: 'bytes' | 'files' | 'blobs', 17 | private readonly messagePrefix?: string, 18 | private readonly updateTimerMs: number = 200 19 | ) { } 20 | 21 | public reportToNotification(finishedWork: number, totalWork: number, notificationProgress: NotificationProgress): void { 22 | // This function may be called very frequently. Calls made to notificationProgress.report too rapidly result in incremental 23 | // progress not displaying in the notification window. So debounce calls to notificationProgress.report 24 | if (this.lastUpdated + this.updateTimerMs < Date.now()) { 25 | this.preReport(finishedWork, totalWork); 26 | if (this.percentage !== this.lastPercentage) { 27 | notificationProgress.report({ message: this.message, increment: this.percentage - this.lastPercentage }); 28 | } 29 | this.postReport(); 30 | } 31 | } 32 | 33 | public reportToOutputWindow(finishedWork: number, totalWork: number): void { 34 | this.preReport(finishedWork, totalWork); 35 | if (this.percentage !== this.lastPercentage) { 36 | ext.outputChannel.appendLog(this.message); 37 | } 38 | this.postReport(); 39 | } 40 | 41 | private preReport(finishedWork: number, totalWork: number): void { 42 | this.percentage = Math.trunc((finishedWork / totalWork) * 100); 43 | const prefix: string = this.messagePrefix ? `${this.messagePrefix}: ` : ''; 44 | this.message = `${prefix}${finishedWork}/${totalWork} ${this.units} (${this.percentage}%)`; 45 | } 46 | 47 | private postReport(): void { 48 | this.lastPercentage = this.percentage; 49 | this.lastUpdated = Date.now(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/downloadFile.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizard, AzureWizardPromptStep, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { ITransferSrcOrDstTreeItem } from "../tree/ITransferSrcOrDstTreeItem"; 8 | import { createActivityContext } from "../utils/activityUtils"; 9 | import { localize } from "../utils/localize"; 10 | import { DestinationPromptStep } from "./downloadFiles/DestinationPromptStep"; 11 | import { DownloadFilesStep } from "./downloadFiles/DownloadFilesStep"; 12 | import { GetAzCopyDownloadsStep } from "./downloadFiles/GetAzCopyDownloadsStep"; 13 | import { IDownloadWizardContext } from "./downloadFiles/IDownloadWizardContext"; 14 | import { SasUrlPromptStep } from "./downloadFiles/SasUrlPromptStep"; 15 | 16 | export interface IAzCopyDownload { 17 | remoteFileName: string; 18 | remoteFilePath: string; 19 | localFilePath: string; 20 | isDirectory: boolean; 21 | resourceUri: string; 22 | sasToken: string; 23 | } 24 | 25 | export async function download(context: IActionContext, treeItems?: ITransferSrcOrDstTreeItem[]): Promise { 26 | const wizardContext: IDownloadWizardContext = { ...context, ...(await createActivityContext()) }; 27 | const promptSteps: AzureWizardPromptStep[] = [new DestinationPromptStep()]; 28 | if (!treeItems) { 29 | promptSteps.push(new SasUrlPromptStep()); 30 | } else { 31 | wizardContext.treeItems = treeItems; 32 | } 33 | 34 | const wizard: AzureWizard = new AzureWizard(wizardContext, { 35 | promptSteps, 36 | executeSteps: [new GetAzCopyDownloadsStep(), new DownloadFilesStep()], 37 | title: localize('download', 'Download Files') 38 | }); 39 | 40 | await wizard.prompt(); 41 | await wizard.execute(); 42 | } 43 | 44 | export async function downloadSasUrl(context: IActionContext): Promise { 45 | return await download(context); 46 | } 47 | 48 | export async function downloadTreeItems(context: IActionContext, treeItem: ITransferSrcOrDstTreeItem, treeItems?: ITransferSrcOrDstTreeItem[]): Promise { 49 | treeItems ??= [treeItem]; 50 | await download(context, treeItems); 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/uploadFolder/uploadFolder.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, AzureWizard, IActionContext, IWizardOptions, nonNullProp } from '@microsoft/vscode-azext-utils'; 7 | import { basename } from "path"; 8 | import * as vscode from 'vscode'; 9 | import { BlobContainerTreeItem } from '../../tree/blob/BlobContainerTreeItem'; 10 | import { FileShareTreeItem } from '../../tree/fileShare/FileShareTreeItem'; 11 | import { createActivityContext } from '../../utils/activityUtils'; 12 | import { localize } from '../../utils/localize'; 13 | import { IAzCopyResolution } from '../transfers/azCopy/IAzCopyResolution'; 14 | import { GetFolderDestinationDirectoryStep } from './GetFolderDestinationDirectoryStep'; 15 | import { IUploadFolderWizardContext } from './IUploadFolderWizardContext'; 16 | import { UploadFolderStep } from './UploadFolderStep'; 17 | 18 | export async function uploadFolder( 19 | context: IActionContext, 20 | treeItem?: BlobContainerTreeItem | FileShareTreeItem, 21 | _selectedNodes?: AzExtTreeItem[], 22 | uri?: vscode.Uri, 23 | cancellationToken?: vscode.CancellationToken, 24 | destinationDirectory?: string 25 | ): Promise { 26 | const wizardContext: IUploadFolderWizardContext = { 27 | ...context, ...await createActivityContext(), destinationDirectory, treeItem, uri, 28 | calledFromUploadToAzureStorage: uri !== undefined 29 | }; 30 | const wizardOptions: IWizardOptions = { 31 | promptSteps: [new GetFolderDestinationDirectoryStep()], 32 | executeSteps: [new UploadFolderStep(cancellationToken)], 33 | }; 34 | const wizard: AzureWizard = new AzureWizard(wizardContext, wizardOptions); 35 | await wizard.prompt(); 36 | 37 | const nTreeItem: BlobContainerTreeItem | FileShareTreeItem = nonNullProp(wizardContext, "treeItem"); 38 | const nUri: vscode.Uri = nonNullProp(wizardContext, "uri"); 39 | wizardContext.activityTitle = localize('activityLogUploadFolder', 'Upload "{0}" folder to "{1}"', basename(nUri.path), nTreeItem.label); 40 | 41 | await wizard.execute(); 42 | return wizardContext.resolution as IAzCopyResolution; 43 | } 44 | -------------------------------------------------------------------------------- /extension.bundle.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * This is the external face of extension.bundle.js, the main webpack bundle for the extension. 8 | * Anything needing to be exposed outside of the extension sources must be exported from here, because 9 | * everything else will be in private modules in extension.bundle.js. 10 | */ 11 | 12 | // Exports for tests 13 | // The tests are not packaged with the webpack bundle and therefore only have access to code exported from this file. 14 | // 15 | // The tests should import '../extension.bundle.ts'. At design-time they live in tests/ and so will pick up this file (extension.bundle.ts). 16 | // At runtime the tests live in dist/tests and will therefore pick up the main webpack bundle at dist/extension.bundle.js. 17 | export * from '@microsoft/vscode-azext-azureutils'; 18 | export * from '@microsoft/vscode-azext-utils'; 19 | export { ResolvedAppResourceTreeItem } from '@microsoft/vscode-azext-utils/hostapi'; 20 | export * from './src/commands/blob/blobContainerActionHandlers'; 21 | export * from './src/commands/blob/blobContainerGroupActionHandlers'; 22 | export * from './src/commands/createStorageAccount'; 23 | export * from './src/commands/fileShare/fileShareActionHandlers'; 24 | export * from './src/commands/fileShare/fileShareGroupActionHandlers'; 25 | export * from './src/commands/queue/queueActionHandlers'; 26 | export * from './src/commands/queue/queueGroupActionHandlers'; 27 | export * from './src/commands/storageAccountActionHandlers'; 28 | export * from './src/commands/table/tableActionHandlers'; 29 | export * from './src/commands/table/tableGroupActionHandlers'; 30 | // Export activate/deactivate for main.js 31 | export { activate, deactivate } from './src/extension'; 32 | export { ext } from './src/extensionVariables'; 33 | export { ResolvedStorageAccount } from './src/StorageAccountResolver'; 34 | export { StorageAccountTreeItem } from './src/tree/StorageAccountTreeItem'; 35 | export { delay } from './src/utils/delay'; 36 | export { getRandomHexString } from './src/utils/stringUtils'; 37 | 38 | // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen 39 | -------------------------------------------------------------------------------- /src/utils/checkCanOverwrite.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { DialogResponses, IActionContext } from "@microsoft/vscode-azext-utils"; 7 | import { localize } from "./localize"; 8 | import { OverwriteChoice } from "./uploadUtils"; 9 | 10 | // Pass `overwriteChoice` as an object to make use of pass by reference. 11 | export async function checkCanOverwrite( 12 | context: IActionContext, 13 | destPath: string, 14 | overwriteChoice: { choice: OverwriteChoice | undefined }, 15 | destPathExists: () => Promise 16 | ): Promise { 17 | if (overwriteChoice.choice === OverwriteChoice.yesToAll) { 18 | // Always overwrite 19 | return true; 20 | } 21 | 22 | if (await destPathExists()) { 23 | if (overwriteChoice.choice === OverwriteChoice.noToAll) { 24 | // Resources that already exist shouldn't be overwritten 25 | return false; 26 | } else { 27 | overwriteChoice.choice = await showDuplicateResourceWarning(context, destPath); 28 | switch (overwriteChoice.choice) { 29 | case OverwriteChoice.no: 30 | case OverwriteChoice.noToAll: 31 | return false; 32 | 33 | case OverwriteChoice.yes: 34 | case OverwriteChoice.yesToAll: 35 | default: 36 | return true; 37 | } 38 | } 39 | } else { 40 | // This resource doesn't exist yet, so overwriting is OK 41 | return true; 42 | } 43 | } 44 | 45 | async function showDuplicateResourceWarning(context: IActionContext, resourceName: string): Promise { 46 | const message: string = localize('resourceExists', 'A resource named "{0}" already exists. Do you want to overwrite it?', resourceName); 47 | const items = [ 48 | { title: localize('yesToAll', 'Yes to all'), data: OverwriteChoice.yesToAll }, 49 | { title: DialogResponses.yes.title, data: OverwriteChoice.yes }, 50 | { title: localize('noToAll', 'No to all'), data: OverwriteChoice.noToAll }, 51 | { title: DialogResponses.no.title, data: OverwriteChoice.no } 52 | ]; 53 | return (await context.ui.showWarningMessage(message, { modal: true }, ...items)).data; 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/transfers/azCopy/azCopyLocations.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ILocalLocation, IRemoteAuthLocation, IRemoteSasLocation } from "@azure-tools/azcopy-node"; 7 | import { posix, sep } from "path"; 8 | import { DownloadItem, UploadItem } from "../transfers"; 9 | 10 | export function createAzCopyLocalLocation(path: string, isFolder?: boolean): ILocalLocation { 11 | if (isFolder && !path.endsWith(sep)) { 12 | path += sep; 13 | } 14 | return { type: 'Local', path, useWildCard: !!isFolder }; 15 | } 16 | 17 | export async function createAzCopyRemoteLocation(item: DownloadItem | UploadItem): Promise { 18 | let path = item.remoteFilePath; 19 | if (item.isDirectory && !path.endsWith(posix.sep)) { 20 | path += posix.sep; 21 | } 22 | 23 | // Ensure path begins with '/' to transfer properly 24 | path = path[0] === posix.sep ? path : `${posix.sep}${path}`; 25 | let remoteLocation: IRemoteSasLocation | IRemoteAuthLocation; 26 | const { sasToken, resourceUri, treeItem } = item; 27 | 28 | if (treeItem && !treeItem.root.allowSharedKeyAccess) { 29 | const accessToken = await treeItem.root.getAccessToken(); 30 | const refreshToken = treeItem.root.getAccessToken; 31 | remoteLocation = createRemoteAuthLocation(item, path, resourceUri, accessToken, refreshToken); 32 | } else if (sasToken) { 33 | remoteLocation = { 34 | type: 'RemoteSas', 35 | sasToken, 36 | resourceUri, 37 | path, 38 | useWildCard: !!item.isDirectory 39 | }; 40 | } else { 41 | throw new Error(`No sasToken or accessToken found for resourceUri "${resourceUri}".`); 42 | } 43 | 44 | return remoteLocation; 45 | } 46 | 47 | function createRemoteAuthLocation(item: DownloadItem | UploadItem, path: string, resourceUri: string, accessToken: string, refreshToken: () => Promise): IRemoteAuthLocation { 48 | const tenantId = item.treeItem?.root.tenantId ?? ''; 49 | return { 50 | type: 'RemoteAuth', 51 | authToken: accessToken, 52 | refreshToken, 53 | tenantId, 54 | resourceUri, 55 | path, 56 | aadEndpoint: 'https://storage.azure.com', 57 | useWildCard: !!item.isDirectory 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/uploadFiles/uploadFiles.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, AzureWizard, IActionContext, IWizardOptions, nonNullProp } from "@microsoft/vscode-azext-utils"; 7 | import { basename } from "path"; 8 | import { CancellationToken, Uri } from "vscode"; 9 | import { BlobContainerTreeItem } from "../../tree/blob/BlobContainerTreeItem"; 10 | import { FileShareTreeItem } from "../../tree/fileShare/FileShareTreeItem"; 11 | import { createActivityContext } from "../../utils/activityUtils"; 12 | import { localize } from "../../utils/localize"; 13 | import { IAzCopyResolution } from "../transfers/azCopy/IAzCopyResolution"; 14 | import { GetFileDestinationDirectoryStep } from './GetFileDestinationDirectoryStep'; 15 | import { IUploadFilesWizardContext } from "./IUploadFilesWizardContext"; 16 | import { UploadFilesStep } from "./UploadFilesStep"; 17 | 18 | export async function uploadFiles( 19 | context: IActionContext, 20 | treeItem?: BlobContainerTreeItem | FileShareTreeItem, 21 | _selectedNodes?: AzExtTreeItem[], 22 | uris?: Uri[], 23 | cancellationToken?: CancellationToken, 24 | destinationDirectory?: string 25 | ): Promise { 26 | const wizardContext: IUploadFilesWizardContext = { 27 | ...context, ...await createActivityContext(), destinationDirectory, treeItem, uris, 28 | calledFromUploadToAzureStorage: !!uris?.length 29 | }; 30 | const wizardOptions: IWizardOptions = { 31 | promptSteps: [new GetFileDestinationDirectoryStep()], 32 | executeSteps: [new UploadFilesStep(cancellationToken)], 33 | }; 34 | const wizard: AzureWizard = new AzureWizard(wizardContext, wizardOptions); 35 | await wizard.prompt(); 36 | 37 | const nUris: Uri[] = nonNullProp(wizardContext, "uris"); 38 | const nTreeItem: BlobContainerTreeItem | FileShareTreeItem = nonNullProp(wizardContext, "treeItem"); 39 | if (nUris.length === 1) { 40 | wizardContext.activityTitle = localize('activityLogUploadFiles', 'Upload "{0}" to "{1}"', basename(nUris[0].path), nTreeItem.label); 41 | } else { 42 | wizardContext.activityTitle = localize('activityLogUploadFiles', 'Upload {0} files to "{1}"', nUris.length, nTreeItem.label); 43 | } 44 | 45 | await wizard.execute(); 46 | return wizardContext.resolution as IAzCopyResolution; 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/blobPathUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | function endsWithSlash(path: string) { 7 | return path.endsWith("/"); 8 | } 9 | 10 | export class BlobPathUtils { 11 | /** 12 | * Gets the parent directory of the given `path`. 13 | * This function is unsafe to use on a path to an empty name blob. 14 | * 15 | * @example dirname("abc/efg/") -> "abc/" 16 | */ 17 | public static dirname(path: string): string { 18 | if (endsWithSlash(path)) { 19 | const secondLastIndex = path.slice(0, path.length - 1).lastIndexOf("/"); 20 | return secondLastIndex < 0 ? "" : `${path.slice(0, secondLastIndex)}/`; 21 | } else { 22 | const lastIndex = path.lastIndexOf("/"); 23 | return lastIndex < 0 ? "" : `${path.slice(0, lastIndex)}/`; 24 | } 25 | } 26 | 27 | /** 28 | * Gets the last segment of the given `path`. 29 | * This function is unsafe to use on a path to an empty name blob. 30 | * 31 | * @example basename("abc/efg/") -> "efg/" 32 | */ 33 | public static basename(path: string): string { 34 | const lastIndex = path.lastIndexOf("/"); 35 | if (lastIndex === path.length - 1) { 36 | // path is a directory path 37 | const secondLastIndex = path.slice(0, path.length - 1).lastIndexOf("/"); 38 | return path.slice(secondLastIndex + 1); 39 | } else { 40 | // path is a blob path 41 | return path.slice(lastIndex + 1); 42 | } 43 | } 44 | 45 | /** 46 | * Joins an array of path segments to a base path to form a new path. 47 | */ 48 | public static join(basePath: string, ...segments: [...string[], string]): string { 49 | return basePath + segments.join(""); 50 | } 51 | 52 | /** 53 | * Removes the trailing slash if the `path` ends with a slash. 54 | */ 55 | public static trimSlash(path: string): string { 56 | if (endsWithSlash(path)) { 57 | return path.slice(0, path.length - 1); 58 | } 59 | return path; 60 | } 61 | 62 | /** 63 | * Removes the trailing slash if the `path` ends with a slash. 64 | */ 65 | public static appendSlash(path: string): string { 66 | if (!endsWithSlash(path)) { 67 | return path + "/"; 68 | } 69 | return path; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/fileShare/fileShareActionHandlers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { IActionContext, registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils'; 6 | import * as vscode from 'vscode'; 7 | import { AzureStorageFS } from '../../AzureStorageFS'; 8 | import { storageExplorerLauncher } from '../../storageExplorerLauncher/storageExplorerLauncher'; 9 | import { DirectoryTreeItem } from '../../tree/fileShare/DirectoryTreeItem'; 10 | import { FileShareTreeItem, IFileShareCreateChildContext } from '../../tree/fileShare/FileShareTreeItem'; 11 | import { FileTreeItem } from '../../tree/fileShare/FileTreeItem'; 12 | import { deleteNode } from '../commonTreeCommands'; 13 | 14 | export function registerFileShareActionHandlers(): void { 15 | registerCommandWithTreeNodeUnwrapping("azureStorage.openFileShare", openFileShareInStorageExplorer); 16 | registerCommandWithTreeNodeUnwrapping("azureStorage.editFile", async (context: IActionContext, treeItem: FileTreeItem) => AzureStorageFS.showEditor(context, treeItem), 250); 17 | registerCommandWithTreeNodeUnwrapping("azureStorage.deleteFileShare", deleteFileShare); 18 | registerCommandWithTreeNodeUnwrapping("azureStorage.createDirectory", async (context: IActionContext, treeItem: FileShareTreeItem) => await treeItem.createChild({ ...context, childType: DirectoryTreeItem.contextValue })); 19 | registerCommandWithTreeNodeUnwrapping("azureStorage.createFile", async (context: IActionContext, treeItem: FileShareTreeItem) => { 20 | const childTreeItem = await treeItem.createChild({ ...context, childType: FileTreeItem.contextValue }); 21 | await vscode.commands.executeCommand("azureStorage.editFile", childTreeItem); 22 | }); 23 | } 24 | 25 | async function openFileShareInStorageExplorer(_context: IActionContext, treeItem: FileShareTreeItem): Promise { 26 | const accountId = treeItem.root.storageAccountId; 27 | const subscriptionid = treeItem.subscription.subscriptionId; 28 | const resourceType = 'Azure.FileShare'; 29 | const resourceName = treeItem.shareName; 30 | 31 | await storageExplorerLauncher.openResource(accountId, subscriptionid, resourceType, resourceName); 32 | } 33 | 34 | export async function deleteFileShare(context: IActionContext, treeItem?: FileShareTreeItem): Promise { 35 | await deleteNode(context, FileShareTreeItem.contextValue, treeItem); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/validateNames.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { localize } from "./localize"; 7 | 8 | const invalidBlobDirectoryChar = '\\'; // Keeps behavior consistent on Mac & Windows when creating blob directories via the file explorer 9 | const invalidFileAndDirectoryChars = ['"', '/', '\\', ':', '|', '<', '>', '?', '*']; 10 | const invalidFileAndDirectoryCharsString = invalidFileAndDirectoryChars.join(', '); 11 | 12 | export enum DocumentType { 13 | index = 'index', 14 | error = 'error' 15 | } 16 | 17 | export function validateBlobDirectoryName(name: string): string | undefined { 18 | if (!name || name.includes(invalidBlobDirectoryChar)) { 19 | return `Directory name cannot be empty or contain '${invalidBlobDirectoryChar}'`; 20 | } 21 | 22 | return undefined; 23 | } 24 | 25 | export function validateFileOrDirectoryName(name: string): string | undefined { 26 | const validLength = { min: 1, max: 255 }; 27 | 28 | if (!name) { 29 | return localize('nameCantBeEmpty', 'Name cannot be empty'); 30 | } 31 | 32 | if (name.length < validLength.min || name.length > validLength.max) { 33 | return localize('nameMustContain', 'Name must contain between {0} and {1} characters', validLength.min, validLength.max); 34 | } 35 | 36 | if (invalidFileAndDirectoryChars.some(ch => name.indexOf(ch) >= 0)) { 37 | return localize('nameCantContain', 'Name cannot contain the following characters: {0}', invalidFileAndDirectoryCharsString); 38 | } 39 | 40 | return undefined; 41 | } 42 | 43 | export function validateDocumentPath(documentPath: string, documentType: DocumentType): undefined | string { 44 | const minLengthDocumentPath = 3; 45 | const maxLengthDocumentPath = 255; 46 | 47 | if (documentType === DocumentType.index && documentPath.includes('/')) { 48 | return localize('indexDocumentPathCannotContainForwardSlash', 'The index document path cannot contain a "/" character.'); 49 | } else if (documentType === DocumentType.error && (documentPath.startsWith('/') || documentPath.endsWith('/'))) { 50 | return localize('errorDocumentCannotStartOrEndWithForwardSlash', 'The error document path start or end with a "/" character.'); 51 | } else if (documentPath.length < minLengthDocumentPath || documentPath.length > maxLengthDocumentPath) { 52 | return localize('documentPathLengthIsInvalid', 'The {0} document path must be between {1} and {2} characters in length.', documentType, minLengthDocumentPath, maxLengthDocumentPath); 53 | } 54 | 55 | return undefined; 56 | } 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request Manager 2 | on: 3 | issues: 4 | types: [milestoned] 5 | schedule: 6 | - cron: 15 5 * * * # 10:15pm PT 7 | workflow_dispatch: 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Actions 14 | if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') 15 | uses: actions/checkout@v3 16 | with: 17 | repository: "microsoft/vscode-github-triage-actions" 18 | path: ./actions 19 | ref: stable 20 | - name: Install Actions 21 | if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') 22 | run: npm install --production --prefix ./actions 23 | - name: Run Feature Request Manager 24 | if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') 25 | uses: ./actions/feature-request 26 | with: 27 | app_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_ID }} 28 | app_installation_id: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_INSTALLATION_ID }} 29 | app_private_key: ${{ secrets.AZURETOOLS_VSCODE_BOT_APP_PRIVATE_KEY }} 30 | owner: "microsoft" 31 | repo: "vscode-azurestorage" 32 | candidateMilestoneID: 4 33 | candidateMilestoneName: "Backlog Candidates" 34 | backlogMilestoneID: 24 35 | featureRequestLabel: "feature" 36 | upvotesRequired: 5 37 | numCommentsOverride: 10 38 | initComment: "This feature request is now a candidate for our backlog. The community has 240 days to upvote the issue. If it receives 5 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" 39 | rejectComment: ":slightly_frowning_face: In the last 60 days, this issue has received less than 5 community upvotes and we closed it. Still a big Thank You to you for taking the time to create it! To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" 40 | rejectLabel: "out of scope" 41 | warnComment: "This issue has become stale and is at risk of being closed. The community has 60 days to upvote the issue. If it receives 5 upvotes we will keep it open and take another look. If not, we will close it. To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" 42 | labelsToExclude: "P0,P1" 43 | acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" 44 | warnDays: 60 45 | closeDays: 240 46 | milestoneDelaySeconds: 60 47 | -------------------------------------------------------------------------------- /src/tree/createWizard/storageAccountNameStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CheckNameAvailabilityResult, StorageManagementClient } from '@azure/arm-storage'; 7 | import { IStorageAccountWizardContext, ResourceGroupListStep, resourceGroupNamingRules, storageAccountNamingRules } from '@microsoft/vscode-azext-azureutils'; 8 | import { AzureNameStep } from '@microsoft/vscode-azext-utils'; 9 | import { createStorageClient } from '../../utils/azureClients'; 10 | 11 | export class StorageAccountNameStep extends AzureNameStep { 12 | public async prompt(context: T): Promise { 13 | const client: StorageManagementClient = await createStorageClient(context); 14 | 15 | const suggestedName: string | undefined = context.relatedNameTask ? await context.relatedNameTask : undefined; 16 | context.newStorageAccountName = (await context.ui.showInputBox({ 17 | value: suggestedName, 18 | prompt: 'Enter a globally unique name for the new Storage Account', 19 | validateInput: async (value: string): Promise => await this.validateStorageAccountName(client, value) 20 | })).trim(); 21 | context.valuesToMask.push(context.newStorageAccountName); 22 | if (!context.relatedNameTask) { 23 | context.relatedNameTask = this.generateRelatedName(context, context.newStorageAccountName, resourceGroupNamingRules); 24 | } 25 | } 26 | 27 | public shouldPrompt(wizardContext: T): boolean { 28 | return !wizardContext.newStorageAccountName; 29 | } 30 | 31 | protected async isRelatedNameAvailable(wizardContext: T, name: string): Promise { 32 | return await ResourceGroupListStep.isNameAvailable(wizardContext, name); 33 | } 34 | 35 | private async validateStorageAccountName(client: StorageManagementClient, name: string): Promise { 36 | name = name ? name.trim() : ''; 37 | 38 | if (!name || name.length < storageAccountNamingRules.minLength || name.length > storageAccountNamingRules.maxLength) { 39 | return `The name must be between ${storageAccountNamingRules.minLength} and ${storageAccountNamingRules.maxLength} characters.`; 40 | } else if (name.match(storageAccountNamingRules.invalidCharsRegExp) !== null) { 41 | return "The name can only contain lowercase letters and numbers."; 42 | } else { 43 | const nameAvailabilityResult: CheckNameAvailabilityResult = await client.storageAccounts.checkNameAvailability({ name, type: 'Microsoft.Storage/storageAccounts' }); 44 | if (!nameAvailabilityResult.nameAvailable) { 45 | return nameAvailabilityResult.message; 46 | } else { 47 | return undefined; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/StartingResourcesLogStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { type ResourceGroup } from "@azure/arm-resources"; 7 | import { LocationListStep, type ILocationWizardContext } from "@microsoft/vscode-azext-azureutils"; 8 | import { ActivityChildItem, ActivityChildType, activityInfoContext, activityInfoIcon, AzureWizardPromptStep, createContextValue, type ExecuteActivityContext, type IActionContext } from "@microsoft/vscode-azext-utils"; 9 | import { ext } from "../extensionVariables"; 10 | import { ActivityInfoChild, prependOrInsertAfterLastInfoChild } from "../utils/activityUtils"; 11 | import { localize } from "../utils/localize"; 12 | 13 | type StartingResourcesLogContext = IActionContext & Partial & ILocationWizardContext & { 14 | resourceGroup?: ResourceGroup, 15 | }; 16 | 17 | const startingResourcesContext: string = 'startingResourcesLogStepItem'; 18 | 19 | /** 20 | * Use to display primary Azure resource data to the output and activity log 21 | * i.e. resource group, location, etc. 22 | */ 23 | export class StartingResourcesLogStep extends AzureWizardPromptStep { 24 | public hideStepCount: boolean = true; 25 | protected hasLogged: boolean = false; 26 | 27 | /** 28 | * Implement if you require additional context loading before resource logging 29 | */ 30 | protected configureStartingResources?(context: T): void | Promise; 31 | 32 | public async configureBeforePrompt(context: T): Promise { 33 | if (this.hasLogged) { 34 | return; 35 | } 36 | await this.configureStartingResources?.(context); 37 | await this.logStartingResources(context); 38 | } 39 | 40 | public async prompt(): Promise { 41 | // Don't prompt, just use to log starting resources 42 | } 43 | 44 | public shouldPrompt(): boolean { 45 | return false; 46 | } 47 | 48 | protected async logStartingResources(context: T): Promise { 49 | if (context.resourceGroup) { 50 | prependOrInsertAfterLastInfoChild(context, 51 | new ActivityChildItem({ 52 | contextValue: createContextValue([startingResourcesContext, activityInfoContext]), 53 | label: localize('useResourceGroup', 'Use resource group "{0}"', context.resourceGroup.name), 54 | activityType: ActivityChildType.Info, 55 | iconPath: activityInfoIcon 56 | }) as ActivityInfoChild 57 | ); 58 | ext.outputChannel.appendLog(localize('usingResourceGroup', 'Using resource group "{0}".', context.resourceGroup.name)); 59 | } 60 | 61 | if (LocationListStep.hasLocation(context)) { 62 | const location: string = (await LocationListStep.getLocation(context)).name; 63 | ext.outputChannel.appendLog(localize('usingLocation', 'Using location: "{0}".', location)); 64 | } 65 | 66 | this.hasLogged = true; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/quickPickUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtFsExtra, IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; 7 | import * as path from 'path'; 8 | import * as vscode from 'vscode'; 9 | import { extensionPrefix } from '../constants'; 10 | 11 | export async function showWorkspaceFoldersQuickPick(placeHolderString: string, context: IActionContext, subPathSetting: string | undefined): Promise { 12 | const folderQuickPickItems: IAzureQuickPickItem[] = []; 13 | if (vscode.workspace.workspaceFolders) { 14 | for (const workspaceFolder of vscode.workspace.workspaceFolders) { 15 | { 16 | let fsPath: string = workspaceFolder.uri.fsPath; 17 | if (subPathSetting) { 18 | const subpath: string | undefined = vscode.workspace.getConfiguration(extensionPrefix, workspaceFolder.uri).get(subPathSetting); 19 | if (subpath) { 20 | fsPath = path.join(fsPath, subpath); 21 | } 22 | } 23 | 24 | folderQuickPickItems.push({ 25 | label: path.basename(fsPath), 26 | description: fsPath, 27 | data: fsPath 28 | }); 29 | 30 | // If the workspace has any of build, dist, or out, show those as well 31 | const buildDefaultPaths = ["build", "dist", "out"]; 32 | for (const defaultPath of buildDefaultPaths) { 33 | const buildPath: string = path.join(fsPath, defaultPath); 34 | if (await AzExtFsExtra.pathExists(buildPath)) { 35 | folderQuickPickItems.push({ 36 | label: path.basename(buildPath), 37 | description: buildPath, 38 | data: buildPath 39 | }); 40 | } 41 | } 42 | 43 | } 44 | } 45 | } 46 | 47 | folderQuickPickItems.unshift({ label: '$(file-directory) Browse...', description: '', data: undefined }); 48 | 49 | const folderQuickPickOption = { placeHolder: placeHolderString }; 50 | context.telemetry.properties.cancelStep = 'showWorkspaceFolders'; 51 | const pickedItem = await context.ui.showQuickPick(folderQuickPickItems, folderQuickPickOption); 52 | 53 | if (!pickedItem.data) { 54 | context.telemetry.properties.cancelStep = 'showWorkspaceFoldersBrowse'; 55 | const browseResult = await context.ui.showOpenDialog({ 56 | canSelectFiles: false, 57 | canSelectFolders: true, 58 | canSelectMany: false, 59 | defaultUri: vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri : undefined 60 | }); 61 | 62 | context.telemetry.properties.cancelStep = undefined; 63 | return browseResult[0].fsPath; 64 | } else { 65 | context.telemetry.properties.cancelStep = undefined; 66 | return pickedItem.data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/uploadFolder/UploadFolderStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardExecuteStep, IParsedError, nonNullValue, parseError } from "@microsoft/vscode-azext-utils"; 7 | import * as vscode from "vscode"; 8 | import { NotificationProgress } from '../../constants'; 9 | import { refreshTreeItem } from "../../tree/refreshTreeItem"; 10 | import { isAzCopyError } from "../../utils/errorUtils"; 11 | import { checkCanUpload, convertLocalPathToRemotePath, getUploadingMessageWithSource, showUploadSuccessMessage, uploadLocalFolder } from "../../utils/uploadUtils"; 12 | import { IAzCopyResolution } from "../transfers/azCopy/IAzCopyResolution"; 13 | import { IUploadFolderWizardContext } from "./IUploadFolderWizardContext"; 14 | 15 | export class UploadFolderStep extends AzureWizardExecuteStep { 16 | public priority: number = 100; 17 | 18 | public constructor(private readonly cancellationToken?: vscode.CancellationToken) { 19 | super(); 20 | } 21 | 22 | public async execute(context: Required, notificationProgress: NotificationProgress): Promise { 23 | const sourcePath: string = context.uri.fsPath; 24 | const destPath: string = convertLocalPathToRemotePath(sourcePath, context.destinationDirectory); 25 | const resolution: IAzCopyResolution = { errors: [] }; 26 | if (!context.calledFromUploadToAzureStorage && !(await checkCanUpload(context, destPath, { choice: undefined }, context.treeItem))) { 27 | // Don't upload this folder 28 | context.resolution = resolution; 29 | return; 30 | } 31 | 32 | try { 33 | if (notificationProgress && this.cancellationToken) { 34 | await uploadLocalFolder(context, context.treeItem, sourcePath, destPath, notificationProgress, this.cancellationToken, destPath); 35 | } else { 36 | const title: string = getUploadingMessageWithSource(sourcePath, context.treeItem.label); 37 | await vscode.window.withProgress({ cancellable: true, location: vscode.ProgressLocation.Notification, title }, async (newNotificationProgress, newCancellationToken) => { 38 | await uploadLocalFolder(context, nonNullValue(context.treeItem), sourcePath, nonNullValue(destPath), newNotificationProgress, newCancellationToken, destPath); 39 | }); 40 | } 41 | } catch (error) { 42 | const parsedError: IParsedError = parseError(error); 43 | if (context.calledFromUploadToAzureStorage && isAzCopyError(parsedError)) { 44 | // `uploadToAzureStorage` will deal with this error 45 | resolution.errors.push(parsedError); 46 | } else { 47 | throw error; 48 | } 49 | } 50 | 51 | if (!context.calledFromUploadToAzureStorage) { 52 | showUploadSuccessMessage(context.treeItem.label); 53 | } 54 | 55 | await refreshTreeItem(context, context.treeItem); 56 | context.resolution = resolution; 57 | } 58 | 59 | public shouldExecute(): boolean { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/openInFileExplorer/OpenTreeItemStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; 7 | import { commands, Uri, workspace, WorkspaceFolder } from 'vscode'; 8 | import { AzureStorageFS } from '../../AzureStorageFS'; 9 | import { BlobContainerFS } from '../../BlobContainerFS'; 10 | import { nonNullProp } from "../../utils/nonNull"; 11 | import { IOpenInFileExplorerWizardContext } from './IOpenInFileExplorerWizardContext'; 12 | 13 | export class OpenTreeItemStep extends AzureWizardExecuteStep { 14 | public priority: number = 250; 15 | public hideStepCount: boolean = true; 16 | 17 | 18 | public async execute(context: IOpenInFileExplorerWizardContext): Promise { 19 | const openFolders: readonly WorkspaceFolder[] = workspace.workspaceFolders || []; 20 | if (context.openBehavior === 'AddToWorkspace' && openFolders.length === 0) { 21 | // no point in adding to an empty workspace 22 | context.openBehavior = 'OpenInCurrentWindow'; 23 | } 24 | 25 | const treeItem = nonNullProp(context, 'treeItem'); 26 | 27 | if (!AzureStorageFS.isAttachedAccount(treeItem)) { 28 | const storageAccountId = treeItem.root.storageAccountId; 29 | const serviceType = 'container' in treeItem ? "blob" : "fileShare"; 30 | const containerName = 'container' in treeItem ? treeItem.container.name : treeItem.shareName; 31 | let uriByService: Uri; 32 | if (serviceType === "blob") { 33 | uriByService = BlobContainerFS.constructUri(containerName, storageAccountId); 34 | } else { 35 | // @todo: Use static methods from FileShareFS 36 | uriByService = Uri.parse(`azurestorage:///${containerName}?resourceId=${storageAccountId}&name=${containerName}`); 37 | } 38 | 39 | if (context.openBehavior === 'AddToWorkspace') { 40 | workspace.updateWorkspaceFolders(openFolders.length, 0, { uri: uriByService }); 41 | await commands.executeCommand('workbench.view.explorer'); 42 | } else { 43 | await commands.executeCommand('vscode.openFolder', uriByService, context.openBehavior === 'OpenInNewWindow' /* forceNewWindow */); 44 | } 45 | } else { 46 | // @todo: Support attached accounts in BlobContainerFS 47 | const uri = AzureStorageFS.idToUri(nonNullProp(context, 'treeItem').fullId); 48 | if (context.openBehavior === 'AddToWorkspace') { 49 | // @todo: Test if this should the BlobContainerFS uri or the original uri 50 | workspace.updateWorkspaceFolders(openFolders.length, 0, { uri: uri }); 51 | await commands.executeCommand('workbench.view.explorer'); 52 | } else { 53 | await commands.executeCommand('vscode.openFolder', uri, context.openBehavior === 'OpenInNewWindow' /* forceNewWindow */); 54 | } 55 | } 56 | } 57 | 58 | public shouldExecute(context: IOpenInFileExplorerWizardContext): boolean { 59 | return !!context.openBehavior && context.openBehavior !== 'AlreadyOpen' && context.openBehavior !== 'DontOpen'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tree/createWizard/StaticWebsiteConfigureStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import type { BlobServiceProperties } from "@azure/storage-blob"; 7 | 8 | import { AzExtTreeItem, AzureWizardExecuteStepWithActivityOutput } from "@microsoft/vscode-azext-utils"; 9 | import { NotificationProgress } from '../../constants'; 10 | import { ext } from '../../extensionVariables'; 11 | import { localize } from '../../utils/localize'; 12 | import { StorageAccountTreeItem } from "../StorageAccountTreeItem"; 13 | import { IStaticWebsiteConfigWizardContext } from "./IStaticWebsiteConfigWizardContext"; 14 | import { IStorageAccountTreeItemCreateContext } from "./StorageAccountTreeItemCreateStep"; 15 | 16 | export class StaticWebsiteConfigureStep extends AzureWizardExecuteStepWithActivityOutput { 17 | public priority: number = 200; 18 | public stepName: string = 'staticWebsiteConfigureStep'; 19 | public accountTreeItem: StorageAccountTreeItem | undefined; 20 | 21 | private outputLogMessage: string; 22 | private previouslyEnabled: boolean | undefined; 23 | 24 | protected getOutputLogSuccess = (): string => this.outputLogMessage; 25 | protected getOutputLogFail = (): string => this.outputLogMessage; 26 | protected getTreeItemLabel(context: T): string { 27 | this.accountTreeItem ??= context.accountTreeItem; 28 | 29 | return this.previouslyEnabled ? 30 | localize('updateStaticHostingConfig', 'Update static website hosting config for storage account "{0}"', this.accountTreeItem.label) : 31 | localize('enableStaticHostingConfig', 'Enable static website hosting for storage account "{0}"', this.accountTreeItem.label); 32 | } 33 | 34 | public constructor(accountTreeItem?: StorageAccountTreeItem, previouslyEnabled?: boolean) { 35 | super(); 36 | this.accountTreeItem = accountTreeItem; 37 | this.previouslyEnabled = previouslyEnabled; 38 | } 39 | 40 | public async execute(wizardContext: T, progress: NotificationProgress): Promise { 41 | this.accountTreeItem = this.accountTreeItem || wizardContext.accountTreeItem; 42 | 43 | progress.report({ message: localize('configuringStaticWebsiteHosting', `Configuring static website hosing for storage account "${this.accountTreeItem.label}"...`) }); 44 | 45 | const newStatus: BlobServiceProperties = { 46 | staticWebsite: { 47 | enabled: true, 48 | indexDocument: wizardContext.indexDocument, 49 | errorDocument404Path: wizardContext.errorDocument404Path 50 | } 51 | }; 52 | 53 | await this.accountTreeItem.setWebsiteHostingProperties(newStatus); 54 | 55 | this.outputLogMessage = this.previouslyEnabled ? 56 | localize('staticWebsiteHostingConfigurationUpdated', 'Static website hosting configuration updated for storage account "{0}".', this.accountTreeItem.label) : 57 | localize('storageAccountHasBeenEnabledForStaticWebsiteHosting', 'The storage account "{0}" has been enabled for static website hosting.', this.accountTreeItem.label); 58 | this.outputLogMessage += localize('indexDocumentAndErrorDocument', ' Index document: "{0}", 404 error document: "{1}"', wizardContext.indexDocument, wizardContext.errorDocument404Path); 59 | 60 | if (newStatus.staticWebsite && this.previouslyEnabled !== newStatus.staticWebsite.enabled) { 61 | await ext.rgApi.appResourceTree.refresh(wizardContext, this.accountTreeItem as unknown as AzExtTreeItem); 62 | } 63 | } 64 | 65 | public shouldExecute(wizardContext: T): boolean { 66 | return wizardContext.enableStaticWebsite; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/deleteBlobDirectory/DeleteBlobDirectoryStep.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { AzExtTreeItem, AzureWizardExecuteStep, nonNullProp, parseError } from "@microsoft/vscode-azext-utils"; 7 | import { MessageItem, Progress, window } from "vscode"; 8 | import { ext } from "../../extensionVariables"; 9 | import { BlobDirectoryTreeItem } from "../../tree/blob/BlobDirectoryTreeItem"; 10 | import { BlobTreeItem, ISuppressMessageContext } from "../../tree/blob/BlobTreeItem"; 11 | import { localize } from "../../utils/localize"; 12 | import { IDeleteBlobDirectoryWizardContext } from "./IDeleteBlobDirectoryWizardContext"; 13 | 14 | export class DeleteBlobDirectoryStep extends AzureWizardExecuteStep { 15 | public priority: number = 100; 16 | 17 | public async execute(wizardContext: IDeleteBlobDirectoryWizardContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise { 18 | const directoryName = nonNullProp(wizardContext, 'dirName'); 19 | const deletingBlobDirectory: string = localize('deletingDirectory', 'Deleting directory "{0}"...', directoryName); 20 | ext.outputChannel.appendLog(deletingBlobDirectory); 21 | progress.report({ message: deletingBlobDirectory }); 22 | const errors: boolean = await this.deleteFolder(wizardContext); 23 | if (errors) { 24 | ext.outputChannel.appendLog('Please refresh the viewlet to see the changes made.'); 25 | 26 | const viewOutput: MessageItem = { title: 'View Errors' }; 27 | const errorMessage: string = `Errors occurred when deleting "${directoryName}".`; 28 | void window.showWarningMessage(errorMessage, viewOutput).then((result: MessageItem | undefined) => { 29 | if (result === viewOutput) { 30 | ext.outputChannel.show(); 31 | } 32 | }); 33 | 34 | throw new Error(`Errors occurred when deleting "${directoryName}".`); 35 | } 36 | const deleteSuccessful: string = localize('successfullyDeletedBlobDirectory', 'Successfully deleted directory "{0}".', directoryName); 37 | ext.outputChannel.appendLog(deleteSuccessful); 38 | 39 | if (!wizardContext.suppressNotification) { 40 | void window.showInformationMessage(deleteSuccessful); 41 | } 42 | } 43 | 44 | public shouldExecute(): boolean { 45 | return true; 46 | } 47 | 48 | private async deleteFolder(context: IDeleteBlobDirectoryWizardContext): Promise { 49 | const blobDirectory = nonNullProp(context, 'blobDirectory'); 50 | 51 | const dirPaths: BlobDirectoryTreeItem[] = []; 52 | // eslint-disable-next-line @typescript-eslint/no-this-alias 53 | let dirPath: BlobDirectoryTreeItem | undefined = blobDirectory; 54 | let errors: boolean = false; 55 | 56 | while (dirPath) { 57 | const children: AzExtTreeItem[] = await dirPath.loadAllChildren(context); 58 | for (const child of children) { 59 | if (child instanceof BlobTreeItem) { 60 | try { 61 | await child.deleteTreeItemImpl({ ...context, suppressMessage: true }); 62 | } catch (error) { 63 | ext.outputChannel.appendLog(`Cannot delete ${child.blobPath}. ${parseError(error).message}`); 64 | errors = true; 65 | } 66 | } else if (child instanceof BlobDirectoryTreeItem) { 67 | dirPaths.push(child); 68 | } 69 | } 70 | 71 | dirPath = dirPaths.pop(); 72 | } 73 | return errors; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/tree/fileShare/FileTreeItem.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import type { AccountSASSignatureValues, ShareFileClient } from '@azure/storage-file-share'; 7 | 8 | import { polyfill } from '../../polyfill.worker'; 9 | polyfill(); 10 | 11 | import { AccountSASPermissions } from '@azure/storage-file-share'; 12 | 13 | import { AzExtTreeItem, DialogResponses, IActionContext, TreeItemIconPath, UserCancelledError } from '@microsoft/vscode-azext-utils'; 14 | import { posix } from 'path'; 15 | import * as vscode from 'vscode'; 16 | import { MessageItem, window } from 'vscode'; 17 | import { AzureStorageFS } from "../../AzureStorageFS"; 18 | import { threeDaysInMS } from '../../constants'; 19 | import { copyAndShowToast } from '../../utils/copyAndShowToast'; 20 | import { createFileClient, deleteFile } from '../../utils/fileUtils'; 21 | import { ICopyUrl } from '../ICopyUrl'; 22 | import { IStorageRoot } from '../IStorageRoot'; 23 | import { ITransferSrcOrDstTreeItem } from '../ITransferSrcOrDstTreeItem'; 24 | import { DirectoryTreeItem, IDirectoryDeleteContext } from "./DirectoryTreeItem"; 25 | import { FileShareTreeItem } from './FileShareTreeItem'; 26 | 27 | export class FileTreeItem extends AzExtTreeItem implements ICopyUrl, ITransferSrcOrDstTreeItem { 28 | public parent: FileShareTreeItem | DirectoryTreeItem; 29 | constructor( 30 | parent: FileShareTreeItem | DirectoryTreeItem, 31 | public readonly fileName: string, 32 | public readonly directoryPath: string, 33 | public readonly shareName: string, 34 | public readonly resourceUri: string) { 35 | super(parent); 36 | this.commandId = 'azureStorage.editFile'; 37 | } 38 | 39 | public label: string = this.fileName; 40 | public static contextValue: string = 'azureFile'; 41 | public contextValue: string = FileTreeItem.contextValue; 42 | 43 | public get root(): IStorageRoot { 44 | return this.parent.root; 45 | } 46 | 47 | public get remoteFilePath(): string { 48 | return posix.join(this.directoryPath, this.fileName); 49 | } 50 | 51 | public get iconPath(): TreeItemIconPath { 52 | return new vscode.ThemeIcon('file'); 53 | } 54 | 55 | public get transferSasToken(): string { 56 | const accountSASSignatureValues: AccountSASSignatureValues = { 57 | expiresOn: new Date(Date.now() + threeDaysInMS), 58 | permissions: AccountSASPermissions.parse("rwl"), // read, write, list 59 | services: 'f', // file 60 | resourceTypes: 'co' // container, object 61 | }; 62 | return this.root.generateSasToken(accountSASSignatureValues); 63 | } 64 | 65 | public async copyUrl(): Promise { 66 | const fileClient: ShareFileClient = await createFileClient(this.root, this.shareName, this.directoryPath, this.fileName); 67 | const url = fileClient.url; 68 | await copyAndShowToast(url, 'File URL'); 69 | } 70 | 71 | public async deleteTreeItemImpl(context: IActionContext & IDirectoryDeleteContext): Promise { 72 | let result: MessageItem | undefined; 73 | if (!context.suppressMessage) { 74 | const message: string = `Are you sure you want to delete the file '${this.label}'?`; 75 | result = await window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); 76 | } else { 77 | result = DialogResponses.deleteResponse; 78 | } 79 | 80 | if (result === DialogResponses.deleteResponse) { 81 | await deleteFile(this.directoryPath, this.fileName, this.shareName, this.root); 82 | } else { 83 | throw new UserCancelledError(); 84 | } 85 | 86 | AzureStorageFS.fireDeleteEvent(context, this); 87 | } 88 | } 89 | --------------------------------------------------------------------------------