Skip to content

Commit 6d356b4

Browse files
committed
chore(deps): migrate from azure-storage to @azure/storage-blob and @azure/storage-queue
Replace deprecated azure-storage package with the modern Azure SDK: - @azure/storage-blob for blob storage operations - @azure/storage-queue for queue operations The new SDK uses native Promises, async iterators, and built-in retry policies.
1 parent aae1a0b commit 6d356b4

File tree

10 files changed

+626
-466
lines changed

10 files changed

+626
-466
lines changed

package-lock.json

Lines changed: 316 additions & 290 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"url": "git+https://github.com/clearlydefined/service.git"
2626
},
2727
"dependencies": {
28+
"@azure/storage-blob": "12.30.0",
29+
"@azure/storage-queue": "12.29.0",
2830
"@clearlydefined/spdx": "github:clearlydefined/spdx#v0.1.10",
2931
"@gitbeaker/rest": "^43.8.0",
3032
"@octokit/rest": "^22.0.0",
@@ -33,7 +35,6 @@
3335
"ajv-formats": "3.0.1",
3436
"applicationinsights": "^1.8.10",
3537
"axios": "^1.10.0",
36-
"azure-storage": "^2.10.2",
3738
"base-64": "^1.0.0",
3839
"body-parser": "^2.2.0",
3940
"bottleneck": "^2.15.3",
@@ -75,7 +76,8 @@
7576
"tiny-attribution-generator": "0.1.3",
7677
"tmp": "0.2.5",
7778
"vso-node-api": "6.5.0",
78-
"winston": "^3.17.0"
79+
"winston": "^3.17.0",
80+
"xml2js": "0.6.2"
7981
},
8082
"devDependencies": {
8183
"@tsconfig/node24": "24.0.4",

providers/queueing/azureStorageQueue.js

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright (c) Microsoft Corporation and others. Licensed under the MIT license.
22
// SPDX-License-Identifier: MIT
33

4-
const azure = require('azure-storage')
4+
const { QueueServiceClient } = require('@azure/storage-queue')
55
const logger = require('../logging/logger')
6-
const { promisify } = require('util')
76

87
class AzureStorageQueue {
98
constructor(options) {
@@ -12,10 +11,9 @@ class AzureStorageQueue {
1211
}
1312

1413
async initialize() {
15-
this.queueService = azure
16-
.createQueueService(this.options.connectionString)
17-
.withFilter(new azure.LinearRetryPolicyFilter())
18-
await promisify(this.queueService.createQueueIfNotExists).bind(this.queueService)(this.options.queueName)
14+
this.queueServiceClient = QueueServiceClient.fromConnectionString(this.options.connectionString)
15+
this.queueClient = this.queueServiceClient.getQueueClient(this.options.queueName)
16+
await this.queueClient.createIfNotExists()
1917
}
2018

2119
/**
@@ -25,7 +23,9 @@ class AzureStorageQueue {
2523
* @param {string} message
2624
*/
2725
async queue(message) {
28-
await promisify(this.queueService.createMessage).bind(this.queueService)(this.options.queueName, message)
26+
// The new SDK expects base64 encoded messages by default for compatibility
27+
const encodedMessage = Buffer.from(message).toString('base64')
28+
await this.queueClient.sendMessage(encodedMessage)
2929
}
3030

3131
/**
@@ -37,32 +37,42 @@ class AzureStorageQueue {
3737
* @returns {object} - { original: message, data: "JSON parsed, base64 decoded message" }
3838
*/
3939
async dequeue() {
40-
const message = await promisify(this.queueService.getMessage).bind(this.queueService)(this.options.queueName)
41-
if (!message) return null
42-
if (message.dequeueCount <= 5)
43-
return { original: message, data: JSON.parse(Buffer.from(message.messageText, 'base64').toString('utf8')) }
40+
const response = await this.queueClient.receiveMessages({ numberOfMessages: 1 })
41+
if (!response.receivedMessageItems || response.receivedMessageItems.length === 0) return null
42+
43+
const message = response.receivedMessageItems[0]
44+
if (message.dequeueCount <= 5) {
45+
return {
46+
original: message,
47+
data: JSON.parse(Buffer.from(message.messageText, 'base64').toString('utf8'))
48+
}
49+
}
4450
await this.delete({ original: message })
4551
return this.dequeue()
4652
}
4753

4854
/** Similar to dequeue() but returns multiple messages to improve performance */
4955
async dequeueMultiple() {
50-
const messages = await promisify(this.queueService.getMessages).bind(this.queueService)(
51-
this.options.queueName,
52-
this.options.dequeueOptions
53-
)
54-
if (!messages || messages.length === 0) return []
55-
for (const i in messages) {
56-
if (messages[i].dequeueCount <= 5) {
57-
messages[i] = {
58-
original: messages[i],
59-
data: JSON.parse(Buffer.from(messages[i].messageText, 'base64').toString('utf8'))
60-
}
56+
const options = this.options.dequeueOptions || {}
57+
const response = await this.queueClient.receiveMessages({
58+
numberOfMessages: options.numOfMessages || 32,
59+
visibilityTimeout: options.visibilityTimeout
60+
})
61+
62+
if (!response.receivedMessageItems || response.receivedMessageItems.length === 0) return []
63+
64+
const results = []
65+
for (const message of response.receivedMessageItems) {
66+
if (message.dequeueCount <= 5) {
67+
results.push({
68+
original: message,
69+
data: JSON.parse(Buffer.from(message.messageText, 'base64').toString('utf8'))
70+
})
6171
} else {
62-
await this.delete({ original: messages[i] })
72+
await this.delete({ original: message })
6373
}
6474
}
65-
return messages
75+
return results
6676
}
6777

6878
/**
@@ -72,11 +82,7 @@ class AzureStorageQueue {
7282
* @param {object} message
7383
*/
7484
async delete(message) {
75-
await promisify(this.queueService.deleteMessage).bind(this.queueService)(
76-
this.options.queueName,
77-
message.original.messageId,
78-
message.original.popReceipt
79-
)
85+
await this.queueClient.deleteMessage(message.original.messageId, message.original.popReceipt)
8086
}
8187
}
8288

providers/stores/abstractAzblobStore.js

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
// Copyright (c) Microsoft Corporation and others. Licensed under the MIT license.
22
// SPDX-License-Identifier: MIT
33

4-
const azure = require('azure-storage')
4+
const { BlobServiceClient } = require('@azure/storage-blob')
55
const AbstractFileStore = require('./abstractFileStore')
66
const logger = require('../logging/logger')
77

8-
const { promisify } = require('util')
9-
108
/**
119
* @typedef {import('./abstractAzblobStore').AzBlobStoreOptions} AzBlobStoreOptions
1210
* @typedef {import('./abstractAzblobStore').BlobEntry} BlobEntry
@@ -40,10 +38,9 @@ class AbstractAzBlobStore {
4038
* @returns {Promise<void>} Promise that resolves when initialization is complete
4139
*/
4240
async initialize() {
43-
this.blobService = azure
44-
.createBlobService(this.options.connectionString)
45-
.withFilter(new azure.LinearRetryPolicyFilter())
46-
return promisify(this.blobService.createContainerIfNotExists).bind(this.blobService)(this.containerName)
41+
this.blobServiceClient = BlobServiceClient.fromConnectionString(this.options.connectionString)
42+
this.containerClient = this.blobServiceClient.getContainerClient(this.containerName)
43+
await this.containerClient.createIfNotExists()
4744
}
4845

4946
/**
@@ -57,27 +54,20 @@ class AbstractAzBlobStore {
5754
async list(coordinates, visitor) {
5855
/** @type {any[]} */
5956
const list = []
60-
let continuation = null
61-
do {
62-
const name = AbstractFileStore.toStoragePathFromCoordinates(coordinates)
63-
// @ts-ignore - azure-storage promisify signature differs from standard promisify
64-
const result = await promisify(this.blobService.listBlobsSegmentedWithPrefix).bind(this.blobService)(
65-
this.containerName,
66-
name,
67-
continuation,
68-
// @ts-ignore - azure-storage expects 4 args for this operation
69-
{
70-
include: azure.BlobUtilities.BlobListingDetails.METADATA
71-
}
72-
)
73-
continuation = result.continuationToken
74-
result.entries.forEach(
75-
/** @param {BlobEntry} entry */ entry => {
76-
const visitResult = visitor(entry)
77-
if (visitResult !== null) list.push(visitResult)
78-
}
79-
)
80-
} while (continuation)
57+
const name = AbstractFileStore.toStoragePathFromCoordinates(coordinates)
58+
const listOptions = {
59+
prefix: name,
60+
includeMetadata: true
61+
}
62+
63+
for await (const blob of this.containerClient.listBlobsFlat(listOptions)) {
64+
const entry = {
65+
name: blob.name,
66+
metadata: blob.metadata || {}
67+
}
68+
const visitResult = visitor(entry)
69+
if (visitResult !== null) list.push(visitResult)
70+
}
8171
return list
8272
}
8373

@@ -91,8 +81,10 @@ class AbstractAzBlobStore {
9181
let name = AbstractFileStore.toStoragePathFromCoordinates(coordinates)
9282
if (!name.endsWith('.json')) name += '.json'
9383
try {
94-
const result = await promisify(this.blobService.getBlobToText).bind(this.blobService)(this.containerName, name)
95-
return JSON.parse(result)
84+
const blobClient = this.containerClient.getBlobClient(name)
85+
const downloadResponse = await blobClient.download()
86+
const content = await this._streamToString(downloadResponse.readableStreamBody)
87+
return JSON.parse(content)
9688
} catch (error) {
9789
const azureError = /** @type {{statusCode?: number}} */ (error)
9890
if (azureError.statusCode === 404) return null
@@ -130,6 +122,22 @@ class AbstractAzBlobStore {
130122
_toResultCoordinatesFromStoragePath(path) {
131123
return AbstractFileStore.toResultCoordinatesFromStoragePath(path)
132124
}
125+
126+
/**
127+
* Helper to convert a readable stream to a string
128+
*
129+
* @protected
130+
* @param {NodeJS.ReadableStream} readableStream - The stream to convert
131+
* @returns {Promise<string>} The string content
132+
*/
133+
async _streamToString(readableStream) {
134+
/** @type {Buffer[]} */
135+
const chunks = []
136+
for await (const chunk of readableStream) {
137+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
138+
}
139+
return Buffer.concat(chunks).toString('utf8')
140+
}
133141
}
134142

135143
module.exports = AbstractAzBlobStore

providers/stores/azblobAttachmentStore.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Copyright (c) Microsoft Corporation and others. Licensed under the MIT license.
22
// SPDX-License-Identifier: MIT
33

4-
const azure = require('azure-storage')
5-
const { promisify } = require('util')
4+
const { BlobServiceClient } = require('@azure/storage-blob')
65
const Bottleneck = require('bottleneck').default
76
const limiter = new Bottleneck({ maxConcurrent: 1000 })
87
const logger = require('../logging/logger')
@@ -15,10 +14,9 @@ class AzBlobAttachmentStore {
1514
}
1615

1716
async initialize() {
18-
this.blobService = azure
19-
.createBlobService(this.options.connectionString)
20-
.withFilter(new azure.LinearRetryPolicyFilter())
21-
return promisify(this.blobService.createContainerIfNotExists).bind(this.blobService)(this.containerName)
17+
this.blobServiceClient = BlobServiceClient.fromConnectionString(this.options.connectionString)
18+
this.containerClient = this.blobServiceClient.getContainerClient(this.containerName)
19+
await this.containerClient.createIfNotExists()
2220
}
2321

2422
/**
@@ -32,15 +30,33 @@ class AzBlobAttachmentStore {
3230
try {
3331
const name = 'attachment/' + key + '.json'
3432
this.logger.info('2:1:1:notice_generate:get_single_file:start', { ts: new Date().toISOString(), file: key })
35-
const result = await promisify(this.blobService.getBlobToText).bind(this.blobService)(this.containerName, name)
33+
const blobClient = this.containerClient.getBlobClient(name)
34+
const downloadResponse = await blobClient.download()
35+
const content = await this._streamToString(downloadResponse.readableStreamBody)
3636
this.logger.info('2:1:1:notice_generate:get_single_file:end', { ts: new Date().toISOString(), file: key })
37-
return JSON.parse(result).attachment
37+
return JSON.parse(content).attachment
3838
} catch (error) {
3939
if (error.statusCode === 404) return null
4040
throw error
4141
}
4242
})()
4343
}
44+
45+
/**
46+
* Helper to convert a readable stream to a string
47+
*
48+
* @private
49+
* @param {NodeJS.ReadableStream} readableStream - The stream to convert
50+
* @returns {Promise<string>} The string content
51+
*/
52+
async _streamToString(readableStream) {
53+
/** @type {Buffer[]} */
54+
const chunks = []
55+
for await (const chunk of readableStream) {
56+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
57+
}
58+
return Buffer.concat(chunks).toString('utf8')
59+
}
4460
}
4561

4662
module.exports = options => new AzBlobAttachmentStore(options)

providers/stores/azblobDefinitionStore.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const AbstractAzBlobStore = require('./abstractAzblobStore')
55
const AbstractFileStore = require('./abstractFileStore')
66
const EntityCoordinates = require('../../lib/entityCoordinates')
77
const { sortedUniq } = require('lodash')
8-
const { promisify } = require('util')
98

109
class AzBlobDefinitionStore extends AbstractAzBlobStore {
1110
/**
@@ -24,26 +23,23 @@ class AzBlobDefinitionStore extends AbstractAzBlobStore {
2423
return sortedUniq(list.filter(x => x))
2524
}
2625

27-
store(definition) {
26+
async store(definition) {
2827
const blobName = this._toStoragePathFromCoordinates(definition.coordinates) + '.json'
29-
return promisify(this.blobService.createBlockBlobFromText).bind(this.blobService)(
30-
this.containerName,
31-
blobName,
32-
JSON.stringify(definition),
33-
{
34-
blockIdPrefix: 'block',
35-
contentSettings: { contentType: 'application/json' },
36-
metadata: { id: definition.coordinates.toString() }
37-
}
38-
)
28+
const blockBlobClient = this.containerClient.getBlockBlobClient(blobName)
29+
const content = JSON.stringify(definition)
30+
await blockBlobClient.upload(content, Buffer.byteLength(content), {
31+
blobHTTPHeaders: { blobContentType: 'application/json' },
32+
metadata: { id: definition.coordinates.toString() }
33+
})
3934
}
4035

4136
async delete(coordinates) {
4237
const blobName = this._toStoragePathFromCoordinates(coordinates) + '.json'
38+
const blobClient = this.containerClient.getBlobClient(blobName)
4339
try {
44-
await promisify(this.blobService.deleteBlob).bind(this.blobService)(this.containerName, blobName)
40+
await blobClient.delete()
4541
} catch (error) {
46-
if (error.code !== 'BlobNotFound') throw error
42+
if (error.statusCode !== 404) throw error
4743
}
4844
}
4945
}

0 commit comments

Comments
 (0)