1- import fs from "node:fs" ;
2- import path from "node:path" ;
31import { Readable } from "node:stream" ;
42import type streamWeb from "node:stream/web" ;
5- import { BlobServiceClient , type BlobClient } from "@azure/storage-blob" ;
3+ import {
4+ BlobServiceClient ,
5+ type BlobClient ,
6+ type BlockBlobClient ,
7+ } from "@azure/storage-blob" ;
68import type { StorageService } from "@storybooker/core/types" ;
79
810export class AzureBlobStorageService implements StorageService {
@@ -12,32 +14,63 @@ export class AzureBlobStorageService implements StorageService {
1214 this . #client = BlobServiceClient . fromConnectionString ( connectionString ) ;
1315 }
1416
15- createContainer : StorageService [ "createContainer" ] = async ( name ) => {
16- await this . #client. createContainer ( name ) ;
17+ createContainer : StorageService [ "createContainer" ] = async (
18+ containerId ,
19+ options ,
20+ ) => {
21+ await this . #client. createContainer ( containerId , {
22+ abortSignal : options . abortSignal ,
23+ } ) ;
24+ } ;
25+
26+ deleteContainer : StorageService [ "deleteContainer" ] = async (
27+ containerId ,
28+ options ,
29+ ) => {
30+ await this . #client. getContainerClient ( containerId ) . deleteIfExists ( {
31+ abortSignal : options . abortSignal ,
32+ } ) ;
1733 } ;
1834
19- deleteContainer : StorageService [ "deleteContainer" ] = async ( name ) => {
20- await this . #client. getContainerClient ( name ) . deleteIfExists ( ) ;
35+ hasContainer : StorageService [ "hasContainer" ] = async (
36+ containerId ,
37+ options ,
38+ ) => {
39+ return await this . #client. getContainerClient ( containerId ) . exists ( {
40+ abortSignal : options . abortSignal ,
41+ } ) ;
2142 } ;
2243
23- listContainers : StorageService [ "listContainers" ] = async ( ) => {
44+ listContainers : StorageService [ "listContainers" ] = async ( options ) => {
2445 const containers : string [ ] = [ ] ;
25- for await ( const item of this . #client. listContainers ( ) ) {
46+ for await ( const item of this . #client. listContainers ( {
47+ abortSignal : options . abortSignal ,
48+ } ) ) {
2649 containers . push ( item . name ) ;
2750 }
2851
2952 return containers ;
3053 } ;
3154
32- deleteFile : StorageService [ "deleteFile " ] = async ( name , path ) => {
33- await this . #client . getContainerClient ( name ) . deleteBlob ( path ) ;
34- } ;
35-
36- deleteFiles : StorageService [ "deleteFiles" ] = async ( name , prefix ) => {
37- const containerClient = this . #client. getContainerClient ( name ) ;
55+ deleteFiles : StorageService [ "deleteFiles " ] = async (
56+ containerId ,
57+ filePathsOrPrefix ,
58+ options ,
59+ ) => {
60+ const containerClient = this . #client. getContainerClient ( containerId ) ;
3861 const blobClientsToDelete : BlobClient [ ] = [ ] ;
39- for await ( const blob of containerClient . listBlobsFlat ( { prefix } ) ) {
40- blobClientsToDelete . push ( containerClient . getBlobClient ( blob . name ) ) ;
62+
63+ if ( typeof filePathsOrPrefix === "string" ) {
64+ for await ( const blob of containerClient . listBlobsFlat ( {
65+ abortSignal : options . abortSignal ,
66+ prefix : filePathsOrPrefix ,
67+ } ) ) {
68+ blobClientsToDelete . push ( containerClient . getBlobClient ( blob . name ) ) ;
69+ }
70+ } else {
71+ for ( const filepath of filePathsOrPrefix ) {
72+ blobClientsToDelete . push ( containerClient . getBlobClient ( filepath ) ) ;
73+ }
4174 }
4275
4376 if ( blobClientsToDelete . length === 0 ) {
@@ -46,138 +79,109 @@ export class AzureBlobStorageService implements StorageService {
4679
4780 const response = await containerClient
4881 . getBlobBatchClient ( )
49- . deleteBlobs ( blobClientsToDelete ) ;
82+ . deleteBlobs ( blobClientsToDelete , {
83+ abortSignal : options . abortSignal ,
84+ } ) ;
5085
5186 if ( response . errorCode ) {
5287 throw new Error ( `Failed to delete blobs: ${ response . errorCode } ` ) ;
5388 }
5489 return ;
5590 } ;
5691
57- uploadFile : StorageService [ "uploadFile " ] = async (
58- containerName ,
59- file ,
92+ uploadFiles : StorageService [ "uploadFiles " ] = async (
93+ containerId ,
94+ files ,
6095 options ,
6196 ) => {
62- const { destinationPath, mimeType = "application/octet-stream" } = options ;
63- const client = this . #client
64- . getContainerClient ( containerName )
65- . getBlockBlobClient ( destinationPath ) ;
97+ const containerClient = this . #client. getContainerClient ( containerId ) ;
98+ // oxlint-disable-next-line require-await
99+ const promises = files . map ( async ( { content, path, mimeType } ) =>
100+ this . #uploadFile(
101+ containerClient . getBlockBlobClient ( path ) ,
102+ content ,
103+ mimeType ,
104+ options . abortSignal ,
105+ ) ,
106+ ) ;
107+
108+ await Promise . allSettled ( promises ) ;
109+ } ;
66110
67- if ( typeof file === "string" ) {
68- await client . uploadFile ( file , {
111+ // oxlint-disable-next-line max-params
112+ #uploadFile = async (
113+ client : BlockBlobClient ,
114+ data : Blob | string | ReadableStream ,
115+ mimeType : string ,
116+ abortSignal ?: AbortSignal ,
117+ ) : Promise < void > => {
118+ if ( typeof data === "string" ) {
119+ const blob = new Blob ( [ data ] , { type : mimeType } ) ;
120+ await client . uploadData ( blob , {
121+ abortSignal,
69122 blobHTTPHeaders : { blobContentType : mimeType } ,
70123 } ) ;
71124 return ;
72125 }
73- if ( file instanceof Blob ) {
74- await client . uploadData ( file , {
126+ if ( data instanceof Blob ) {
127+ await client . uploadData ( data , {
128+ abortSignal,
75129 blobHTTPHeaders : { blobContentType : mimeType } ,
76130 } ) ;
77131 return ;
78132 }
79- if ( file instanceof ReadableStream ) {
80- const stream = file as unknown as streamWeb . ReadableStream ;
133+ if ( data instanceof ReadableStream ) {
134+ const stream = data as unknown as streamWeb . ReadableStream ;
81135 await client . uploadStream (
82136 Readable . fromWeb ( stream ) ,
83137 undefined ,
84138 undefined ,
85- { blobHTTPHeaders : { blobContentType : mimeType } } ,
139+ { abortSignal , blobHTTPHeaders : { blobContentType : mimeType } } ,
86140 ) ;
87141 return ;
88142 }
89143
90144 throw new Error ( `Unknown file type` ) ;
91145 } ;
92146
93- uploadDir : StorageService [ "uploadDir " ] = async (
94- containerName ,
95- dirpath ,
96- destPrefix ,
147+ hasFile : StorageService [ "hasFile " ] = async (
148+ containerId ,
149+ filepath ,
150+ options ,
97151 ) => {
98- const containerClient = this . #client. getContainerClient ( containerName ) ;
99-
100- const files = fs
101- . readdirSync ( dirpath , {
102- recursive : true ,
103- withFileTypes : true ,
104- } )
105- . filter ( ( file ) => file . isFile ( ) && ! file . name . startsWith ( "." ) )
106- . map ( ( file ) => path . join ( file . parentPath , file . name ) ) ;
107-
108- // context.info(`Found ${files.length} files in dir to upload: ${dirpath}.`);
109- const uploadErrors = new Map < string , unknown > ( ) ;
110-
111- for ( const filepath of files ) {
112- if ( ! fs . existsSync ( filepath ) ) {
113- // context.warn(`File ${filepath} does not exist, skipping.`);
114- continue ;
115- }
116-
117- let blobName = filepath . replace ( `${ dirpath } /` , "" ) ;
118- if ( destPrefix ) {
119- blobName = path . posix . join ( destPrefix , blobName ) ;
120- }
121-
122- try {
123- // context.debug(`Uploading '${filepath}' to '${newFilepath}'...`);
124- // oxlint-disable-next-line no-await-in-loop
125- const response = await containerClient
126- . getBlockBlobClient ( blobName )
127- . uploadFile ( filepath ) ;
128-
129- if ( response . errorCode ) {
130- throw response . errorCode ;
131- }
132- } catch ( error ) {
133- // context.error(
134- // `Failed to upload blob '${blobName}'. Error: ${errorMessage}`,
135- // );
136- uploadErrors . set ( blobName , error ) ;
137- }
138- }
139-
140- if ( uploadErrors . size > 0 ) {
141- throw new Error (
142- `Failed to upload ${ uploadErrors . size } files to container: ${ containerClient . containerName } .` ,
143- ) ;
144- }
145-
146- return ;
152+ const containerClient = this . #client. getContainerClient ( containerId ) ;
153+ const blockBlobClient = containerClient . getBlockBlobClient ( filepath ) ;
154+ return await blockBlobClient . exists ( { abortSignal : options . abortSignal } ) ;
147155 } ;
148156
149- downloadFile = async (
150- containerName : string ,
151- filepath : string ,
152- ) : Promise < ReadableStream > => {
153- const containerClient = this . #client. getContainerClient ( containerName ) ;
157+ downloadFile : StorageService [ "downloadFile" ] = async (
158+ containerId ,
159+ filepath ,
160+ options ,
161+ ) => {
162+ const containerClient = this . #client. getContainerClient ( containerId ) ;
154163 const blockBlobClient = containerClient . getBlockBlobClient ( filepath ) ;
155164
156165 if ( ! ( await blockBlobClient . exists ( ) ) ) {
157166 throw new Error (
158- `File '${ filepath } ' not found in container '${ containerName } '.` ,
167+ `File '${ filepath } ' not found in container '${ containerId } '.` ,
159168 ) ;
160169 }
161170
162- const downloadResponse = await blockBlobClient . download ( 0 ) ;
171+ const downloadResponse = await blockBlobClient . download ( 0 , undefined , {
172+ abortSignal : options . abortSignal ,
173+ } ) ;
163174
164175 if ( ! downloadResponse . readableStreamBody ) {
165176 throw new Error (
166- `File '${ filepath } ' in container '${ containerName } ' is not downloadable.` ,
177+ `File '${ filepath } ' in container '${ containerId } ' is not downloadable.` ,
167178 ) ;
168179 }
169180
170- const headers = new Headers ( ) ;
171- if ( downloadResponse . contentType ) {
172- headers . set ( "Content-Type" , downloadResponse . contentType ) ;
173- }
174- if ( downloadResponse . contentLength ) {
175- headers . set ( "Content-Length" , downloadResponse . contentLength . toString ( ) ) ;
176- }
177- if ( downloadResponse . contentEncoding ) {
178- headers . set ( "Content-Encoding" , downloadResponse . contentEncoding ) ;
179- }
180-
181- return downloadResponse . readableStreamBody as unknown as ReadableStream ;
181+ return {
182+ content : downloadResponse . readableStreamBody as unknown as ReadableStream ,
183+ mimeType : downloadResponse . contentType ,
184+ path : filepath ,
185+ } ;
182186 } ;
183187}
0 commit comments