Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ⬆️ to newest Azure Storage SDK for blobs 🗄 #513

Merged
merged 18 commits into from
Dec 4, 2019

Conversation

wwlorey
Copy link
Contributor

@wwlorey wwlorey commented Nov 18, 2019

Fixes partially: #328

@wwlorey wwlorey requested a review from a team as a code owner November 18, 2019 23:59
@@ -1,218 +0,0 @@
/*---------------------------------------------------------------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was one of the few files that is named correctly (the name should match the class). Also ideally file name changes happen in their own PR so that it doesn't mess with the diff

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I wait to rename those files in a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please :) Also feel free to bother someone to review that PR as soon as you submit it since it should have no functional changes (aka easy to review) and so that we can avoid a ton of merge conflicts

});

const blockBlobClient = treeItem.root.createBlockBlobClient(treeItem.container.name, treeItem.fullPath);
await blockBlobClient.downloadToFile(filePath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the logic that displays progress?

package.json Outdated
@@ -195,7 +195,7 @@
},
{
"command": "azureStorage.createBlockTextBlob",
"title": "Create Empty Text Blob...",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I snuck in the fix for text/plain content type defaults for blobs. Now it guesses the content type when creating/updating and respects the existing content type if it exists. I'll address the fix for file shares separately.

Can we unsneak the fix and do it separately? 🙃

let response = blobServiceClient.listContainers().byPage({ continuationToken, maxPageSize });

// tslint:disable-next-line: no-unsafe-any
return (await response.next()).value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the "unsafe-any" here? Is the package missing correct types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, types aren't provided here :(

For some context, here's an updated version with more typing:

        let response: AsyncIterableIterator<azureStorageBlob.ServiceListContainersSegmentResponse> = blobServiceClient.listContainers().byPage({ continuationToken, maxPageSize });
        // tslint:disable-next-line:no-any
        let next: IteratorResult<azureStorageBlob.ServiceListContainersSegmentResponse, any> = await response.next();

        // tslint:disable-next-line: no-unsafe-any
        return next.value;

return {
metadata: existingProperties.metadata,
blobHTTPHeaders: {
blobCacheControl: existingProperties.cacheControl,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge fan of copying out each property here. Seems like we could get into problems if they ever add properties. Can we just return existingProperties as is? And only modify blobContentMD5?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this isn't ideal. The reason I enumerated everything is because the property names don't match up. Do you know if there's a cleaner way to take care of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per offline conversation: reading properties from some resource returns a lot of info we don't explicitly need when setting new properties. Also the BlobGetPropertiesResponse object structure is different from BlockBlobUploadOptions needed when uploading resources


private createBlobServiceClient(): azureStorageBlob.BlobServiceClient {
const credential = new azureStorageBlob.StorageSharedKeyCredential(this.storageAccount.name, this.key.value);
return new azureStorageBlob.BlobServiceClient(`https://${this.storageAccount.name}.blob.core.windows.net`, credential);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this'll work with sovereign accounts because the url will be different. Check out this.storageAccount.primaryEndpoints.blob which should have the url you need without hard-coding anything

createBlockBlobClient: (containerName: string, blobName: string) => {
const blobContainerClient = this.createBlobContainerClient(containerName);
return blobContainerClient.getBlockBlobClient(blobName);
},
createBlobService: () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the old createBlobService now?

createBlockBlobClient: (containerName: string, blobName: string) => {
const blobContainerClient = this.createBlobContainerClient(containerName);
return blobContainerClient.getBlockBlobClient(blobName);
},
createBlobService: () => {
return azureStorage.createBlobService(this.storageAccount.name, this.key.value, this.storageAccount.primaryEndpoints.blob).withFilter(new azureStorage.ExponentialRetryPolicyFilter());
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an analagous ExponentialRetryPolicyFilter for the new sdk? This was a pretty important fix: #380

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That retry behavior appears to be built in. The default is an exponential retry policy. Those config settings are under retryOptions of type StorageRetryOptions

import * as azureStorage from "azure-storage";
import { ISubscriptionContext } from "vscode-azureextensionui";
import { StorageAccountWrapper } from "../components/storageWrappers";

export interface IStorageRoot extends ISubscriptionContext {
storageAccount: StorageAccountWrapper;
createBlobService(): azureStorage.BlobService;
createBlobServiceClient(): azureStorageBlob.BlobServiceClient;
createBlobContainerClient(containerName: string): azureStorageBlob.ContainerClient;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions seem like they shouldn't be here (except createBlobServiceClient) They all require specific information regarding the container and blob names, but this seems more like a general-purpose class for just leveraging the subscription information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to move them to blobUtils?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so. Eventually we'll have to have similar functions for File, Queue, and Table as well and this file seems like it'd get kind of messy in functionality.


let containersResponse = await this.listContainers();
let createdContainer: azureStorageBlob.ContainerItem | undefined;
for (let container of containersResponse.containerItems) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does containerClient.exist() not work for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

containerClient.exists() only returns a bool and I needed a reference to the container here

public static contextValue: string = 'azureBlobDirectory';
public contextValue: string = 'azureBlobDirectory';

public fullPath: string = path.posix.join(this.parentPath, this.directory.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the fullPath the path on Azure? I'm wondering if we need to use posix for joining the paths since that would be using the OS of the user, not the OS of the storage account for joining paths.

Copy link
Contributor Author

@wwlorey wwlorey Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the blob's path on the user OS

public contextValue: string = 'azureBlobDirectory';

public fullPath: string = path.posix.join(this.parentPath, this.directory.name);
public dirPath: string = `${this.fullPath}/`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if this is the dirPath on the local machine, then we need to check the OS to know which separator to use. I think there's an os.separator for that.

await createBlockBlobFromLocalFile(blobName, containerName, root, filePath, createOptions);
export async function loadMoreBlobChildren(parent: BlobContainerTreeItem | BlobDirectoryTreeItem, continuationToken?: string): Promise<{ children: AzExtTreeItem[], continuationToken?: string }> {
const dirPath = parent instanceof BlobDirectoryTreeItem ? parent.dirPath : '';
const listOptions = dirPath ? { prefix: dirPath } : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does having { prefix: undefined } break the listOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix is OK to be undefined, I'll simplify that logic

@@ -452,19 +453,41 @@ export class BlobContainerTreeItem extends AzureParentTreeItem<IStorageRoot> imp
}

private async uploadFileToBlockBlob(filePath: string, blobPath: string, suppressLogs: boolean = false): Promise<void> {
let blobFriendlyPath = `${this.friendlyContainerName}/${blobPath}`;
const blobFriendlyPath = `${this.friendlyContainerName}/${blobPath}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add typings wherever you can? I'm sure we'll inevitably turn on that tslint rule eventually and it'd just be easier to do it as we go

blobContentType: mime.getType(blobPath) || undefined
},
onProgress: (progress: TransferProgressEvent) => {
if (lastUpdated + updateTimerMs < Date.now()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write a comment explaining why you need to debounce the progress.report.

await window.withProgress({ title: `Downloading ${treeItem.blob.name}`, location: ProgressLocation.Notification }, async (notificationProgress) => {
await blockBlobClient.downloadToFile(filePath, undefined, undefined, {
onProgress: (progress) => {
if (lastUpdated + updateTimerMs < Date.now()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're using this in two different spots, it probably makes sense to extract this to another file and just reuse it.

}

if (!createdContainer) {
throw new Error(`Could not find container ${name}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather not could not find, I think the error should indicate that there was an error in the creation itself. I'd assume if we can't find it, it means it wasn't created properly?

}

public async loadMoreChildrenImpl(clearCache: boolean): Promise<AzExtTreeItem[]> {
const result: AzExtTreeItem[] = [];
let result: AzExtTreeItem[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this from const to let?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why I did that 🤦‍♂️ I'll change it back

// tslint:disable-next-line:promise-function-async // Grandfathered in
listBlobs(currentToken: azureStorage.common.ContinuationToken, maxResults: number = 50): Promise<azureStorage.BlobService.ListBlobsResult> {
return new Promise<azureStorage.BlobService.ListBlobsResult>((resolve, reject) => {
console.log(`${new Date().toLocaleTimeString()}: Querying Azure... Method: listBlobsSegmentedWithPrefix blobContainerName: "${this.container.name}" prefix: ""`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if you were supposed to remove this output with this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some SDK calls had those logs and others didn't so I removed them when I consolidated functions. Do you think that would be good to add back in?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how busy the output looks today. When does it output? I don't see it when I expand a blobContainer tree item.

But also, if you do add it back in, you can leverage ext.outputChannel.appendLog() to have the timestamp prefixed to the output message (rather than manually inserting it as it was coded before)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That outputs during a static website deployment when all blobs to delete are found. I can add it back in

blobs.push(...result.entries);
currentToken = result.continuationToken;

const containerClient: azureStorageBlob.ContainerClient = createBlobContainerClient(this.root, this.container.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can go outside the while loop so you're not creating a new containerClient with each iteration

let currentToken: azureStorage.common.ContinuationToken | undefined;
let blobs: azureStorage.BlobService.BlobResult[] = [];
let currentToken: string | undefined;
let blobs: azureStorageBlob.BlobItem[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This could be const too since it's never reassigned (probably should enable that tslint rule)

});
});
const containerClient: azureStorageBlob.ContainerClient = createBlobContainerClient(this.root, this.container.name);
await containerClient.delete();
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed now. ext.ui.showWarningMessage handles userCancelled errors for us.

https://github.com/microsoft/vscode-azuretools/blob/master/ui/src/AzureUserInput.ts#L86-L87

let errors: boolean = false;

// tslint:disable-next-line: strict-boolean-expressions
while (dirPath) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this was like this before or not, but why use a while loop instead of just using a for loop that iterates over dirPaths?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function wasn't modified by the SDK changes. It looks like they went with a while loop to handle folders & nested folders more easily

Size was being read from a non-existent block blob
private _continuationToken: string | undefined;

public static contextValue: string = 'azureBlobDirectory';
public contextValue: string = 'azureBlobDirectory';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to just change this to = BlobDirectoryTreeItem.contextValue rather than having repeated strings.

}

export class TransferProgressState {
public readonly updateTimerMs: number = 200;
Copy link
Member

@nturinski nturinski Dec 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be a property that you can set in the constructor. I can see instances where people would only want the % to update every second or something.

public readonly updateTimerMs: number = 200;

constructor(
public percentage: number = 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like that these should all not be in the constructor actually. I'm not sure what use-case you would have where you create a new TransferProgressState with these already set.

I think it might make sense to make handleTransferProgress a function of TransferProgressState and make all of those properties private so that you ensure nothing else is setting the properties except handleTransferProgress.

ErrorDocument404Path: errorDocument404Path,
IndexDocument: indexDocument
// tslint:disable-next-line: strict-boolean-expressions
}) || undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this logic added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per offline discussion: errorDocument404Path can't be an empty string, so default to undefined. Also value passed into showInputBox must be of type string

@wwlorey wwlorey merged commit 6021feb into master Dec 4, 2019
@wwlorey wwlorey deleted the wwl/blob-sdk branch December 4, 2019 01:27
@github-actions github-actions bot locked and limited conversation to collaborators Feb 10, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants