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

[Storage] Async iterator for listing blobs and containers - storage-blob #3136

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9524d20
Async iterator for container - listBlobs
HarshaNalluru May 22, 2019
eecb12a
remove iter in the generator
HarshaNalluru May 22, 2019
28e8773
Add marker in the generator
HarshaNalluru May 22, 2019
dc03069
yield -> yield *
HarshaNalluru May 22, 2019
bb41616
Add sample using `.next().value`
HarshaNalluru May 23, 2019
da1afa5
containerClient - *listContainers
HarshaNalluru May 23, 2019
34e3772
udpdate - remove resumable from comment
HarshaNalluru May 23, 2019
eb3b1ca
Addressed comments
HarshaNalluru May 23, 2019
72cd066
Add description for options attribute
HarshaNalluru May 23, 2019
7d7cb2f
remove process.env
HarshaNalluru May 23, 2019
7922584
un-specify the type Models.xxx
HarshaNalluru May 24, 2019
d1cf6d0
remove !options
HarshaNalluru May 24, 2019
c8128b5
rectify indentation
HarshaNalluru May 24, 2019
49c1da6
Merge branch 'AsyncIteratorsStorageBlob' of https://github.com/Harsha…
HarshaNalluru May 24, 2019
c272c6a
listBlobs -> listBlobsFlat
HarshaNalluru May 28, 2019
5b07b44
Add test for ListContainers
HarshaNalluru May 28, 2019
cdd3d2e
remove .only
HarshaNalluru May 28, 2019
43d30c0
Add test for listBlobsFlat
HarshaNalluru May 28, 2019
bc932b3
resolve merge conflicts
HarshaNalluru May 29, 2019
3fb743a
Merge remote-tracking branch 'upstream/feature/storage' into AsyncIte…
HarshaNalluru May 30, 2019
842de33
Add test case for listBlobsFlat - for loop syntax
HarshaNalluru May 30, 2019
d7b7f71
Add test case for listContainers - for loop syntax
HarshaNalluru May 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions sdk/storage/storage-blob/samples/typescript/iterators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Setup: Enter your storage account name and shared key in main()
*/

import {
ContainerClient,
BlobServiceClient,
StorageClient,
SharedKeyCredential,
BlobClient,
BlockBlobClient
} from "../../src"; // Change to "@azure/storage-blob" in your package

async function main() {
// Enter your storage account name and shared key
const account = "";
const accountKey = "";

// Use SharedKeyCredential with storage account and account key
const sharedKeyCredential = new SharedKeyCredential(account, accountKey);

// Use sharedKeyCredential, tokenCredential or anonymousCredential to create a pipeline
const pipeline = StorageClient.newPipeline(sharedKeyCredential);

// List containers
const blobServiceClient = new BlobServiceClient(
// When using AnonymousCredential, following url should include a valid SAS or support public access
`https://${account}.blob.core.windows.net`,
pipeline
);

// List Containers
let iter1 = await blobServiceClient.listContainers();
let i = 1;
for await (const container of iter1) {
console.log(`Container ${i++}: ${container.name}`);
}

// List containers - generator syntax
let iter2 = await blobServiceClient.listContainers();
i = 1;
let containerItem = await iter2.next();
do {
console.log(`Container ${i++}: ${containerItem.value.name}`);
containerItem = await iter2.next();
} while (containerItem.value);

// Create a container
const containerName = `newcontainer${new Date().getTime()}`;
const containerClient = ContainerClient.fromBlobServiceClient(blobServiceClient, containerName);

const createContainerResponse = await containerClient.create();
console.log(`Created container ${containerName} successfully`, createContainerResponse.requestId);

for (let index = 0; index < 4; index++) {
// Create a blob
let content = "hello";
let blobName = "newblob" + new Date().getTime();
let blobClient = BlobClient.fromContainerClient(containerClient, blobName);
let blockBlobClient = BlockBlobClient.fromBlobClient(blobClient);
let uploadBlobResponse = await blockBlobClient.upload(content, content.length);
console.log(`Uploaded block blob ${blobName} successfully`, uploadBlobResponse.requestId);
}

// List blobs
iter1 = await containerClient.listBlobsFlat();
i = 1;
for await (const blob of iter1) {
console.log(`Blob ${i++}: ${blob.name}`);
}

// List blobs - generator syntax
iter2 = await containerClient.listBlobsFlat();
i = 1;
let blobItem = await iter2.next();
do {
console.log(`Blob ${i++}: ${blobItem.value.name}`);
blobItem = await iter2.next();
} while (blobItem.value);
}

