-
Notifications
You must be signed in to change notification settings - Fork 39
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
Conversation
@@ -1,218 +0,0 @@ | |||
/*--------------------------------------------------------------------------------------------- |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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...", |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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: () => { |
There was a problem hiding this comment.
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()); | ||
}, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}/`; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Forgot to do this originally 😰
@@ -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}`; |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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}`); |
There was a problem hiding this comment.
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[] = []; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: ""`); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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[] = []; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Fixes partially: #328