Skip to content

Commit 2a9517b

Browse files
committed
feat: implement maxDepth for azure-storage-blob
Implements native `maxDepth` for the `azure-storage-blob` driver by using hierarchical fetches.
1 parent cc0b0ca commit 2a9517b

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

src/drivers/azure-storage-blob.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,47 @@ export interface AzureStorageBlobOptions {
3636

3737
const DRIVER_NAME = "azure-storage-blob";
3838

39+
async function getKeysByDepth(
40+
client: ContainerClient,
41+
maxDepth: number
42+
): Promise<string[]> {
43+
const queue: Array<{ depth: number; name: string }> = [];
44+
let current: { depth: number; name: string } | undefined = {
45+
name: "",
46+
depth: 0,
47+
};
48+
const keys: string[] = [];
49+
50+
do {
51+
const iterator = client
52+
.listBlobsByHierarchy(":", {
53+
prefix: current.name,
54+
})
55+
.byPage({ maxPageSize: 1000 });
56+
57+
for await (const result of iterator) {
58+
const { blobPrefixes, blobItems } = result.segment;
59+
60+
if (blobPrefixes && current.depth < maxDepth) {
61+
for (const childPrefix of blobPrefixes) {
62+
queue.push({
63+
name: childPrefix.name,
64+
depth: current.depth + 1,
65+
});
66+
}
67+
}
68+
69+
for (const item of blobItems) {
70+
keys.push(item.name);
71+
}
72+
}
73+
74+
current = queue.pop();
75+
} while (current !== undefined);
76+
77+
return keys;
78+
}
79+
3980
export default defineDriver((opts: AzureStorageBlobOptions) => {
4081
let containerClient: ContainerClient;
4182
const getContainerClient = () => {
@@ -81,6 +122,9 @@ export default defineDriver((opts: AzureStorageBlobOptions) => {
81122
return {
82123
name: DRIVER_NAME,
83124
options: opts,
125+
flags: {
126+
maxDepth: true,
127+
},
84128
getInstance: getContainerClient,
85129
async hasItem(key) {
86130
return await getContainerClient().getBlockBlobClient(key).exists();
@@ -108,7 +152,11 @@ export default defineDriver((opts: AzureStorageBlobOptions) => {
108152
async removeItem(key) {
109153
await getContainerClient().getBlockBlobClient(key).delete();
110154
},
111-
async getKeys() {
155+
async getKeys(_base, opts) {
156+
if (opts?.maxDepth !== undefined) {
157+
return getKeysByDepth(getContainerClient(), opts.maxDepth);
158+
}
159+
112160
const iterator = getContainerClient()
113161
.listBlobsFlat()
114162
.byPage({ maxPageSize: 1000 });

test/drivers/azure-storage-blob.test.ts

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { describe, beforeAll, afterAll } from "vitest";
1+
import {
2+
describe,
3+
beforeAll,
4+
afterAll,
5+
it,
6+
expect,
7+
vi,
8+
afterEach,
9+
} from "vitest";
210
import driver from "../../src/drivers/azure-storage-blob";
311
import { testDriver } from "./utils";
412
import { BlobServiceClient } from "@azure/storage-blob";
513
import { ChildProcess, exec } from "node:child_process";
14+
import { ContainerClient } from "@azure/storage-blob";
615

716
describe.skip("drivers: azure-storage-blob", () => {
817
let azuriteProcess: ChildProcess;
@@ -17,10 +26,65 @@ describe.skip("drivers: azure-storage-blob", () => {
1726
afterAll(() => {
1827
azuriteProcess.kill(9);
1928
});
29+
afterEach(() => {
30+
vi.restoreAllMocks();
31+
});
2032
testDriver({
2133
driver: driver({
2234
connectionString: "UseDevelopmentStorage=true",
2335
accountName: "local",
2436
}),
37+
additionalTests(ctx) {
38+
it("natively supports depth in getKeys", async () => {
39+
const spy = vi.spyOn(ContainerClient.prototype, "listBlobsByHierarchy");
40+
41+
await ctx.storage.setItem("depth-test/key0", "boop");
42+
await ctx.storage.setItem("depth-test/depth0/key1", "boop");
43+
await ctx.storage.setItem("depth-test/depth0/depth1/key2", "boop");
44+
await ctx.storage.setItem("depth-test/depth0/depth1/key3", "boop");
45+
46+
expect(
47+
(
48+
await ctx.driver.getKeys('', {
49+
maxDepth: 1,
50+
})
51+
).sort()
52+
).toMatchObject(["depth-test:key0"]);
53+
54+
// assert that the underlying blob storage was only called upto 1 depth
55+
// to confirm the native filtering was used
56+
expect(spy).toHaveBeenCalledTimes(2);
57+
expect(spy).toHaveBeenCalledWith(":", {
58+
// azure actually mutates `options` and sets `prefix` to
59+
// `undefined` even though we pass it in as `""`. it seems this
60+
// assertion works by reference, so we see the mutated value
61+
prefix: undefined,
62+
});
63+
expect(spy).toHaveBeenCalledWith(":", {
64+
prefix: "depth-test:",
65+
});
66+
67+
spy.mockClear();
68+
69+
expect(
70+
(
71+
await ctx.driver.getKeys('', {
72+
maxDepth: 2,
73+
})
74+
).sort()
75+
).toMatchObject(["depth-test:depth0:key1", "depth-test:key0"]);
76+
77+
expect(spy).toHaveBeenCalledTimes(3);
78+
expect(spy).toHaveBeenCalledWith(":", {
79+
prefix: undefined,
80+
});
81+
expect(spy).toHaveBeenCalledWith(":", {
82+
prefix: "depth-test:",
83+
});
84+
expect(spy).toHaveBeenCalledWith(":", {
85+
prefix: "depth-test:depth0:",
86+
});
87+
});
88+
},
2589
});
2690
});

test/drivers/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export function testDriver(opts: TestOptions) {
196196
}
197197

198198
it("removeItem", async () => {
199+
await ctx.storage.setItem("s1:a", "test_data");
199200
await ctx.storage.removeItem("s1:a", false);
200201
expect(await ctx.storage.hasItem("s1:a")).toBe(false);
201202
expect(await ctx.storage.getItem("s1:a")).toBe(null);

0 commit comments

Comments
 (0)