// An async method returns a Promise object, which is compatible with then().catch() coding style.
main()
.then(() => {
console.log("Successfully executed the sample.");
})
.catch((err) => {
console.log(err.message);
});
47 changes: 47 additions & 0 deletions sdk/storage/storage-blob/src/BlobServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,53 @@ export class BlobServiceClient extends StorageClient {
});
}

/**
* Iterates over containers under the specified account.
*
* @param {ServiceListContainersSegmentOptions} [options={}] Options to list containers(optional)
* @returns {AsyncIterableIterator<Models.ContainerItem>}
* @memberof BlobServiceClient
*
* @example
* for await (const container of blobServiceClient.listContainers()) {
Copy link
Member

Choose a reason for hiding this comment

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

Just wondering what will happen if Aborter aborted iteration

Copy link
Member Author

Choose a reason for hiding this comment

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

Example -

  // List containers - generator syntax
  let iter2 = await blobServiceClient.listContainers({ abortSignal: Aborter.timeout(X) });
  i = 1;
  let containerItem = await iter2.next();
  do {
    console.log(`Container ${i++}: ${containerItem.value.name}`);
    containerItem = await iter2.next();
  } while (containerItem.value);

[ About 6500 containers are present in the account ]

  • When X = 50000 (large number), all the 6500 containers got listed(and their names are printed in the terminal)
  • When X = 2000, only 5000 containers got listed and a message got printed saying The request was aborted.
    image
    [ X = 10000 yielded the same result ]
  • When X < 1000, none of the containers got listed, The request was aborted is printed in the terminal.

* console.log(`Container: ${container.name}`);
* }
*
* @example
* let iter1 = blobServiceClient.listContainers();
* let i = 1;
* for await (const container of iter1) {
* console.log(`${i}: ${container.name}`);
* i++;
* }
*
* @example
* let iter2 = await blobServiceClient.listContainers();
* i = 1;
* let containerItem = await iter2.next();
* do {
* console.log(`Container ${i++}: ${containerItem.value.name}`);
* containerItem = await iter2.next();
* } while (containerItem.value);
*
*/
public async *listContainers(
jeremymeng marked this conversation as resolved.
Show resolved Hide resolved
options: ServiceListContainersSegmentOptions = {}
): AsyncIterableIterator<Models.ContainerItem> {
let marker = undefined;
const blobServiceClient = this;
const aborter = !options.abortSignal ? Aborter.none : options.abortSignal;
let listContainersResponse;
do {
listContainersResponse = await blobServiceClient.listContainersSegment(marker, {
...options,
abortSignal: aborter
});
marker = listContainersResponse.nextMarker;
yield* listContainersResponse.containerItems;
} while (marker);
}

/**
* Returns a list of the containers under the specified account.
* @see https://docs.microsoft.com/en-us/rest/api/storageservices/list-containers2
Expand Down
47 changes: 47 additions & 0 deletions sdk/storage/storage-blob/src/ContainerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,53 @@ export class ContainerClient extends StorageClient {
});
}

/**
* Iterates over blobs under the specified container.
*
* @param {ContainerListBlobsSegmentOptions} [options={}] Options to list blobs(optional)
* @returns {AsyncIterableIterator<Models.BlobItem>}
* @memberof ContainerClient
*
* @example
* for await (const blob of containerClient.listBlobs()) {
* console.log(`Container: ${blob.name}`);
* }
*
* @example
* let iter1 = containerClient.listBlobs();
* let i = 1;
* for await (const blob of iter1) {
* console.log(`${i}: ${blob.name}`);
* i++;
* }
*
* @example
* let iter2 = await containerClient.listBlobs();
* i = 1;
* let blobItem = await iter2.next();
* do {
* console.log(`Blob ${i++}: ${blobItem.value.name}`);
* blobItem = await iter2.next();
* } while (blobItem.value);
*
*/
public async *listBlobsFlat(
options: ContainerListBlobsSegmentOptions = {}
): AsyncIterableIterator<Models.BlobItem> {
let marker = undefined;
const containerClient = this;
const aborter = !options.abortSignal ? Aborter.none : options.abortSignal;
let listBlobsResponse;
do {
listBlobsResponse = await containerClient.listBlobFlatSegment(marker, {
...options,
abortSignal: aborter
});
marker = listBlobsResponse.nextMarker;
yield* listBlobsResponse.segment.blobItems;
} while (marker);
}

