Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/friendly-wolves-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tus/azure-store": patch
---

Allow consumers to provide a configured Azure `ContainerClient`.
30 changes: 30 additions & 0 deletions packages/azure-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
88 changes: 49 additions & 39 deletions packages/azure-store/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
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,
BlobServiceClient,
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<Upload>
account: string
accountKey: string
containerName: string
}
type Options =
| {
cache?: KvStore<Upload>
client: ContainerClient
account?: never
accountKey?: never
containerName?: never
}
| {
cache?: KvStore<Upload>
client?: never
account: string
accountKey: string
containerName: string
}

const log = debug('tus-node-server:stores:azurestore')

Expand All @@ -32,7 +41,6 @@ const log = debug('tus-node-server:stores:azurestore')
*/
export class AzureStore extends DataStore {
private cache: KvStore<Upload>
private blobServiceClient: BlobServiceClient
private containerClient: ContainerClient
private containerName: string

Expand All @@ -41,30 +49,32 @@ export class AzureStore extends DataStore {
this.cache = options.cache ?? new MemoryKvStore<Upload>()
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
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/azure-store/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
})
})