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.
- package/README.md +366 -34
- package/dist/cjs/config/index.d.ts +10 -0
- package/dist/cjs/config/index.d.ts.map +1 -0
- package/dist/cjs/config/index.js +19 -0
- package/dist/cjs/config/index.js.map +1 -0
- package/dist/cjs/drivers/azure.driver.d.ts +73 -0
- package/dist/cjs/drivers/azure.driver.d.ts.map +1 -0
- package/dist/cjs/drivers/azure.driver.js +390 -0
- package/dist/cjs/drivers/azure.driver.js.map +1 -0
- package/dist/cjs/drivers/base.driver.d.ts +136 -0
- package/dist/cjs/drivers/base.driver.d.ts.map +1 -0
- package/dist/cjs/drivers/base.driver.js +357 -0
- package/dist/cjs/drivers/base.driver.js.map +1 -0
- package/dist/{drivers → cjs/drivers}/gcs.driver.d.ts +20 -38
- package/dist/cjs/drivers/gcs.driver.d.ts.map +1 -0
- package/dist/cjs/drivers/gcs.driver.js +343 -0
- package/dist/cjs/drivers/gcs.driver.js.map +1 -0
- package/dist/cjs/drivers/index.d.ts +15 -0
- package/dist/cjs/drivers/index.d.ts.map +1 -0
- package/dist/cjs/drivers/index.js +26 -0
- package/dist/cjs/drivers/index.js.map +1 -0
- package/dist/cjs/drivers/local.driver.d.ts +86 -0
- package/dist/cjs/drivers/local.driver.d.ts.map +1 -0
- package/dist/cjs/drivers/local.driver.js +556 -0
- package/dist/cjs/drivers/local.driver.js.map +1 -0
- package/dist/{drivers → cjs/drivers}/s3.driver.d.ts +19 -39
- package/dist/cjs/drivers/s3.driver.d.ts.map +1 -0
- package/dist/cjs/drivers/s3.driver.js +400 -0
- package/dist/cjs/drivers/s3.driver.js.map +1 -0
- package/dist/cjs/factory/driver.factory.d.ts +43 -0
- package/dist/cjs/factory/driver.factory.d.ts.map +1 -0
- package/dist/cjs/factory/driver.factory.js +101 -0
- package/dist/cjs/factory/driver.factory.js.map +1 -0
- package/dist/cjs/index.d.ts +26 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/storage-manager.d.ts +210 -0
- package/dist/cjs/storage-manager.d.ts.map +1 -0
- package/dist/cjs/storage-manager.js +649 -0
- package/dist/cjs/storage-manager.js.map +1 -0
- package/dist/cjs/types/storage.types.d.ts +438 -0
- package/dist/cjs/types/storage.types.d.ts.map +1 -0
- package/dist/cjs/types/storage.types.js +3 -0
- package/dist/cjs/types/storage.types.js.map +1 -0
- package/dist/cjs/utils/config.utils.d.ts.map +1 -0
- package/dist/cjs/utils/config.utils.js +213 -0
- package/dist/cjs/utils/config.utils.js.map +1 -0
- package/dist/{utils → cjs/utils}/file.utils.d.ts +62 -8
- package/dist/cjs/utils/file.utils.d.ts.map +1 -0
- package/dist/cjs/utils/file.utils.js +464 -0
- package/dist/cjs/utils/file.utils.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +12 -0
- package/dist/cjs/utils/index.d.ts.map +1 -0
- package/dist/cjs/utils/index.js +36 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/cjs/utils/rate-limiter.d.ts +40 -0
- package/dist/cjs/utils/rate-limiter.d.ts.map +1 -0
- package/dist/cjs/utils/rate-limiter.js +87 -0
- package/dist/cjs/utils/rate-limiter.js.map +1 -0
- package/dist/esm/config/index.d.ts +10 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +10 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/drivers/azure.driver.d.ts +73 -0
- package/dist/esm/drivers/azure.driver.d.ts.map +1 -0
- package/dist/esm/drivers/azure.driver.js +353 -0
- package/dist/esm/drivers/azure.driver.js.map +1 -0
- package/dist/esm/drivers/base.driver.d.ts +136 -0
- package/dist/esm/drivers/base.driver.d.ts.map +1 -0
- package/dist/esm/drivers/base.driver.js +350 -0
- package/dist/esm/drivers/base.driver.js.map +1 -0
- package/dist/esm/drivers/gcs.driver.d.ts +68 -0
- package/dist/esm/drivers/gcs.driver.d.ts.map +1 -0
- package/dist/esm/drivers/gcs.driver.js +306 -0
- package/dist/esm/drivers/gcs.driver.js.map +1 -0
- package/dist/esm/drivers/index.d.ts +15 -0
- package/dist/esm/drivers/index.d.ts.map +1 -0
- package/dist/esm/drivers/index.js +15 -0
- package/dist/esm/drivers/index.js.map +1 -0
- package/dist/esm/drivers/local.driver.d.ts +86 -0
- package/dist/esm/drivers/local.driver.d.ts.map +1 -0
- package/dist/esm/drivers/local.driver.js +549 -0
- package/dist/esm/drivers/local.driver.js.map +1 -0
- package/dist/esm/drivers/s3.driver.d.ts +69 -0
- package/dist/esm/drivers/s3.driver.d.ts.map +1 -0
- package/dist/esm/drivers/s3.driver.js +363 -0
- package/dist/esm/drivers/s3.driver.js.map +1 -0
- package/dist/esm/factory/driver.factory.d.ts +43 -0
- package/dist/esm/factory/driver.factory.d.ts.map +1 -0
- package/dist/esm/factory/driver.factory.js +92 -0
- package/dist/esm/factory/driver.factory.js.map +1 -0
- package/dist/esm/index.d.ts +26 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/storage-manager.d.ts +210 -0
- package/dist/esm/storage-manager.d.ts.map +1 -0
- package/dist/esm/storage-manager.js +645 -0
- package/dist/esm/storage-manager.js.map +1 -0
- package/dist/esm/types/storage.types.d.ts +438 -0
- package/dist/esm/types/storage.types.d.ts.map +1 -0
- package/dist/esm/types/storage.types.js.map +1 -0
- package/dist/esm/utils/config.utils.d.ts +45 -0
- package/dist/esm/utils/config.utils.d.ts.map +1 -0
- package/dist/esm/utils/config.utils.js.map +1 -0
- package/dist/esm/utils/file.utils.d.ts +196 -0
- package/dist/esm/utils/file.utils.d.ts.map +1 -0
- package/dist/esm/utils/file.utils.js +439 -0
- package/dist/esm/utils/file.utils.js.map +1 -0
- package/dist/esm/utils/index.d.ts +12 -0
- package/dist/esm/utils/index.d.ts.map +1 -0
- package/dist/esm/utils/index.js +11 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/utils/rate-limiter.d.ts +40 -0
- package/dist/esm/utils/rate-limiter.d.ts.map +1 -0
- package/dist/esm/utils/rate-limiter.js +82 -0
- package/dist/esm/utils/rate-limiter.js.map +1 -0
- package/package.json +90 -52
- package/src/config/index.ts +17 -0
- package/src/drivers/azure.driver.ts +434 -0
- package/src/drivers/base.driver.ts +436 -0
- package/src/drivers/gcs.driver.ts +366 -0
- package/src/drivers/index.ts +15 -0
- package/src/drivers/local.driver.ts +626 -0
- package/src/drivers/s3.driver.ts +459 -0
- package/src/factory/driver.factory.ts +101 -0
- package/src/index.ts +72 -0
- package/src/storage-manager.ts +801 -0
- package/src/types/storage.types.ts +561 -0
- package/src/utils/config.utils.ts +229 -0
- package/src/utils/file.utils.ts +536 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/rate-limiter.ts +94 -0
- package/dist/drivers/azure.driver.d.ts +0 -88
- package/dist/drivers/azure.driver.d.ts.map +0 -1
- package/dist/drivers/azure.driver.js +0 -391
- package/dist/drivers/azure.driver.js.map +0 -1
- package/dist/drivers/base.driver.d.ts +0 -170
- package/dist/drivers/base.driver.d.ts.map +0 -1
- package/dist/drivers/base.driver.js +0 -347
- package/dist/drivers/base.driver.js.map +0 -1
- package/dist/drivers/gcs.driver.d.ts.map +0 -1
- package/dist/drivers/gcs.driver.js +0 -354
- package/dist/drivers/gcs.driver.js.map +0 -1
- package/dist/drivers/local.driver.d.ts +0 -107
- package/dist/drivers/local.driver.d.ts.map +0 -1
- package/dist/drivers/local.driver.js +0 -621
- package/dist/drivers/local.driver.js.map +0 -1
- package/dist/drivers/s3.driver.d.ts.map +0 -1
- package/dist/drivers/s3.driver.js +0 -387
- package/dist/drivers/s3.driver.js.map +0 -1
- package/dist/factory/driver.factory.d.ts +0 -62
- package/dist/factory/driver.factory.d.ts.map +0 -1
- package/dist/factory/driver.factory.js +0 -177
- package/dist/factory/driver.factory.js.map +0 -1
- package/dist/index.d.ts +0 -30
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -33
- package/dist/index.js.map +0 -1
- package/dist/storage-manager.d.ts +0 -228
- package/dist/storage-manager.d.ts.map +0 -1
- package/dist/storage-manager.js +0 -715
- package/dist/storage-manager.js.map +0 -1
- package/dist/types/storage.types.d.ts +0 -295
- package/dist/types/storage.types.d.ts.map +0 -1
- package/dist/types/storage.types.js.map +0 -1
- package/dist/utils/config.utils.d.ts.map +0 -1
- package/dist/utils/config.utils.js.map +0 -1
- package/dist/utils/file.utils.d.ts.map +0 -1
- package/dist/utils/file.utils.js +0 -278
- package/dist/utils/file.utils.js.map +0 -1
- /package/dist/{utils → cjs/utils}/config.utils.d.ts +0 -0
- /package/dist/{types → esm/types}/storage.types.js +0 -0
- /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
|
+
}
|