/**
* listBlobFlatSegment returns a single segment of blobs starting from the
* specified Marker. Use an empty Marker to start enumeration from the beginning.
Expand Down
40 changes: 40 additions & 0 deletions sdk/storage/storage-blob/test/blobserviceclient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,46 @@ describe("BlobServiceClient", () => {
await containerClient2.delete();
});

it("Verify AsyncIterator for ListContainers", async () => {
const blobServiceClient = getBSU();

const containerNamePrefix = getUniqueName("container");
const containerName1 = `${containerNamePrefix}x1`;
const containerName2 = `${containerNamePrefix}x2`;
const containerClient1 = blobServiceClient.createContainerClient(containerName1);
const containerClient2 = blobServiceClient.createContainerClient(containerName2);
await containerClient1.create({ metadata: { key: "val" } });
await containerClient2.create({ metadata: { key: "val" } });

const iterator = await blobServiceClient.listContainers({
include: "metadata",
prefix: containerNamePrefix
});

let containerItem = await iterator.next();
assert.ok(containerItem.value.name.startsWith(containerNamePrefix));
assert.ok(containerItem.value.properties.etag.length > 0);
assert.ok(containerItem.value.properties.lastModified);
assert.ok(!containerItem.value.properties.leaseDuration);
assert.ok(!containerItem.value.properties.publicAccess);
assert.deepEqual(containerItem.value.properties.leaseState, "available");
assert.deepEqual(containerItem.value.properties.leaseStatus, "unlocked");
assert.deepEqual(containerItem.value.metadata!.key, "val");

containerItem = await iterator.next();
assert.ok(containerItem.value.name.startsWith(containerNamePrefix));
assert.ok(containerItem.value.properties.etag.length > 0);
assert.ok(containerItem.value.properties.lastModified);
assert.ok(!containerItem.value.properties.leaseDuration);
assert.ok(!containerItem.value.properties.publicAccess);
assert.deepEqual(containerItem.value.properties.leaseState, "available");
assert.deepEqual(containerItem.value.properties.leaseStatus, "unlocked");
assert.deepEqual(containerItem.value.metadata!.key, "val");

await containerClient1.delete();
await containerClient2.delete();
});

it("GetProperties", async () => {
const blobServiceClient = getBSU();
const result = await blobServiceClient.getProperties();
Expand Down
88 changes: 52 additions & 36 deletions sdk/storage/storage-blob/test/containerclient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ describe("ContainerClient", () => {
it("listBlobFlatSegment with default parameters", async () => {
const blobClients = [];
for (let i = 0; i < 3; i++) {
const blobClient = containerClient.createBlobClient(
getUniqueName(`blockblob/${i}`)
);
const blobClient = containerClient.createBlobClient(getUniqueName(`blockblob/${i}`));
const blockBlobClient = blobClient.createBlockBlobClient();
await blockBlobClient.upload("", 0);
blobClients.push(blobClient);
Expand All @@ -188,9 +186,7 @@ describe("ContainerClient", () => {
keyb: "c"
};
for (let i = 0; i < 2; i++) {
const blobClient = containerClient.createBlobClient(
getUniqueName(`${prefix}/${i}`)
);
const blobClient = containerClient.createBlobClient(getUniqueName(`${prefix}/${i}`));
const blockBlobClient = blobClient.createBlockBlobClient();
await blockBlobClient.upload("", 0, {
metadata
Expand Down Expand Up @@ -226,12 +222,44 @@ describe("ContainerClient", () => {
}
});

it("Verify AsyncIterator for listBlobsFlat", async () => {
const blobClients = [];
const prefix = "blockblob";
const metadata = {
keya: "a",
keyb: "c"
};
for (let i = 0; i < 2; i++) {
const blobClient = containerClient.createBlobClient(getUniqueName(`${prefix}/${i}`));
const blockBlobClient = blobClient.createBlockBlobClient();
await blockBlobClient.upload("", 0, {
metadata
});
blobClients.push(blobClient);
}

const iterator = await containerClient.listBlobsFlat({
Copy link
Member

Choose a reason for hiding this comment

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

Do we need test against other usage scenarios for async iterator? such as async for loop

Copy link
Member

@XiaoningLiu XiaoningLiu May 29, 2019

Choose a reason for hiding this comment

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

Can we cover the cross segment scenario? Because implementation of listBlobsFlat needs to handle marker update, while existing test case doesn't cover that

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we cover the cross segment scenario? Because implementation of listBlobsFlat needs to handle marker update, while existing test case doesn't cover that

I'm not sure if we are doing it.

public async *listBlobsFlat(
options: ContainerListBlobsSegmentOptions = {}
): AsyncIterableIterator<Models.BlobItem> {
let marker = undefined;
const containerClient = this;

The current implementation of listBlobsFlat doesn't take marker as an input.
We can make marker as an optional attribute to the method. I'm just not sure if we want to do it.

Copy link
Member Author

@HarshaNalluru HarshaNalluru May 30, 2019

Choose a reason for hiding this comment

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

  • Sorry for the confusion.
  • After an offline discussion with Jeremy, he suggested that you might be referring to the usage of more than one listBlobFlatSegment in the same listBlobsFlat call.

842de33 - Added a for-loop test case.

Outputs (console.logs) to verify your question.
[By passing maxresults in the options for listBlobsFlat ]

Case 1 - maxresults is not set

------------------------- called listBlobFlatSegment -------------------------
blockblob/0155925458974007856
blockblob/1155925459003608508
blockblob/2155925459032203396
blockblob/3155925459061100420
    √ Verify AsyncIterator(for-loop syntax) for listBlobsFlat (2714ms)

Case 2 - maxresults is set to 2

------------------------- called listBlobFlatSegment -------------------------
blockblob/0155925451934305295
blockblob/1155925451964308353
------------------------- called listBlobFlatSegment -------------------------
blockblob/2155925451993002640
blockblob/3155925452022007968
    √ Verify AsyncIterator(for-loop syntax) for listBlobsFlat (3012ms)

include: ["snapshots", "metadata", "uncommittedblobs", "copy", "deleted"],
prefix
});

let blobItem = await iterator.next();
assert.ok(blobClients[0].url.indexOf(blobItem.value.name));
assert.deepStrictEqual(blobItem.value.metadata, metadata);

blobItem = await iterator.next();
assert.ok(blobClients[1].url.indexOf(blobItem.value.name));
assert.deepStrictEqual(blobItem.value.metadata, metadata);

for (const blob of blobClients) {
await blob.delete();
}
});

it("listBlobHierarchySegment with default parameters", async () => {
const blobClients = [];
for (let i = 0; i < 3; i++) {
const blobClient = containerClient.createBlobClient(
getUniqueName(`blockblob${i}/${i}`)
);
const blobClient = containerClient.createBlobClient(getUniqueName(`blockblob${i}/${i}`));
const blockBlobClient = blobClient.createBlockBlobClient();
await blockBlobClient.upload("", 0);
blobClients.push(blobClient);
Expand Down Expand Up @@ -274,45 +302,33 @@ describe("ContainerClient", () => {
blobClients.push(blobClient);
}

const result = await containerClient.listBlobHierarchySegment(
delimiter,
undefined,
{
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 1,
prefix
}
);
const result = await containerClient.listBlobHierarchySegment(delimiter, undefined, {
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 1,
prefix
});
assert.ok(result.serviceEndpoint.length > 0);
assert.ok(containerClient.url.indexOf(result.containerName));
assert.deepStrictEqual(result.segment.blobPrefixes!.length, 1);
assert.deepStrictEqual(result.segment.blobItems!.length, 0);
assert.ok(blobClients[0].url.indexOf(result.segment.blobPrefixes![0].name));

const result2 = await containerClient.listBlobHierarchySegment(
delimiter,
result.nextMarker,
{
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 2,
prefix
}
);
const result2 = await containerClient.listBlobHierarchySegment(delimiter, result.nextMarker, {
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 2,
prefix
});
assert.ok(result2.serviceEndpoint.length > 0);
assert.ok(containerClient.url.indexOf(result2.containerName));
assert.deepStrictEqual(result2.segment.blobPrefixes!.length, 1);
assert.deepStrictEqual(result2.segment.blobItems!.length, 0);
assert.ok(blobClients[0].url.indexOf(result2.segment.blobPrefixes![0].name));

const result3 = await containerClient.listBlobHierarchySegment(
delimiter,
undefined,
{
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 2,
prefix: `${prefix}0${delimiter}`
}
);
const result3 = await containerClient.listBlobHierarchySegment(delimiter, undefined, {
include: ["metadata", "uncommittedblobs", "copy", "deleted"],
maxresults: 2,
prefix: `${prefix}0${delimiter}`
});
assert.ok(result3.serviceEndpoint.length > 0);
assert.ok(containerClient.url.indexOf(result3.containerName));
assert.deepStrictEqual(result3.nextMarker, "");
Expand Down