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): add raw support #565

Open
wants to merge 5 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
4 changes: 4 additions & 0 deletions docs/2.drivers/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ The driver supports the following authentication methods:
- `containerName`: The name of the blob container to use. Defaults to `unstorage`.
- `accountKey`: The account key to use for authentication. This is only required if you are using `AzureNamedKeyCredential`.
- `sasKey`: The SAS token to use for authentication. This is only required if you are using `AzureSASCredential`.
- `sasUrl`: The SAS URL of the storage account. This is an alternative to providing `accountName` and `sasKey` separately. The URL can be either:
- A storage account URL: `https://<account>.blob.core.windows.net?<sas-token>`
- A container URL: `https://<account>.blob.core.windows.net/<container>?<sas-token>` you must specify the `containerName` option
- `connectionString`: The storage accounts' connection string. `accountKey` and `sasKey` take precedence.
- `endpointSuffix`: Storage account endpoint suffix. Needs to be changed for Microsoft Azure operated by 21Vianet, Azure Government or Azurite. Defaults to `.blob.core.windows.net`.

## Azure Table Storage

Expand Down
76 changes: 66 additions & 10 deletions src/drivers/azure-storage-blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export interface AzureStorageBlobOptions {
/**
* The name of the Azure Storage account.
*/
accountName: string;
accountName?: string;

/**
* The name of the storage container. All entities will be stored in the same container.
* The name of the storage container. All entities will be stored in the same container. Will be created if it doesn't exist.
* @default "unstorage"
*/
containerName?: string;
Expand All @@ -24,41 +24,69 @@ export interface AzureStorageBlobOptions {
accountKey?: string;

/**
* The SAS key. If provided, the account key will be ignored.
* The SAS token. If provided, the account key will be ignored. Include at least read, list and write permissions to be able to list keys.
*/
sasKey?: string;

/**
* The SAS URL. If provided, the account key, SAS key and container name will be ignored.
*/
sasUrl?: string;

/**
* The connection string. If provided, the account key and SAS key will be ignored. Only available in Node.js runtime.
*/
connectionString?: string;

/**
* Storage account endpoint suffix. Need to be changed for Microsoft Azure operated by 21Vianet, Azure Government or Azurite.
* @default ".blob.core.windows.net"
*/
endpointSuffix?: string;
}

const DRIVER_NAME = "azure-storage-blob";

export default defineDriver((opts: AzureStorageBlobOptions) => {
let containerClient: ContainerClient;
const endpointSuffix = opts.endpointSuffix || ".blob.core.windows.net";
const getContainerClient = () => {
if (containerClient) {
return containerClient;
}
if (!opts.accountName) {
throw createError(DRIVER_NAME, "accountName");
if (!(opts.connectionString || opts.sasUrl) && !opts.accountName) {
throw createError(DRIVER_NAME, "missing accountName");
}
let serviceClient: BlobServiceClient;
if (opts.accountKey) {
// StorageSharedKeyCredential is only available in Node.js runtime, not in browsers
const credential = new StorageSharedKeyCredential(
opts.accountName,
opts.accountName!,
opts.accountKey
);
serviceClient = new BlobServiceClient(
`https://${opts.accountName}.blob.core.windows.net`,
`https://${opts.accountName}${endpointSuffix}`,
credential
);
} else if (opts.sasUrl) {
if (
opts.containerName &&
opts.sasUrl.includes(`${opts.containerName}?`)
) {
// Check if the sas url is a container url
containerClient = new ContainerClient(`${opts.sasUrl}`);
return containerClient;
}
serviceClient = new BlobServiceClient(opts.sasUrl);
} else if (opts.sasKey) {
if (opts.containerName) {
containerClient = new ContainerClient(
`https://${opts.accountName}${endpointSuffix}/${opts.containerName}?${opts.sasKey}`
);
return containerClient;
}
serviceClient = new BlobServiceClient(
`https://${opts.accountName}.blob.core.windows.net${opts.sasKey}`
`https://${opts.accountName}${endpointSuffix}?${opts.sasKey}`
);
} else if (opts.connectionString) {
// fromConnectionString is only available in Node.js runtime, not in browsers
Expand All @@ -68,13 +96,14 @@ export default defineDriver((opts: AzureStorageBlobOptions) => {
} else {
const credential = new DefaultAzureCredential();
serviceClient = new BlobServiceClient(
`https://${opts.accountName}.blob.core.windows.net`,
`https://${opts.accountName}${endpointSuffix}`,
credential
);
}
containerClient = serviceClient.getContainerClient(
opts.containerName || "unstorage"
);
containerClient.createIfNotExists();
return containerClient;
};

Expand All @@ -100,13 +129,40 @@ export default defineDriver((opts: AzureStorageBlobOptions) => {
return null;
}
},
async getItemRaw(key) {
try {
const blob = await getContainerClient()
.getBlockBlobClient(key)
.download();
if (isBrowser) {
return blob.blobBody ? await blobToString(await blob.blobBody) : null;
}
return blob.readableStreamBody
? await streamToBuffer(blob.readableStreamBody)
: null;
} catch {
return null;
}
},
async setItem(key, value) {
await getContainerClient()
.getBlockBlobClient(key)
.upload(value, Buffer.byteLength(value));
},
async setItemRaw(key, value) {
await getContainerClient()
.getBlockBlobClient(key)
.upload(value, Buffer.byteLength(value));
},
async removeItem(key) {
await getContainerClient().getBlockBlobClient(key).delete();
const exists = await getContainerClient()
.getBlockBlobClient(key)
.exists();
if (exists) {
await getContainerClient()
.getBlockBlobClient(key)
.delete({ deleteSnapshots: "include" });
}
},
async getKeys() {
const iterator = getContainerClient()
Expand Down
83 changes: 77 additions & 6 deletions test/drivers/azure-storage-blob.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,97 @@
import { describe, beforeAll, afterAll } from "vitest";
import { describe, expect, beforeAll, afterAll, test } from "vitest";
import { readFile } from "../../src/drivers/utils/node-fs";
import driver from "../../src/drivers/azure-storage-blob";
import { testDriver } from "./utils";
import { BlobServiceClient } from "@azure/storage-blob";
import { AccountSASPermissions, BlobServiceClient } from "@azure/storage-blob";
import { ChildProcess, exec } from "node:child_process";
import { createStorage } from "../../src";

describe.skip("drivers: azure-storage-blob", () => {
let azuriteProcess: ChildProcess;
let sasUrl: string;
beforeAll(async () => {
azuriteProcess = exec("npx azurite-blob --silent");
azuriteProcess = exec("pnpm exec azurite-blob --silent");
const client = BlobServiceClient.fromConnectionString(
"UseDevelopmentStorage=true"
"UseDevelopmentStorage=true;"
);
const containerClient = client.getContainerClient("unstorage");
await containerClient.createIfNotExists();
sasUrl = client.generateAccountSasUrl(
new Date(Date.now() + 1000 * 60),
AccountSASPermissions.from({ read: true, list: true, write: true })
);
});
afterAll(() => {
azuriteProcess.kill(9);
});
testDriver({
driver: driver({
connectionString: "UseDevelopmentStorage=true",
accountName: "local",
connectionString: "UseDevelopmentStorage=true;",
accountName: "devstoreaccount1",
}),
additionalTests() {
test("no empty account name", async () => {
const invalidStorage = createStorage({
driver: driver({
accountKey: "UseDevelopmentStorage=true",
} as any),
});
await expect(
async () => await invalidStorage.hasItem("test")
).rejects.toThrowError("missing accountName");
});
test("sas key", async ({ skip }) => {
if (
!process.env.AZURE_STORAGE_BLOB_SAS_KEY ||
!process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME
) {
skip();
}
const storage = createStorage({
driver: driver({
sasKey: process.env.AZURE_STORAGE_BLOB_SAS_KEY,
accountName: process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME,
containerName: "unstorage",
}),
});
await storage.getKeys();
});
test("sas url", async () => {
const storage = createStorage({
driver: driver({
sasUrl,
containerName: "unstorage",
}),
});
await storage.getKeys();
});
test("account key", async ({ skip }) => {
if (
!process.env.AZURE_STORAGE_BLOB_ACCOUNT_KEY ||
!process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME
) {
skip();
}
const storage = createStorage({
driver: driver({
accountName: process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME,
accountKey: process.env.AZURE_STORAGE_BLOB_ACCOUNT_KEY,
}),
});
await storage.getKeys();
});
test("use DefaultAzureCredential", async ({ skip }) => {
if (!process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME) {
skip();
}
const storage = createStorage({
driver: driver({
accountName: process.env.AZURE_STORAGE_BLOB_ACCOUNT_NAME,
containerName: "unstorage",
}),
});
await storage.getKeys();
});
},
});
});