1
- import { PassThrough , Readable } from 'stream' ;
2
- import { Upload } from '@aws-sdk/lib-storage' ;
1
+ import { Readable } from 'stream' ;
3
2
import {
4
3
S3Client ,
5
4
PutObjectCommand ,
6
- PutObjectCommandOutput ,
7
5
GetObjectCommand ,
8
6
DeleteObjectCommand ,
9
7
ListBucketsCommand ,
10
8
} from '@aws-sdk/client-s3' ;
11
9
import { getSignedUrl } from '@aws-sdk/s3-request-presigner' ;
12
10
import { FileNotExistError , Defaults } from '@impler/shared' ;
13
11
12
+ // Azure Storage imports
13
+ import {
14
+ BlobSASPermissions ,
15
+ BlobServiceClient ,
16
+ BlockBlobUploadResponse ,
17
+ ContainerSASPermissions ,
18
+ } from '@azure/storage-blob' ;
19
+
14
20
export interface IFilePath {
15
21
path : string ;
16
22
name : string ;
17
23
}
18
24
25
+ export interface StorageResponse {
26
+ success : boolean ;
27
+ metadata ?: any ;
28
+ }
29
+
19
30
export abstract class StorageService {
20
- abstract uploadFile (
21
- key : string ,
22
- file : Buffer | string | PassThrough ,
23
- contentType : string
24
- ) : Promise < PutObjectCommandOutput > ;
31
+ abstract uploadFile ( key : string , file : Buffer | string | Readable , contentType : string ) : Promise < StorageResponse > ;
25
32
abstract getFileContent ( key : string , encoding ?: BufferEncoding ) : Promise < string > ;
26
33
abstract getFileStream ( key : string ) : Promise < Readable > ;
27
- abstract writeStream ( key : string , stream : Readable , contentType : string ) : Upload ;
34
+ abstract writeStream ( key : string , stream : Readable , contentType : string ) : Promise < void > ;
28
35
abstract deleteFile ( key : string ) : Promise < void > ;
29
36
abstract isConnected ( ) : boolean ;
30
37
abstract getSignedUrl ( key : string ) : Promise < string > ;
@@ -63,15 +70,22 @@ export class S3StorageService implements StorageService {
63
70
} ) ;
64
71
}
65
72
66
- async uploadFile ( key : string , file : Buffer , contentType : string ) : Promise < PutObjectCommandOutput > {
73
+ async uploadFile ( key : string , file : Buffer | string | Readable , contentType : string ) : Promise < StorageResponse > {
67
74
const command = new PutObjectCommand ( {
68
75
Bucket : process . env . S3_BUCKET_NAME ,
69
76
Key : key ,
70
77
Body : file ,
71
78
ContentType : contentType ,
72
79
} ) ;
73
80
74
- return await this . s3 . send ( command ) ;
81
+ const result = await this . s3 . send ( command ) ;
82
+ return {
83
+ success : true ,
84
+ metadata : {
85
+ eTag : result . ETag ,
86
+ versionId : result . VersionId ,
87
+ } ,
88
+ } ;
75
89
}
76
90
77
91
async getFileContent ( key : string , encoding = 'utf8' as BufferEncoding ) : Promise < string > {
@@ -81,7 +95,6 @@ export class S3StorageService implements StorageService {
81
95
Key : key ,
82
96
} ) ;
83
97
const data = await this . s3 . send ( command ) ;
84
-
85
98
return await streamToString ( data . Body as Readable , encoding ) ;
86
99
} catch ( error ) {
87
100
if ( error . code === Defaults . NOT_FOUND_STATUS_CODE || error . message === 'The specified key does not exist.' ) {
@@ -98,7 +111,6 @@ export class S3StorageService implements StorageService {
98
111
Key : key ,
99
112
} ) ;
100
113
const data = await this . s3 . send ( command ) ;
101
-
102
114
return data . Body as Readable ;
103
115
} catch ( error ) {
104
116
if ( error . code === Defaults . NOT_FOUND_STATUS_CODE || error . message === 'The specified key does not exist.' ) {
@@ -108,17 +120,14 @@ export class S3StorageService implements StorageService {
108
120
}
109
121
}
110
122
111
- writeStream ( key : string , stream : Readable | PassThrough , contentType : string ) : Upload {
112
- return new Upload ( {
113
- client : this . s3 ,
114
- queueSize : 4 ,
115
- params : {
116
- Bucket : process . env . S3_BUCKET_NAME ,
117
- Key : key ,
118
- Body : stream ,
119
- ContentType : contentType ,
120
- } ,
123
+ async writeStream ( key : string , stream : Readable , contentType : string ) : Promise < void > {
124
+ const command = new PutObjectCommand ( {
125
+ Bucket : process . env . S3_BUCKET_NAME ,
126
+ Key : key ,
127
+ Body : stream ,
128
+ ContentType : contentType ,
121
129
} ) ;
130
+ await this . s3 . send ( command ) ;
122
131
}
123
132
124
133
async deleteFile ( key : string ) : Promise < void > {
@@ -134,16 +143,126 @@ export class S3StorageService implements StorageService {
134
143
}
135
144
136
145
async getSignedUrl ( key : string ) : Promise < string > {
137
- return await getSignedUrl (
138
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
139
- // @ts -ignore
140
- this . s3 ,
141
- new GetObjectCommand ( {
142
- Bucket : process . env . S3_BUCKET_NAME ,
143
- Key : key ,
144
- } ) ,
145
- // eslint-disable-next-line no-magic-numbers
146
- { expiresIn : 15 * 60 } // 15 minutes
146
+ const command = new GetObjectCommand ( {
147
+ Bucket : process . env . S3_BUCKET_NAME ,
148
+ Key : key ,
149
+ } ) ;
150
+ return getSignedUrl ( this . s3 , command , { expiresIn : 3600 } ) ;
151
+ }
152
+ }
153
+
154
+ export class AzureStorageService implements StorageService {
155
+ private isAzureConnected = false ;
156
+ private blobServiceClient : BlobServiceClient ;
157
+ private containerClient : any ;
158
+
159
+ constructor ( ) {
160
+ if ( ! process . env . AZURE_STORAGE_CONNECTION_STRING ) {
161
+ throw new Error ( 'AZURE_STORAGE_CONNECTION_STRING is not configured' ) ;
162
+ }
163
+
164
+ this . blobServiceClient = BlobServiceClient . fromConnectionString ( process . env . AZURE_STORAGE_CONNECTION_STRING ) ;
165
+ this . containerClient = this . blobServiceClient . getContainerClient (
166
+ process . env . AZURE_STORAGE_CONTAINER || 'default-container'
147
167
) ;
168
+
169
+ // Verify connection
170
+ this . blobServiceClient
171
+ . listContainers ( )
172
+ . next ( )
173
+ . then ( ( ) => {
174
+ this . isAzureConnected = true ;
175
+ } )
176
+ . catch ( ( ) => {
177
+ this . isAzureConnected = false ;
178
+ } ) ;
179
+ }
180
+
181
+ async uploadFile ( key : string , file : Buffer | string | Readable , contentType : string ) : Promise < StorageResponse > {
182
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
183
+ let uploadResponse : BlockBlobUploadResponse ;
184
+
185
+ if ( typeof file === 'string' ) {
186
+ const buffer = Buffer . from ( file ) ;
187
+ uploadResponse = await blockBlobClient . upload ( buffer , buffer . length ) ;
188
+ } else if ( file instanceof Buffer ) {
189
+ uploadResponse = await blockBlobClient . upload ( file , file . length ) ;
190
+ } else {
191
+ // For Readable streams
192
+ const chunks : Buffer [ ] = [ ] ;
193
+ for await ( const chunk of file ) {
194
+ chunks . push ( chunk ) ;
195
+ }
196
+ const buffer = Buffer . concat ( chunks ) ;
197
+ uploadResponse = await blockBlobClient . upload ( buffer , buffer . length ) ;
198
+ }
199
+
200
+ await blockBlobClient . setHTTPHeaders ( { blobContentType : contentType } ) ;
201
+ return {
202
+ success : true ,
203
+ metadata : {
204
+ eTag : uploadResponse . etag ,
205
+ } ,
206
+ } ;
207
+ }
208
+
209
+ async getFileContent ( key : string , encoding = 'utf8' as BufferEncoding ) : Promise < string > {
210
+ try {
211
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
212
+ const downloadResponse = await blockBlobClient . download ( ) ;
213
+ const content = await downloadResponse . blobBody ?. toString ( encoding ) ;
214
+ if ( ! content ) {
215
+ throw new Error ( 'Failed to download file content' ) ;
216
+ }
217
+ return content ;
218
+ } catch ( error ) {
219
+ if ( error . name === 'RestError' && error . statusCode === 404 ) {
220
+ throw new FileNotExistError ( key ) ;
221
+ }
222
+ throw error ;
223
+ }
224
+ }
225
+
226
+ async getFileStream ( key : string ) : Promise < Readable > {
227
+ try {
228
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
229
+ const downloadResponse = await blockBlobClient . download ( ) ;
230
+ return Readable . from ( downloadResponse . blobBody as any ) ;
231
+ } catch ( error ) {
232
+ if ( error . name === 'RestError' && error . statusCode === 404 ) {
233
+ throw new FileNotExistError ( key ) ;
234
+ }
235
+ throw error ;
236
+ }
237
+ }
238
+
239
+ async writeStream ( key : string , stream : Readable , contentType : string ) : Promise < void > {
240
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
241
+ const chunks : Buffer [ ] = [ ] ;
242
+ for await ( const chunk of stream ) {
243
+ chunks . push ( chunk ) ;
244
+ }
245
+ const buffer = Buffer . concat ( chunks ) ;
246
+ await blockBlobClient . upload ( buffer , buffer . length ) ;
247
+ await blockBlobClient . setHTTPHeaders ( { blobContentType : contentType } ) ;
248
+ }
249
+
250
+ async deleteFile ( key : string ) : Promise < void > {
251
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
252
+ await blockBlobClient . delete ( ) ;
253
+ }
254
+
255
+ isConnected ( ) : boolean {
256
+ return this . isAzureConnected ;
257
+ }
258
+
259
+ async getSignedUrl ( key : string ) : Promise < string > {
260
+ const blockBlobClient = this . containerClient . getBlockBlobClient ( key ) ;
261
+ const permissions = BlobSASPermissions . parse ( 'r' ) ;
262
+
263
+ return blockBlobClient . generateSasUrl ( {
264
+ permissions,
265
+ expiresOn : new Date ( Date . now ( ) + 3600 * 1000 ) , // 1 hour expiry
266
+ } ) ;
148
267
}
149
268
}
0 commit comments