1- import { BlobServiceClient , ContainerClient } from "@azure/storage-blob" ;
2- import { DefaultAzureCredential } from "@azure/identity" ;
3- import path from "path" ;
41import fs from 'fs-extra' ;
2+ import type { BlobServiceClient , ContainerClient } from "@azure/storage-blob" ;
53
64/**
75 * A service for uploading and downloading files. In local development, this will be a service that uses the local file system. Otherwise, this will be a service that uses Azure Storage blob containers.
@@ -13,33 +11,25 @@ export interface BlobStorage {
1311
1412/**
1513 * Blob storage service that connects to PWABuilder's Azure Storage account for uploading and downloading files.
14+ * NOTE: we lazily initialize the Azure BlobServiceClient and ContainerClient to avoid loading Azure SDK packages too early, which can cause conflicts with OpenTelemetry in our environment. Without this, we'd see errors like, "Module @azure/core-tracing has been loaded before @azure/opentelemetry-instrumentation-azure-sdk so it might not work, please initialize it before requiring @azure/core-tracing'"
1615 */
1716export class AzureStorageBlobService implements BlobStorage {
18- private blobServiceClient : BlobServiceClient ;
19- private containerClient : ContainerClient ;
17+ private blobServiceClientTask : Promise < BlobServiceClient > | null = null ;
18+ private containerClientTask : Promise < ContainerClient > | null = null ;
2019private readonly containerName = "google-play-packages" ;
20+ private readonly azureStorageAccountName :string ;
21+ private readonly azureManagedIdentityAppId :string ;
2122
2223constructor ( ) {
23- const accountName = process . env . AZURE_STORAGE_ACCOUNT_NAME ;
24- if ( ! accountName ) {
24+ this . azureStorageAccountName = process . env . AZURE_STORAGE_ACCOUNT_NAME || "" ;
25+ if ( ! this . azureStorageAccountName ) {
2526throw new Error ( "AZURE_STORAGE_ACCOUNT_NAME environment variable is not set." ) ;
2627}
2728
28- const managedIdentityClientId = process . env . AZURE_MANAGED_IDENTITY_APPLICATION_ID ;
29- if ( ! managedIdentityClientId ) {
29+ this . azureManagedIdentityAppId = process . env . AZURE_MANAGED_IDENTITY_APPLICATION_ID || "" ;
30+ if ( ! this . azureManagedIdentityAppId ) {
3031throw new Error ( "AZURE_MANAGED_IDENTITY_APPLICATION_ID environment variable is not set." ) ;
3132}
32-
33- // Use user-assigned managed identity for authentication
34- const credential = new DefaultAzureCredential ( {
35- managedIdentityClientId :managedIdentityClientId
36- } ) ;
37- this . blobServiceClient = new BlobServiceClient (
38- `https://${ accountName } .blob.core.windows.net` ,
39- credential
40- ) ;
41-
42- this . containerClient = this . blobServiceClient . getContainerClient ( this . containerName ) ;
4333}
4434
4535/**
@@ -50,8 +40,9 @@ export class AzureStorageBlobService implements BlobStorage {
5040 */
5141async uploadFile ( filePath :string , blobName :string ) :Promise < string > {
5242try {
43+ const containerClient = await this . initializeContainerClient ( ) ;
5344const blobSafeFileName = this . getBlobSafeFileName ( blobName ) ;
54- const blockBlobClient = this . containerClient . getBlockBlobClient ( blobSafeFileName ) ;
45+ const blockBlobClient = containerClient . getBlockBlobClient ( blobSafeFileName ) ;
5546
5647// Note: our Azure Blob Storage account is configured to automatically delete these after several hours.
5748console . info ( `Uploading file${ filePath } to blob${ blobSafeFileName } ...` ) ;
@@ -72,7 +63,8 @@ export class AzureStorageBlobService implements BlobStorage {
7263 */
7364async downloadFileStream ( blobName :string ) :Promise < NodeJS . ReadableStream > {
7465try {
75- const blockBlobClient = this . containerClient . getBlockBlobClient ( blobName ) ;
66+ const containerClient = await this . initializeContainerClient ( ) ;
67+ const blockBlobClient = containerClient . getBlockBlobClient ( blobName ) ;
7668
7769console . info ( `Downloading blob${ blobName } as stream...` ) ;
7870const downloadResponse = await blockBlobClient . download ( ) ;
@@ -97,6 +89,41 @@ export class AzureStorageBlobService implements BlobStorage {
9789. replace ( / : / g, '-' )
9890. replace ( / [ ^ a - z A - Z 0 - 9 - _ ] / g, '' ) ;
9991}
92+
93+ private initializeBlobServiceClient ( ) :Promise < BlobServiceClient > {
94+ if ( ! this . blobServiceClientTask ) {
95+ this . blobServiceClientTask = new Promise < BlobServiceClient > ( async ( resolve , reject ) => {
96+ try {
97+ // Dynamic imports to prevent early loading of Azure packages
98+ const { BlobServiceClient} = await import ( "@azure/storage-blob" ) ;
99+ const { DefaultAzureCredential} = await import ( "@azure/identity" ) ;
100+
101+ // Use user-assigned managed identity for authentication
102+ const credential = new DefaultAzureCredential ( {
103+ managedIdentityClientId :this . azureManagedIdentityAppId
104+ } ) ;
105+ const client = new BlobServiceClient (
106+ `https://${ this . azureStorageAccountName } .blob.core.windows.net` ,
107+ credential
108+ ) ;
109+ resolve ( client ) ;
110+ } catch ( error ) {
111+ reject ( error ) ;
112+ }
113+ } ) ;
114+ }
115+
116+ return this . blobServiceClientTask ;
117+ }
118+
119+ private initializeContainerClient ( ) :Promise < ContainerClient > {
120+ if ( ! this . containerClientTask ) {
121+ this . containerClientTask = this . initializeBlobServiceClient ( )
122+ . then ( blobService => blobService . getContainerClient ( this . containerName ) ) ;
123+ }
124+
125+ return this . containerClientTask ;
126+ }
100127}
101128
102129/**