express-storage 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +366 -34
  2. package/dist/cjs/config/index.d.ts +10 -0
  3. package/dist/cjs/config/index.d.ts.map +1 -0
  4. package/dist/cjs/config/index.js +19 -0
  5. package/dist/cjs/config/index.js.map +1 -0
  6. package/dist/cjs/drivers/azure.driver.d.ts +73 -0
  7. package/dist/cjs/drivers/azure.driver.d.ts.map +1 -0
  8. package/dist/cjs/drivers/azure.driver.js +390 -0
  9. package/dist/cjs/drivers/azure.driver.js.map +1 -0
  10. package/dist/cjs/drivers/base.driver.d.ts +136 -0
  11. package/dist/cjs/drivers/base.driver.d.ts.map +1 -0
  12. package/dist/cjs/drivers/base.driver.js +357 -0
  13. package/dist/cjs/drivers/base.driver.js.map +1 -0
  14. package/dist/{drivers → cjs/drivers}/gcs.driver.d.ts +20 -38
  15. package/dist/cjs/drivers/gcs.driver.d.ts.map +1 -0
  16. package/dist/cjs/drivers/gcs.driver.js +343 -0
  17. package/dist/cjs/drivers/gcs.driver.js.map +1 -0
  18. package/dist/cjs/drivers/index.d.ts +15 -0
  19. package/dist/cjs/drivers/index.d.ts.map +1 -0
  20. package/dist/cjs/drivers/index.js +26 -0
  21. package/dist/cjs/drivers/index.js.map +1 -0
  22. package/dist/cjs/drivers/local.driver.d.ts +86 -0
  23. package/dist/cjs/drivers/local.driver.d.ts.map +1 -0
  24. package/dist/cjs/drivers/local.driver.js +556 -0
  25. package/dist/cjs/drivers/local.driver.js.map +1 -0
  26. package/dist/{drivers → cjs/drivers}/s3.driver.d.ts +19 -39
  27. package/dist/cjs/drivers/s3.driver.d.ts.map +1 -0
  28. package/dist/cjs/drivers/s3.driver.js +400 -0
  29. package/dist/cjs/drivers/s3.driver.js.map +1 -0
  30. package/dist/cjs/factory/driver.factory.d.ts +43 -0
  31. package/dist/cjs/factory/driver.factory.d.ts.map +1 -0
  32. package/dist/cjs/factory/driver.factory.js +101 -0
  33. package/dist/cjs/factory/driver.factory.js.map +1 -0
  34. package/dist/cjs/index.d.ts +26 -0
  35. package/dist/cjs/index.d.ts.map +1 -0
  36. package/dist/cjs/index.js +31 -0
  37. package/dist/cjs/index.js.map +1 -0
  38. package/dist/cjs/package.json +1 -0
  39. package/dist/cjs/storage-manager.d.ts +210 -0
  40. package/dist/cjs/storage-manager.d.ts.map +1 -0
  41. package/dist/cjs/storage-manager.js +649 -0
  42. package/dist/cjs/storage-manager.js.map +1 -0
  43. package/dist/cjs/types/storage.types.d.ts +438 -0
  44. package/dist/cjs/types/storage.types.d.ts.map +1 -0
  45. package/dist/cjs/types/storage.types.js +3 -0
  46. package/dist/cjs/types/storage.types.js.map +1 -0
  47. package/dist/cjs/utils/config.utils.d.ts.map +1 -0
  48. package/dist/cjs/utils/config.utils.js +213 -0
  49. package/dist/cjs/utils/config.utils.js.map +1 -0
  50. package/dist/{utils → cjs/utils}/file.utils.d.ts +62 -8
  51. package/dist/cjs/utils/file.utils.d.ts.map +1 -0
  52. package/dist/cjs/utils/file.utils.js +464 -0
  53. package/dist/cjs/utils/file.utils.js.map +1 -0
  54. package/dist/cjs/utils/index.d.ts +12 -0
  55. package/dist/cjs/utils/index.d.ts.map +1 -0
  56. package/dist/cjs/utils/index.js +36 -0
  57. package/dist/cjs/utils/index.js.map +1 -0
  58. package/dist/cjs/utils/rate-limiter.d.ts +40 -0
  59. package/dist/cjs/utils/rate-limiter.d.ts.map +1 -0
  60. package/dist/cjs/utils/rate-limiter.js +87 -0
  61. package/dist/cjs/utils/rate-limiter.js.map +1 -0
  62. package/dist/esm/config/index.d.ts +10 -0
  63. package/dist/esm/config/index.d.ts.map +1 -0
  64. package/dist/esm/config/index.js +10 -0
  65. package/dist/esm/config/index.js.map +1 -0
  66. package/dist/esm/drivers/azure.driver.d.ts +73 -0
  67. package/dist/esm/drivers/azure.driver.d.ts.map +1 -0
  68. package/dist/esm/drivers/azure.driver.js +353 -0
  69. package/dist/esm/drivers/azure.driver.js.map +1 -0
  70. package/dist/esm/drivers/base.driver.d.ts +136 -0
  71. package/dist/esm/drivers/base.driver.d.ts.map +1 -0
  72. package/dist/esm/drivers/base.driver.js +350 -0
  73. package/dist/esm/drivers/base.driver.js.map +1 -0
  74. package/dist/esm/drivers/gcs.driver.d.ts +68 -0
  75. package/dist/esm/drivers/gcs.driver.d.ts.map +1 -0
  76. package/dist/esm/drivers/gcs.driver.js +306 -0
  77. package/dist/esm/drivers/gcs.driver.js.map +1 -0
  78. package/dist/esm/drivers/index.d.ts +15 -0
  79. package/dist/esm/drivers/index.d.ts.map +1 -0
  80. package/dist/esm/drivers/index.js +15 -0
  81. package/dist/esm/drivers/index.js.map +1 -0
  82. package/dist/esm/drivers/local.driver.d.ts +86 -0
  83. package/dist/esm/drivers/local.driver.d.ts.map +1 -0
  84. package/dist/esm/drivers/local.driver.js +549 -0
  85. package/dist/esm/drivers/local.driver.js.map +1 -0
  86. package/dist/esm/drivers/s3.driver.d.ts +69 -0
  87. package/dist/esm/drivers/s3.driver.d.ts.map +1 -0
  88. package/dist/esm/drivers/s3.driver.js +363 -0
  89. package/dist/esm/drivers/s3.driver.js.map +1 -0
  90. package/dist/esm/factory/driver.factory.d.ts +43 -0
  91. package/dist/esm/factory/driver.factory.d.ts.map +1 -0
  92. package/dist/esm/factory/driver.factory.js +92 -0
  93. package/dist/esm/factory/driver.factory.js.map +1 -0
  94. package/dist/esm/index.d.ts +26 -0
  95. package/dist/esm/index.d.ts.map +1 -0
  96. package/dist/esm/index.js +26 -0
  97. package/dist/esm/index.js.map +1 -0
  98. package/dist/esm/package.json +1 -0
  99. package/dist/esm/storage-manager.d.ts +210 -0
  100. package/dist/esm/storage-manager.d.ts.map +1 -0
  101. package/dist/esm/storage-manager.js +645 -0
  102. package/dist/esm/storage-manager.js.map +1 -0
  103. package/dist/esm/types/storage.types.d.ts +438 -0
  104. package/dist/esm/types/storage.types.d.ts.map +1 -0
  105. package/dist/esm/types/storage.types.js.map +1 -0
  106. package/dist/esm/utils/config.utils.d.ts +45 -0
  107. package/dist/esm/utils/config.utils.d.ts.map +1 -0
  108. package/dist/esm/utils/config.utils.js.map +1 -0
  109. package/dist/esm/utils/file.utils.d.ts +196 -0
  110. package/dist/esm/utils/file.utils.d.ts.map +1 -0
  111. package/dist/esm/utils/file.utils.js +439 -0
  112. package/dist/esm/utils/file.utils.js.map +1 -0
  113. package/dist/esm/utils/index.d.ts +12 -0
  114. package/dist/esm/utils/index.d.ts.map +1 -0
  115. package/dist/esm/utils/index.js +11 -0
  116. package/dist/esm/utils/index.js.map +1 -0
  117. package/dist/esm/utils/rate-limiter.d.ts +40 -0
  118. package/dist/esm/utils/rate-limiter.d.ts.map +1 -0
  119. package/dist/esm/utils/rate-limiter.js +82 -0
  120. package/dist/esm/utils/rate-limiter.js.map +1 -0
  121. package/package.json +90 -52
  122. package/src/config/index.ts +17 -0
  123. package/src/drivers/azure.driver.ts +434 -0
  124. package/src/drivers/base.driver.ts +436 -0
  125. package/src/drivers/gcs.driver.ts +366 -0
  126. package/src/drivers/index.ts +15 -0
  127. package/src/drivers/local.driver.ts +626 -0
  128. package/src/drivers/s3.driver.ts +459 -0
  129. package/src/factory/driver.factory.ts +101 -0
  130. package/src/index.ts +72 -0
  131. package/src/storage-manager.ts +801 -0
  132. package/src/types/storage.types.ts +561 -0
  133. package/src/utils/config.utils.ts +229 -0
  134. package/src/utils/file.utils.ts +536 -0
  135. package/src/utils/index.ts +35 -0
  136. package/src/utils/rate-limiter.ts +94 -0
  137. package/dist/drivers/azure.driver.d.ts +0 -88
  138. package/dist/drivers/azure.driver.d.ts.map +0 -1
  139. package/dist/drivers/azure.driver.js +0 -391
  140. package/dist/drivers/azure.driver.js.map +0 -1
  141. package/dist/drivers/base.driver.d.ts +0 -170
  142. package/dist/drivers/base.driver.d.ts.map +0 -1
  143. package/dist/drivers/base.driver.js +0 -347
  144. package/dist/drivers/base.driver.js.map +0 -1
  145. package/dist/drivers/gcs.driver.d.ts.map +0 -1
  146. package/dist/drivers/gcs.driver.js +0 -354
  147. package/dist/drivers/gcs.driver.js.map +0 -1
  148. package/dist/drivers/local.driver.d.ts +0 -107
  149. package/dist/drivers/local.driver.d.ts.map +0 -1
  150. package/dist/drivers/local.driver.js +0 -621
  151. package/dist/drivers/local.driver.js.map +0 -1
  152. package/dist/drivers/s3.driver.d.ts.map +0 -1
  153. package/dist/drivers/s3.driver.js +0 -387
  154. package/dist/drivers/s3.driver.js.map +0 -1
  155. package/dist/factory/driver.factory.d.ts +0 -62
  156. package/dist/factory/driver.factory.d.ts.map +0 -1
  157. package/dist/factory/driver.factory.js +0 -177
  158. package/dist/factory/driver.factory.js.map +0 -1
  159. package/dist/index.d.ts +0 -30
  160. package/dist/index.d.ts.map +0 -1
  161. package/dist/index.js +0 -33
  162. package/dist/index.js.map +0 -1
  163. package/dist/storage-manager.d.ts +0 -228
  164. package/dist/storage-manager.d.ts.map +0 -1
  165. package/dist/storage-manager.js +0 -715
  166. package/dist/storage-manager.js.map +0 -1
  167. package/dist/types/storage.types.d.ts +0 -295
  168. package/dist/types/storage.types.d.ts.map +0 -1
  169. package/dist/types/storage.types.js.map +0 -1
  170. package/dist/utils/config.utils.d.ts.map +0 -1
  171. package/dist/utils/config.utils.js.map +0 -1
  172. package/dist/utils/file.utils.d.ts.map +0 -1
  173. package/dist/utils/file.utils.js +0 -278
  174. package/dist/utils/file.utils.js.map +0 -1
  175. /package/dist/{utils → cjs/utils}/config.utils.d.ts +0 -0
  176. /package/dist/{types → esm/types}/storage.types.js +0 -0
  177. /package/dist/{utils → esm/utils}/config.utils.js +0 -0
