diff --git a/.changeset/friendly-wolves-listen.md b/.changeset/friendly-wolves-listen.md new file mode 100644 index 00000000..17f12153 --- /dev/null +++ b/.changeset/friendly-wolves-listen.md @@ -0,0 +1,5 @@ +--- +"@tus/azure-store": patch +--- + +Allow consumers to provide a configured Azure `ContainerClient`. diff --git a/packages/azure-store/README.md b/packages/azure-store/README.md index 26f30be7..7d87e4e0 100644 --- a/packages/azure-store/README.md +++ b/packages/azure-store/README.md @@ -39,6 +39,29 @@ const server = new Server({ // ... ``` +Alternatively, construct a [`ContainerClient`][] yourself to use any +authentication method supported by the Azure SDK: + +```js +import { DefaultAzureCredential } from "@azure/identity"; +import { ContainerClient } from "@azure/storage-blob"; +import { Server } from "@tus/server"; +import { AzureStore } from "@tus/azure-store"; + +const account = process.env.AZURE_ACCOUNT_ID; +const container = process.env.AZURE_CONTAINER_NAME; +const client = new ContainerClient( + `https://${account}.blob.core.windows.net/${container}`, + new DefaultAzureCredential(), +); + +const server = new Server({ + path: "/files", + datastore: new AzureStore({ client }), +}); +// ... +``` + ## API This package exports `AzureStore`. There is no default export. @@ -59,6 +82,12 @@ Azure account key (`string`). Azure storage container name (`string`). +#### `options.client` + +An Azure [`ContainerClient`][] configured by the consumer. Use this instead of +`account`, `accountKey`, and `containerName` to configure authentication, +connection details, and the Azure SDK pipeline yourself. + #### `options.cache` Provide your own cache solution for the metadata of uploads ([`KvStore`][]) to reduce the calls to storage server. @@ -107,3 +136,4 @@ See [kvstores]: https://github.com/tus/tus-node-server/tree/main/packages/server#kvstores [`KvStore`]: https://github.com/tus/tus-node-server/blob/main/packages/utils/src/kvstores/Types.ts [`MemoryKvStore`]: https://github.com/tus/tus-node-server/blob/main/packages/utils/src/kvstores/MemoryKvStore.ts +[`ContainerClient`]: https://learn.microsoft.com/javascript/api/@azure/storage-blob/containerclient diff --git a/packages/azure-store/src/index.ts b/packages/azure-store/src/index.ts index 3c732020..205fbcd6 100644 --- a/packages/azure-store/src/index.ts +++ b/packages/azure-store/src/index.ts @@ -1,14 +1,4 @@ import type stream from 'node:stream' -import debug from 'debug' -import { - DataStore, - Upload, - ERRORS, - type KvStore, - MemoryKvStore, - TUS_RESUMABLE, - Metadata, -} from '@tus/utils' import { type AppendBlobClient, type BlobGetPropertiesResponse, @@ -16,13 +6,32 @@ import { type ContainerClient, StorageSharedKeyCredential, } from '@azure/storage-blob' +import { + DataStore, + ERRORS, + type KvStore, + MemoryKvStore, + Metadata, + TUS_RESUMABLE, + Upload, +} from '@tus/utils' +import debug from 'debug' -type Options = { - cache?: KvStore - account: string - accountKey: string - containerName: string -} +type Options = + | { + cache?: KvStore + client: ContainerClient + account?: never + accountKey?: never + containerName?: never + } + | { + cache?: KvStore + client?: never + account: string + accountKey: string + containerName: string + } const log = debug('tus-node-server:stores:azurestore') @@ -32,7 +41,6 @@ const log = debug('tus-node-server:stores:azurestore') */ export class AzureStore extends DataStore { private cache: KvStore - private blobServiceClient: BlobServiceClient private containerClient: ContainerClient private containerName: string @@ -41,30 +49,32 @@ export class AzureStore extends DataStore { this.cache = options.cache ?? new MemoryKvStore() this.extensions = ['creation', 'creation-defer-length'] - if (!options.account) { - throw new Error('Azure store must have a account') - } - if (!options.accountKey) { - throw new Error('Azure store must have a account key') - } - if (!options.containerName) { - throw new Error('Azure store must have a container name') - } + if (options.client) { + this.containerClient = options.client + } else { + if (!options.account) { + throw new Error('Azure store must have a account') + } + if (!options.accountKey) { + throw new Error('Azure store must have a account key') + } + if (!options.containerName) { + throw new Error('Azure store must have a container name') + } - const storageAccountBaseUrl = `https://${options.account}.blob.core.windows.net` - const sharedKeyCredential = new StorageSharedKeyCredential( - options.account, - options.accountKey - ) + const storageAccountBaseUrl = `https://${options.account}.blob.core.windows.net` + const sharedKeyCredential = new StorageSharedKeyCredential( + options.account, + options.accountKey + ) + const blobServiceClient = new BlobServiceClient( + storageAccountBaseUrl, + sharedKeyCredential + ) + this.containerClient = blobServiceClient.getContainerClient(options.containerName) + } - this.blobServiceClient = new BlobServiceClient( - storageAccountBaseUrl, - sharedKeyCredential - ) - this.containerClient = this.blobServiceClient.getContainerClient( - options.containerName - ) - this.containerName = options.containerName + this.containerName = this.containerClient.containerName } /** diff --git a/packages/azure-store/src/test/index.ts b/packages/azure-store/src/test/index.ts index b2f19026..27137f47 100644 --- a/packages/azure-store/src/test/index.ts +++ b/packages/azure-store/src/test/index.ts @@ -1,5 +1,7 @@ import 'should' +import {strict as assert} from 'node:assert' import path from 'node:path' +import {ContainerClient} from '@azure/storage-blob' import {AzureStore} from '@tus/azure-store' import * as shared from '../../../utils/dist/test/stores.js' @@ -29,4 +31,13 @@ describe('AzureStore', () => { shared.shouldWriteUploads() shared.shouldHandleOffset() shared.shouldDeclareUploadLength() // Creation-defer-length extension + + it('should accept a client', () => { + const client = new ContainerClient( + 'https://testaccount.blob.core.windows.net/testcontainer' + ) + const store = new AzureStore({client}) + + assert.ok(store) + }) })