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

feat(azure-storage-blob): native maxDepth support #553

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 49 additions & 1 deletion src/drivers/azure-storage-blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@

const DRIVER_NAME = "azure-storage-blob";

async function getKeysByDepth(
client: ContainerClient,
maxDepth: number
): Promise<string[]> {
const queue: Array<{ depth: number; name: string }> = [];
let current: { depth: number; name: string } | undefined = {
name: "",
depth: 0,
};
const keys: string[] = [];

Check warning on line 48 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L39-L48

Added lines #L39 - L48 were not covered by tests

do {
const iterator = client
.listBlobsByHierarchy(":", {
prefix: current.name,
})
.byPage({ maxPageSize: 1000 });

Check warning on line 55 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L50-L55

Added lines #L50 - L55 were not covered by tests

for await (const result of iterator) {
const { blobPrefixes, blobItems } = result.segment;

Check warning on line 58 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L57-L58

Added lines #L57 - L58 were not covered by tests

if (blobPrefixes && current.depth < maxDepth) {
for (const childPrefix of blobPrefixes) {
queue.push({
name: childPrefix.name,
depth: current.depth + 1,
});
}
}

Check warning on line 67 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L60-L67

Added lines #L60 - L67 were not covered by tests

for (const item of blobItems) {
keys.push(item.name);
}
}

Check warning on line 72 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L69-L72

Added lines #L69 - L72 were not covered by tests

current = queue.pop();
} while (current !== undefined);

Check warning on line 75 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L74-L75

Added lines #L74 - L75 were not covered by tests

return keys;
}

Check warning on line 78 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L77-L78

Added lines #L77 - L78 were not covered by tests

export default defineDriver((opts: AzureStorageBlobOptions) => {
let containerClient: ContainerClient;
const getContainerClient = () => {
Expand Down Expand Up @@ -81,6 +122,9 @@
return {
name: DRIVER_NAME,
options: opts,
flags: {
maxDepth: true,
},
getInstance: getContainerClient,
async hasItem(key) {
return await getContainerClient().getBlockBlobClient(key).exists();
Expand Down Expand Up @@ -108,7 +152,11 @@
async removeItem(key) {
await getContainerClient().getBlockBlobClient(key).delete();
},
async getKeys() {
async getKeys(_base, opts) {
if (opts?.maxDepth !== undefined) {
return getKeysByDepth(getContainerClient(), opts.maxDepth);
}

Check warning on line 158 in src/drivers/azure-storage-blob.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/azure-storage-blob.ts#L156-L158

Added lines #L156 - L158 were not covered by tests

const iterator = getContainerClient()
.listBlobsFlat()
.byPage({ maxPageSize: 1000 });
Expand Down
85 changes: 84 additions & 1 deletion test/drivers/azure-storage-blob.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { describe, beforeAll, afterAll } from "vitest";
import {
describe,
beforeAll,
afterAll,
it,
expect,
vi,
afterEach,
} from "vitest";
import driver from "../../src/drivers/azure-storage-blob";
import { testDriver } from "./utils";
import { BlobServiceClient } from "@azure/storage-blob";
import { ChildProcess, exec } from "node:child_process";
import { ContainerClient } from "@azure/storage-blob";

describe.skip("drivers: azure-storage-blob", () => {
let azuriteProcess: ChildProcess;
Expand All @@ -17,10 +26,84 @@ describe.skip("drivers: azure-storage-blob", () => {
afterAll(() => {
azuriteProcess.kill(9);
});
afterEach(() => {
vi.restoreAllMocks();
});
testDriver({
driver: driver({
connectionString: "UseDevelopmentStorage=true",
accountName: "local",
}),
additionalTests(ctx) {
it("throws when no account name", async () => {
const badDriver = driver({
connectionString: "UseDevelopmentStorage=true",
accountName: "",
});
expect(() => {
// trigger initialisation of the client
badDriver.getInstance?.();
}).toThrow("[unstorage] [azure-storage-blob] accountName");
});

it("native meta", async () => {
await ctx.storage.setItem("foo:bar", "test_data");
const meta = await ctx.storage.getMeta("foo:bar");
// undefined because we didn't access it yet
expect(meta.atime).toBe(undefined);
expect(meta.mtime?.constructor.name).toBe("Date");
});

it("natively supports depth in getKeys", async () => {
const spy = vi.spyOn(ContainerClient.prototype, "listBlobsByHierarchy");

await ctx.storage.setItem("depth-test/key0", "boop");
await ctx.storage.setItem("depth-test/depth0/key1", "boop");
await ctx.storage.setItem("depth-test/depth0/depth1/key2", "boop");
await ctx.storage.setItem("depth-test/depth0/depth1/key3", "boop");

expect(
(
await ctx.driver.getKeys("", {
maxDepth: 1,
})
).sort()
).toMatchObject(["depth-test:key0"]);

// assert that the underlying blob storage was only called upto 1 depth
// to confirm the native filtering was used
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith(":", {
// azure actually mutates `options` and sets `prefix` to
// `undefined` even though we pass it in as `""`. it seems this
// assertion works by reference, so we see the mutated value
prefix: undefined,
});
expect(spy).toHaveBeenCalledWith(":", {
prefix: "depth-test:",
});

spy.mockClear();

expect(
(
await ctx.driver.getKeys("", {
maxDepth: 2,
})
).sort()
).toMatchObject(["depth-test:depth0:key1", "depth-test:key0"]);

expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenCalledWith(":", {
prefix: undefined,
});
expect(spy).toHaveBeenCalledWith(":", {
prefix: "depth-test:",
});
expect(spy).toHaveBeenCalledWith(":", {
prefix: "depth-test:depth0:",
});
});
},
});
});
1 change: 1 addition & 0 deletions test/drivers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export function testDriver(opts: TestOptions) {
}

it("removeItem", async () => {
await ctx.storage.setItem("s1:a", "test_data");
await ctx.storage.removeItem("s1:a", false);
expect(await ctx.storage.hasItem("s1:a")).toBe(false);
expect(await ctx.storage.getItem("s1:a")).toBe(null);
Expand Down
Loading