@@ -0,0 +1,434 @@
1
+ import type {
2
+ BlobServiceClient as BlobServiceClientType,
3
+ ContainerClient as ContainerClientType,
4
+ } from '@azure/storage-blob';
5
+ import { BaseStorageDriver } from './base.driver.js';
6
+ import { FileUploadResult, PresignedUrlResult, StorageConfig, BlobValidationOptions, BlobValidationResult, ListFilesResult, UploadOptions, FileInfo, DeleteResult } from '../types/storage.types.js';
7
+ import { encodePathSegments } from '../utils/file.utils.js';
8
+
9
+ // Lazy SDK loaders — modules are imported on first use, not at import time.
10
+
11
+ let _azureBlobMod: Promise<typeof import('@azure/storage-blob')> | undefined;
12
+ function loadAzureBlobSDK(): Promise<typeof import('@azure/storage-blob')> {
13
+ if (!_azureBlobMod) {
14
+ _azureBlobMod = import('@azure/storage-blob').catch(() => {
15
+ _azureBlobMod = undefined;
16
+ throw new Error(
17
+ '@azure/storage-blob is required for Azure storage.\n' +
18
+ 'Install: npm install @azure/storage-blob @azure/identity'
19
+ );
20
+ });
21
+ }
22
+ return _azureBlobMod;
23
+ }
24
+
25
+ let _azureIdentityMod: Promise<typeof import('@azure/identity')> | undefined;
26
+ function loadAzureIdentity(): Promise<typeof import('@azure/identity')> {
27
+ if (!_azureIdentityMod) {
28
+ _azureIdentityMod = import('@azure/identity').catch(() => {
29
+ _azureIdentityMod = undefined;
30
+ throw new Error(
31
+ '@azure/identity is required for Azure Managed Identity authentication.\n' +
32
+ 'Install: npm install @azure/identity'
33
+ );
34
+ });
35
+ }
36
+ return _azureIdentityMod;
37
+ }
38
+
39
+ /**
40
+ * AzureStorageDriver - Handles file operations with Azure Blob Storage.
41
+ *
42
+ * Supports three authentication methods:
43
+ * 1. Connection string (simplest — recommended for getting started)
44
+ * 2. Account name + Account key (more control)
45
+ * 3. Managed Identity (when running on Azure — no secrets needed!)
46
+ *
47
+ * Important: SAS URL generation requires an account key.
48
+ * Managed Identity works great for direct uploads but can't create presigned URLs.
49
+ *
50
+ * When driver is 'azure-presigned', upload() returns SAS URLs instead of
51
+ * uploading directly. Always call validateAndConfirmUpload() after client
52
+ * uploads — Azure doesn't enforce constraints on SAS URLs.
53
+ *
54
+ * Required packages: @azure/storage-blob, @azure/identity
55
+ */
56
+ export class AzureStorageDriver extends BaseStorageDriver {
57
+ private _blobServiceClient?: BlobServiceClientType | undefined;
58
+ private _containerClient?: ContainerClientType | undefined;
59
+ private readonly containerName: string;
60
+ private readonly accountName: string;
61
+ private readonly accountKey?: string;
62
+
63
+ constructor(config: StorageConfig) {
64
+ super(config);
65
+
66
+ this.containerName = config.azureContainerName || config.bucketName || '';
67
+ if (!this.containerName) {
68
+ throw new Error('Azure container name is required. Set BUCKET_NAME environment variable or pass azureContainerName in credentials.');
69
+ }
70
+ this.accountName = '';
71
+
72
+ if (config.azureConnectionString) {
73
+ const accountNameMatch = config.azureConnectionString.match(/AccountName=([a-z0-9]{3,24})(?:;|$)/i);
74
+ if (accountNameMatch && accountNameMatch[1]) {
75
+ this.accountName = accountNameMatch[1].toLowerCase();
76
+ } else {
77
+ throw new Error(
78
+ 'Could not extract AccountName from Azure connection string. ' +
79
+ 'Ensure the connection string contains "AccountName=<name>" where name is 3-24 lowercase letters/numbers.'
80
+ );
81
+ }
82
+
83
+ const keyMatch = config.azureConnectionString.match(/AccountKey=([A-Za-z0-9+/=]{20,})(?:;|$)/);
84
+ if (keyMatch && keyMatch[1]) {
85
+ this.accountKey = keyMatch[1];
86
+ }
87
+ } else if (config.azureAccountName) {
88
+ this.accountName = config.azureAccountName;
89
+ if (config.azureAccountKey) {
90
+ this.accountKey = config.azureAccountKey;
91
+ }
92
+ } else {
93
+ throw new Error('Azure configuration requires either AZURE_CONNECTION_STRING, AZURE_ACCOUNT_NAME + AZURE_ACCOUNT_KEY, or AZURE_ACCOUNT_NAME (for Managed Identity)');
94
+ }
95
+
96
+ // Presigned mode requires an account key for SAS URL generation
97
+ if (this.presignedMode && this.accountKey === undefined) {
98
+ throw new Error(
99
+ 'Azure presigned mode requires an account key for SAS URL generation. ' +
100
+ 'Use AZURE_CONNECTION_STRING or provide both AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY. ' +
101
+ 'Managed Identity cannot be used with presigned URLs - use the regular "azure" driver instead.'
102
+ );
103
+ }
104
+ }
105
+
106
+ private async ensureContainerClient(): Promise<ContainerClientType> {
107
+ if (this._containerClient) return this._containerClient;
108
+
109
+ const azureBlob = await loadAzureBlobSDK();
110
+
111
+ if (this.config.azureConnectionString) {
112
+ this._blobServiceClient = azureBlob.BlobServiceClient.fromConnectionString(this.config.azureConnectionString);
113
+ } else if (this.config.azureAccountName && this.config.azureAccountKey) {
114
+ const sharedKeyCredential = new azureBlob.StorageSharedKeyCredential(
115
+ this.config.azureAccountName,
116
+ this.config.azureAccountKey
117
+ );
118
+ this._blobServiceClient = new azureBlob.BlobServiceClient(
119
+ `https://${this.config.azureAccountName}.blob.core.windows.net`,
120
+ sharedKeyCredential
121
+ );
122
+ } else if (this.config.azureAccountName) {
123
+ const azureIdentity = await loadAzureIdentity();
124
+ this._blobServiceClient = new azureBlob.BlobServiceClient(
125
+ `https://${this.config.azureAccountName}.blob.core.windows.net`,
126
+ new azureIdentity.DefaultAzureCredential()
127
+ );
128
+ } else {
129
+ throw new Error('Azure configuration requires either AZURE_CONNECTION_STRING, AZURE_ACCOUNT_NAME + AZURE_ACCOUNT_KEY, or AZURE_ACCOUNT_NAME (for Managed Identity)');
130
+ }
131
+
132
+ this._containerClient = this._blobServiceClient.getContainerClient(this.containerName);
133
+ return this._containerClient;
134
+ }
135
+
136
+ override destroy(): void {
137
+ this._blobServiceClient = undefined;
138
+ this._containerClient = undefined;
139
+ }
140
+
141
+ /**
142
+ * Uploads a file to Azure, or returns a SAS URL when in presigned mode.
143
+ *
144
+ * For large files (>100MB), uses streaming upload to reduce
145
+ * memory usage and improve reliability.
146
+ */
147
+ async upload(file: Express.Multer.File, options?: UploadOptions): Promise<FileUploadResult> {
148
+ if (this.presignedMode) {
149
+ return this.presignedUpload(file);
150
+ }
151
+
152
+ try {
153
+ const { errors: validationErrors, resolvedSize } = await this.validateFile(file);
154
+ if (validationErrors.length > 0) {
155
+ return this.createErrorResult(validationErrors.join(', '), 'VALIDATION_FAILED');
156
+ }
157
+
158
+ const fileName = this.generateFileName(file.originalname);
159
+ const blobPath = this.buildFilePath(fileName);
160
+ const containerClient = await this.ensureContainerClient();
161
+ const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
162
+
163
+ const uploadOptions: {
164
+ blobHTTPHeaders: {
165
+ blobContentType: string;
166
+ blobCacheControl?: string;
167
+ blobContentDisposition?: string;
168
+ };
169
+ metadata?: Record<string, string>;
170
+ } = {
171
+ blobHTTPHeaders: {
172
+ blobContentType: options?.contentType || file.mimetype,
173
+ },
174
+ };
175
+
176
+ if (options?.cacheControl) {
177
+ uploadOptions.blobHTTPHeaders.blobCacheControl = options.cacheControl;
178
+ }
179
+ if (options?.contentDisposition) {
180
+ uploadOptions.blobHTTPHeaders.blobContentDisposition = options.contentDisposition;
181
+ }
182
+ if (options?.metadata) {
183
+ uploadOptions.metadata = options.metadata;
184
+ }
185
+
186
+ options?.signal?.throwIfAborted();
187
+
188
+ const abortSignal = options?.signal;
189
+
190
+ if (this.shouldUseStreaming(resolvedSize)) {
191
+ const fileStream = this.getFileStream(file);
192
+ const streamOptions: {
193
+ blobHTTPHeaders: typeof uploadOptions.blobHTTPHeaders;
194
+ metadata?: Record<string, string>;
195
+ abortSignal?: AbortSignal;
196
+ } = {
197
+ blobHTTPHeaders: uploadOptions.blobHTTPHeaders,
198
+ };
199
+ if (uploadOptions.metadata) {
200
+ streamOptions.metadata = uploadOptions.metadata;
201
+ }
202
+ if (abortSignal) {
203
+ streamOptions.abortSignal = abortSignal;
204
+ }
205
+ await blockBlobClient.uploadStream(
206
+ fileStream,
207
+ 4 * 1024 * 1024,
208
+ 4,
209
+ streamOptions
210
+ );
211
+ } else {
212
+ const fileContent = await this.getFileContent(file);
213
+ await blockBlobClient.uploadData(fileContent, {
214
+ ...uploadOptions,
215
+ ...(abortSignal ? { abortSignal } : {}),
216
+ });
217
+ }
218
+
219
+ const fileUrl = `https://${this.accountName}.blob.core.windows.net/${this.containerName}/${encodePathSegments(blobPath)}`;
220
+
221
+ return this.createSuccessResult(blobPath, fileUrl);
222
+ } catch (error) {
223
+ await this.cleanupTempFile(file);
224
+ return this.createErrorResult(
225
+ error instanceof Error ? error.message : 'Failed to upload file to Azure'
226
+ );
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Creates a SAS URL for uploading directly to Azure.
232
+ *
233
+ * Important: Unlike S3 and GCS, Azure SAS URLs do NOT enforce file size
234
+ * or content type. Always call validateAndConfirmUpload() after the
235
+ * client uploads to verify the file is what you expected.
236
+ */
237
+ async generateUploadUrl(fileName: string, contentType?: string, _fileSize?: number): Promise<PresignedUrlResult> {
238
+ try {
239
+ const decoded = this.decodeFileName(fileName);
240
+ const url = await this.generateSasUrl(decoded, 'cw', contentType || 'application/octet-stream');
241
+ return this.createPresignedSuccessResult(url);
242
+ } catch (error) {
243
+ return this.createPresignedErrorResult(
244
+ error instanceof Error ? error.message : 'Failed to generate upload URL'
245
+ );
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Creates a SAS URL for downloading/viewing a file.
251
+ */
252
+ async generateViewUrl(fileName: string): Promise<PresignedUrlResult> {
253
+ try {
254
+ const decoded = this.decodeFileName(fileName);
255
+ const url = await this.generateSasUrl(decoded, 'r');
256
+ return this.createPresignedSuccessResult(undefined, url);
257
+ } catch (error) {
258
+ return this.createPresignedErrorResult(
259
+ error instanceof Error ? error.message : 'Failed to generate view URL'
260
+ );
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Generates a SAS URL for a blob with the specified permissions.
266
+ */
267
+ private async generateSasUrl(blobName: string, permissions: string, contentType?: string): Promise<string> {
268
+ if (!this.accountKey) {
269
+ throw new Error('Account key is required for generating SAS URLs. Use connection string or provide AZURE_ACCOUNT_KEY.');
270
+ }
271
+
272
+ const azureBlob = await loadAzureBlobSDK();
273
+ const containerClient = await this.ensureContainerClient();
274
+ const blockBlobClient = containerClient.getBlockBlobClient(blobName);
275
+ const expiresOn = new Date(Date.now() + (this.getPresignedUrlExpiry() * 1000));
276
+
277
+ const sasOptions = {
278
+ containerName: this.containerName,
279
+ blobName,
280
+ permissions: azureBlob.BlobSASPermissions.parse(permissions),
281
+ expiresOn,
282
+ ...(contentType ? { contentType } : {}),
283
+ };
284
+
285
+ const sasToken = azureBlob.generateBlobSASQueryParameters(
286
+ sasOptions,
287
+ new azureBlob.StorageSharedKeyCredential(this.accountName, this.accountKey)
288
+ ).toString();
289
+
290
+ return `${blockBlobClient.url}?${sasToken}`;
291
+ }
292
+
293
+ /**
294
+ * Deletes a file from Azure Blob Storage.
295
+ */
296
+ async delete(fileName: string): Promise<DeleteResult> {
297
+ try {
298
+ const decodedFileName = this.decodeFileName(fileName);
299
+ const containerClient = await this.ensureContainerClient();
300
+ const blockBlobClient = containerClient.getBlockBlobClient(decodedFileName);
301
+
302
+ const exists = await blockBlobClient.exists();
303
+ if (!exists) {
304
+ return { success: false, reference: fileName, error: 'File not found', code: 'FILE_NOT_FOUND' };
305
+ }
306
+
307
+ await blockBlobClient.delete();
308
+ return { success: true, reference: fileName };
309
+ } catch (error) {
310
+ return { success: false, reference: fileName, error: error instanceof Error ? error.message : 'Failed to delete file', code: 'PROVIDER_ERROR' };
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Validates an upload against expected values and deletes invalid files.
316
+ * Uses shared validation logic from BaseStorageDriver.
317
+ *
318
+ * This is CRITICAL for Azure presigned uploads because Azure doesn't
319
+ * enforce constraints at the URL level.
320
+ */
321
+ override async validateAndConfirmUpload(
322
+ reference: string,
323
+ options?: BlobValidationOptions
324
+ ): Promise<BlobValidationResult> {
325
+ try {
326
+ const containerClient = await this.ensureContainerClient();
327
+ const blockBlobClient = containerClient.getBlockBlobClient(reference);
328
+ const properties = await blockBlobClient.getProperties();
329
+
330
+ const actual = {
331
+ contentType: properties.contentType,
332
+ fileSize: properties.contentLength,
333
+ };
334
+
335
+ const validationError = await this.checkUploadedFileMetadata(reference, actual, options);
336
+ if (validationError) return validationError;
337
+
338
+ const viewResult = await this.generateViewUrl(reference);
339
+ return this.buildValidationSuccess(reference, viewResult.success ? viewResult.viewUrl : undefined, actual.contentType, actual.fileSize);
340
+ } catch (error) {
341
+ return {
342
+ success: false,
343
+ error: error instanceof Error ? error.message : 'Failed to validate upload',
344
+ code: 'PROVIDER_ERROR',
345
+ };
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Returns metadata about a file from Azure without downloading it.
351
+ */
352
+ async getMetadata(reference: string): Promise<FileInfo | null> {
353
+ try {
354
+ const decoded = this.decodeFileName(reference);
355
+ const containerClient = await this.ensureContainerClient();
356
+ const blockBlobClient = containerClient.getBlockBlobClient(decoded);
357
+ const exists = await blockBlobClient.exists();
358
+ if (!exists) return null;
359
+
360
+ const properties = await blockBlobClient.getProperties();
361
+ const info: FileInfo = { name: reference };
362
+ if (properties.contentLength !== undefined) info.size = properties.contentLength;
363
+ if (properties.contentType) info.contentType = properties.contentType;
364
+ if (properties.lastModified) info.lastModified = properties.lastModified;
365
+ return info;
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Lists files in the container with optional prefix filtering and pagination.
373
+ */
374
+ async listFiles(
375
+ prefix?: string,
376
+ maxResults: number = 1000,
377
+ continuationToken?: string
378
+ ): Promise<ListFilesResult> {
379
+ try {
380
+ const validatedMaxResults = this.validateMaxResults(maxResults);
381
+
382
+ const containerClient = await this.ensureContainerClient();
383
+ const files: FileInfo[] = [];
384
+ let nextToken: string | undefined;
385
+
386
+ const listOptions: { prefix?: string } = {};
387
+ if (prefix) listOptions.prefix = prefix;
388
+
389
+ const pageOptions: { maxPageSize: number; continuationToken?: string } = {
390
+ maxPageSize: validatedMaxResults,
391
+ };
392
+ if (continuationToken) pageOptions.continuationToken = continuationToken;
393
+
394
+ const iterator = containerClient.listBlobsFlat(listOptions)
395
+ .byPage(pageOptions);
396
+
397
+ const page = await iterator.next();
398
+
399
+ if (!page.done && page.value) {
400
+ for (const blob of page.value.segment.blobItems) {
401
+ const fileInfo: FileInfo = { name: blob.name };
402
+ if (blob.properties.contentLength !== undefined) {
403
+ fileInfo.size = blob.properties.contentLength;
404
+ }
405
+ if (blob.properties.contentType) {
406
+ fileInfo.contentType = blob.properties.contentType;
407
+ }
408
+ if (blob.properties.lastModified) {
409
+ fileInfo.lastModified = blob.properties.lastModified;
410
+ }
411
+ files.push(fileInfo);
412
+ }
413
+ nextToken = page.value.continuationToken;
414
+ }
415
+
416
+ const result: ListFilesResult = {
417
+ success: true,
418
+ files,
419
+ };
420
+
421
+ if (nextToken) {
422
+ result.nextToken = nextToken;
423
+ }
424
+
425
+ return result;
426
+ } catch (error) {
427
+ return {
428
+ success: false,
429
+ error: error instanceof Error ? error.message : 'Failed to list files',
430
+ code: 'PROVIDER_ERROR',
431
+ };
432
+ }
433
+ }
434
+ }