Skip to content

Commit 1c29f08

Browse files
committed
fix(storage): support Azure connection string for presigned URLs
1 parent d26c8f2 commit 1c29f08

File tree

1 file changed

+63
-31
lines changed

1 file changed

+63
-31
lines changed

apps/sim/lib/uploads/providers/blob/client.ts

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,52 @@ const logger = createLogger('BlobClient')
1818

1919
let _blobServiceClient: BlobServiceClientInstance | null = null
2020

21+
interface ParsedCredentials {
22+
accountName: string
23+
accountKey: string
24+
}
25+
26+
/**
27+
* Extract account name and key from an Azure connection string.
28+
* Connection strings have the format: DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=...
29+
*/
30+
function parseConnectionString(connectionString: string): ParsedCredentials {
31+
const accountNameMatch = connectionString.match(/AccountName=([^;]+)/)
32+
if (!accountNameMatch) {
33+
throw new Error('Cannot extract account name from connection string')
34+
}
35+
36+
const accountKeyMatch = connectionString.match(/AccountKey=([^;]+)/)
37+
if (!accountKeyMatch) {
38+
throw new Error('Cannot extract account key from connection string')
39+
}
40+
41+
return {
42+
accountName: accountNameMatch[1],
43+
accountKey: accountKeyMatch[1],
44+
}
45+
}
46+
47+
/**
48+
* Get account credentials from BLOB_CONFIG, extracting from connection string if necessary.
49+
*/
50+
function getAccountCredentials(): ParsedCredentials {
51+
if (BLOB_CONFIG.accountName && BLOB_CONFIG.accountKey) {
52+
return {
53+
accountName: BLOB_CONFIG.accountName,
54+
accountKey: BLOB_CONFIG.accountKey,
55+
}
56+
}
57+
58+
if (BLOB_CONFIG.connectionString) {
59+
return parseConnectionString(BLOB_CONFIG.connectionString)
60+
}
61+
62+
throw new Error(
63+
'Azure Blob Storage credentials are missing – set AZURE_CONNECTION_STRING or both AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY'
64+
)
65+
}
66+
2167
export async function getBlobServiceClient(): Promise<BlobServiceClientInstance> {
2268
if (_blobServiceClient) return _blobServiceClient
2369

@@ -127,6 +173,8 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
127173
const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName)
128174
const blockBlobClient = containerClient.getBlockBlobClient(key)
129175

176+
const { accountName, accountKey } = getAccountCredentials()
177+
130178
const sasOptions = {
131179
containerName: BLOB_CONFIG.containerName,
132180
blobName: key,
@@ -137,13 +185,7 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
137185

138186
const sasToken = generateBlobSASQueryParameters(
139187
sasOptions,
140-
new StorageSharedKeyCredential(
141-
BLOB_CONFIG.accountName,
142-
BLOB_CONFIG.accountKey ??
143-
(() => {
144-
throw new Error('AZURE_ACCOUNT_KEY is required when using account name authentication')
145-
})()
146-
)
188+
new StorageSharedKeyCredential(accountName, accountKey)
147189
).toString()
148190

149191
return `${blockBlobClient.url}?${sasToken}`
@@ -168,9 +210,14 @@ export async function getPresignedUrlWithConfig(
168210
StorageSharedKeyCredential,
169211
} = await import('@azure/storage-blob')
170212
let tempBlobServiceClient: BlobServiceClientInstance
213+
let accountName: string
214+
let accountKey: string
171215

172216
if (customConfig.connectionString) {
173217
tempBlobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
218+
const credentials = parseConnectionString(customConfig.connectionString)
219+
accountName = credentials.accountName
220+
accountKey = credentials.accountKey
174221
} else if (customConfig.accountName && customConfig.accountKey) {
175222
const sharedKeyCredential = new StorageSharedKeyCredential(
176223
customConfig.accountName,
@@ -180,6 +227,8 @@ export async function getPresignedUrlWithConfig(
180227
`https://${customConfig.accountName}.blob.core.windows.net`,
181228
sharedKeyCredential
182229
)
230+
accountName = customConfig.accountName
231+
accountKey = customConfig.accountKey
183232
} else {
184233
throw new Error(
185234
'Custom blob config must include either connectionString or accountName + accountKey'
@@ -199,13 +248,7 @@ export async function getPresignedUrlWithConfig(
199248

200249
const sasToken = generateBlobSASQueryParameters(
201250
sasOptions,
202-
new StorageSharedKeyCredential(
203-
customConfig.accountName,
204-
customConfig.accountKey ??
205-
(() => {
206-
throw new Error('Account key is required when using account name authentication')
207-
})()
208-
)
251+
new StorageSharedKeyCredential(accountName, accountKey)
209252
).toString()
210253

211254
return `${blockBlobClient.url}?${sasToken}`
@@ -403,13 +446,9 @@ export async function getMultipartPartUrls(
403446
if (customConfig) {
404447
if (customConfig.connectionString) {
405448
blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
406-
const match = customConfig.connectionString.match(/AccountName=([^;]+)/)
407-
if (!match) throw new Error('Cannot extract account name from connection string')
408-
accountName = match[1]
409-
410-
const keyMatch = customConfig.connectionString.match(/AccountKey=([^;]+)/)
411-
if (!keyMatch) throw new Error('Cannot extract account key from connection string')
412-
accountKey = keyMatch[1]
449+
const credentials = parseConnectionString(customConfig.connectionString)
450+
accountName = credentials.accountName
451+
accountKey = credentials.accountKey
413452
} else if (customConfig.accountName && customConfig.accountKey) {
414453
const credential = new StorageSharedKeyCredential(
415454
customConfig.accountName,
@@ -428,12 +467,9 @@ export async function getMultipartPartUrls(
428467
} else {
429468
blobServiceClient = await getBlobServiceClient()
430469
containerName = BLOB_CONFIG.containerName
431-
accountName = BLOB_CONFIG.accountName
432-
accountKey =
433-
BLOB_CONFIG.accountKey ||
434-
(() => {
435-
throw new Error('AZURE_ACCOUNT_KEY is required')
436-
})()
470+
const credentials = getAccountCredentials()
471+
accountName = credentials.accountName
472+
accountKey = credentials.accountKey
437473
}
438474

439475
const containerClient = blobServiceClient.getContainerClient(containerName)
@@ -501,12 +537,10 @@ export async function completeMultipartUpload(
501537
const containerClient = blobServiceClient.getContainerClient(containerName)
502538
const blockBlobClient = containerClient.getBlockBlobClient(key)
503539

504-
// Sort parts by part number and extract block IDs
505540
const sortedBlockIds = parts
506541
.sort((a, b) => a.partNumber - b.partNumber)
507542
.map((part) => part.blockId)
508543

509-
// Commit the block list to create the final blob
510544
await blockBlobClient.commitBlockList(sortedBlockIds, {
511545
metadata: {
512546
multipartUpload: 'completed',
@@ -557,10 +591,8 @@ export async function abortMultipartUpload(key: string, customConfig?: BlobConfi
557591
const blockBlobClient = containerClient.getBlockBlobClient(key)
558592

559593
try {
560-
// Delete the blob if it exists (this also cleans up any uncommitted blocks)
561594
await blockBlobClient.deleteIfExists()
562595
} catch (error) {
563-
// Ignore errors since we're just cleaning up
564596
logger.warn('Error cleaning up multipart upload:', error)
565597
}
566598
}

0 commit comments

Comments
 (0)