@nestm/storage 0.1.0-alpha.7 → 0.1.0-alpha.8
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/CHANGELOG.md +110 -0
- package/README.md +217 -18
- package/SECURITY.md +34 -5
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +14 -3
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -1
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +54 -23
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/ai-sdk/index.js.map +1 -1
- package/dist/files-sdk/files-sdk.driver.d.ts +54 -10
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +784 -63
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +4 -4
- package/dist/files-sdk/fs/index.d.ts.map +1 -1
- package/dist/files-sdk/fs/index.js +242 -9
- package/dist/files-sdk/fs/index.js.map +1 -1
- package/dist/files-sdk/index.d.ts +1 -1
- package/dist/files-sdk/index.d.ts.map +1 -1
- package/dist/files-sdk/index.js.map +1 -1
- package/dist/files-sdk/provider/index.d.ts +9 -5
- package/dist/files-sdk/provider/index.d.ts.map +1 -1
- package/dist/files-sdk/provider/index.js +53 -15
- package/dist/files-sdk/provider/index.js.map +1 -1
- package/dist/files-sdk/s3/construction-metadata.d.ts +7 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.js +13 -0
- package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
- package/dist/files-sdk/s3/index.d.ts +39 -19
- package/dist/files-sdk/s3/index.d.ts.map +1 -1
- package/dist/files-sdk/s3/index.js +936 -113
- package/dist/files-sdk/s3/index.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +25 -5
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/storage-etag.d.ts +13 -0
- package/dist/storage-etag.d.ts.map +1 -0
- package/dist/storage-etag.js +31 -0
- package/dist/storage-etag.js.map +1 -0
- package/dist/storage.client.d.ts +3 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +87 -22
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +21 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.types.d.ts +90 -19
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/provider-conformance.d.ts +65 -0
- package/dist/testing/provider-conformance.d.ts.map +1 -0
- package/dist/testing/provider-conformance.js +853 -0
- package/dist/testing/provider-conformance.js.map +1 -0
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/storage-workspace.cursor.d.ts +68 -3
- package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.cursor.js +402 -39
- package/dist/workspace/storage-workspace.cursor.js.map +1 -1
- package/dist/workspace/storage-workspace.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.js +54 -46
- package/dist/workspace/storage-workspace.js.map +1 -1
- package/dist/workspace/storage-workspace.types.d.ts +10 -2
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.types.js +1 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,8 +1,315 @@
|
|
|
1
|
-
import { CopyObjectCommand, DeleteObjectCommand, PutObjectCommand, } from '@aws-sdk/client-s3';
|
|
1
|
+
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CopyObjectCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, PutObjectCommand, UploadPartCommand, } from '@aws-sdk/client-s3';
|
|
2
|
+
import { createPresignedPost, } from '@aws-sdk/s3-presigned-post';
|
|
3
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
2
4
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { mapS3Error, s3, } from 'files-sdk/s3';
|
|
4
|
-
import { StorageError, StorageErrorCode } from '../../storage.error.js';
|
|
5
|
-
import {
|
|
5
|
+
import { mapS3Error, s3 as createFilesSdkS3Adapter, } from 'files-sdk/s3';
|
|
6
|
+
import { StorageError, StorageErrorCode, isStorageError, } from '../../storage.error.js';
|
|
7
|
+
import { normalizeProviderStorageEtag, storageEtagHeader, } from '../../storage-etag.js';
|
|
8
|
+
import { assertFilesSdkS3AdapterHasNoReservedExtensions, createFilesSdkDriver, markFilesSdkS3AdapterUndecorated, markFilesSdkS3RawClientProvenance, mapFilesSdkError, } from '../files-sdk.driver.js';
|
|
9
|
+
import { getS3ConstructionMetadata, recordS3ConstructionMetadata, } from './construction-metadata.js';
|
|
10
|
+
const verifiedS3ProviderProfile = Symbol('verifiedS3ProviderProfile');
|
|
11
|
+
const verifiedS3ProviderProfiles = new WeakSet();
|
|
12
|
+
function assertCapabilityBoolean(value, label) {
|
|
13
|
+
if (typeof value !== 'boolean') {
|
|
14
|
+
throw new TypeError(`${label} must be a boolean.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Validates and deeply freezes an explicitly verified S3-compatible profile. */
|
|
18
|
+
export function defineS3ProviderProfile(profile) {
|
|
19
|
+
if (typeof profile.name !== 'string' || profile.name.trim().length === 0) {
|
|
20
|
+
throw new TypeError('S3 provider profile name must be a non-empty string.');
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isSafeInteger(profile.physicalKey.maxBytes) ||
|
|
23
|
+
profile.physicalKey.maxBytes <= 0) {
|
|
24
|
+
throw new TypeError('S3 provider profile physicalKey.maxBytes must be a positive safe integer.');
|
|
25
|
+
}
|
|
26
|
+
const booleanFields = [
|
|
27
|
+
[
|
|
28
|
+
profile.signedUploadPolicy !== undefined,
|
|
29
|
+
'signedUploadPolicy.contentType',
|
|
30
|
+
profile.signedUploadPolicy?.contentType,
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
profile.signedUploadPolicy !== undefined,
|
|
34
|
+
'signedUploadPolicy.sizeRange',
|
|
35
|
+
profile.signedUploadPolicy?.sizeRange,
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
profile.conditionalCreate !== undefined,
|
|
39
|
+
'conditionalCreate.resultEtag',
|
|
40
|
+
profile.conditionalCreate?.resultEtag,
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
profile.conditionalReplace !== undefined,
|
|
44
|
+
'conditionalReplace.resultEtag',
|
|
45
|
+
profile.conditionalReplace?.resultEtag,
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
profile.conditionalDelete !== undefined,
|
|
49
|
+
'conditionalDelete.etag',
|
|
50
|
+
profile.conditionalDelete?.etag,
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
profile.conditionalRead !== undefined,
|
|
54
|
+
'conditionalRead.etag',
|
|
55
|
+
profile.conditionalRead?.etag,
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
profile.conditionalRead !== undefined,
|
|
59
|
+
'conditionalRead.version',
|
|
60
|
+
profile.conditionalRead?.version,
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
profile.conditionalCopySource !== undefined,
|
|
64
|
+
'conditionalCopySource.etag',
|
|
65
|
+
profile.conditionalCopySource?.etag,
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
profile.conditionalCopySource !== undefined,
|
|
69
|
+
'conditionalCopySource.version',
|
|
70
|
+
profile.conditionalCopySource?.version,
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
profile.conditionalCopyDestination !== undefined,
|
|
74
|
+
'conditionalCopyDestination.create',
|
|
75
|
+
profile.conditionalCopyDestination?.create,
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
profile.conditionalCopyDestination !== undefined,
|
|
79
|
+
'conditionalCopyDestination.replace',
|
|
80
|
+
profile.conditionalCopyDestination?.replace,
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
profile.conditionalCopyDestination !== undefined,
|
|
84
|
+
'conditionalCopyDestination.atomicWithSource',
|
|
85
|
+
profile.conditionalCopyDestination?.atomicWithSource,
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
profile.conditionalMultipartCompletion !== undefined,
|
|
89
|
+
'conditionalMultipartCompletion.create',
|
|
90
|
+
profile.conditionalMultipartCompletion?.create,
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
profile.conditionalMultipartCompletion !== undefined,
|
|
94
|
+
'conditionalMultipartCompletion.replace',
|
|
95
|
+
profile.conditionalMultipartCompletion?.replace,
|
|
96
|
+
],
|
|
97
|
+
];
|
|
98
|
+
for (const [declared, label, value] of booleanFields) {
|
|
99
|
+
if (declared)
|
|
100
|
+
assertCapabilityBoolean(value, label);
|
|
101
|
+
}
|
|
102
|
+
if (profile.conditionalCopyDestination?.atomicWithSource === true &&
|
|
103
|
+
profile.conditionalCopySource?.etag !== true &&
|
|
104
|
+
profile.conditionalCopySource?.version !== true) {
|
|
105
|
+
throw new TypeError('An atomic destination-copy profile must enable at least one source-copy condition.');
|
|
106
|
+
}
|
|
107
|
+
if (profile.conditionalCopyDestination?.atomicWithSource === true &&
|
|
108
|
+
profile.conditionalCopyDestination.create !== true &&
|
|
109
|
+
profile.conditionalCopyDestination.replace !== true) {
|
|
110
|
+
throw new TypeError('An atomic destination-copy profile must declare create or replace support.');
|
|
111
|
+
}
|
|
112
|
+
if (profile.conditionalMultipartCompletion?.create === true &&
|
|
113
|
+
profile.conditionalCreate === undefined) {
|
|
114
|
+
throw new TypeError('Conditional multipart create requires conditional create support.');
|
|
115
|
+
}
|
|
116
|
+
if (profile.conditionalMultipartCompletion?.replace === true &&
|
|
117
|
+
profile.conditionalReplace === undefined) {
|
|
118
|
+
throw new TypeError('Conditional multipart replace requires conditional replace support.');
|
|
119
|
+
}
|
|
120
|
+
const clone = {
|
|
121
|
+
name: profile.name.trim(),
|
|
122
|
+
physicalKey: Object.freeze({ ...profile.physicalKey }),
|
|
123
|
+
signedUploadPolicy: Object.freeze(profile.signedUploadPolicy === undefined
|
|
124
|
+
? { contentType: false, sizeRange: false }
|
|
125
|
+
: { ...profile.signedUploadPolicy }),
|
|
126
|
+
...(profile.conditionalCreate !== undefined && {
|
|
127
|
+
conditionalCreate: Object.freeze({ ...profile.conditionalCreate }),
|
|
128
|
+
}),
|
|
129
|
+
...(profile.conditionalReplace !== undefined && {
|
|
130
|
+
conditionalReplace: Object.freeze({ ...profile.conditionalReplace }),
|
|
131
|
+
}),
|
|
132
|
+
...(profile.conditionalDelete !== undefined && {
|
|
133
|
+
conditionalDelete: Object.freeze({ ...profile.conditionalDelete }),
|
|
134
|
+
}),
|
|
135
|
+
...(profile.conditionalRead !== undefined && {
|
|
136
|
+
conditionalRead: Object.freeze({ ...profile.conditionalRead }),
|
|
137
|
+
}),
|
|
138
|
+
...(profile.conditionalCopySource !== undefined && {
|
|
139
|
+
conditionalCopySource: Object.freeze({
|
|
140
|
+
...profile.conditionalCopySource,
|
|
141
|
+
}),
|
|
142
|
+
}),
|
|
143
|
+
...(profile.conditionalCopyDestination !== undefined && {
|
|
144
|
+
conditionalCopyDestination: Object.freeze({
|
|
145
|
+
...profile.conditionalCopyDestination,
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
...(profile.conditionalMultipartCompletion !== undefined && {
|
|
149
|
+
conditionalMultipartCompletion: Object.freeze({
|
|
150
|
+
...profile.conditionalMultipartCompletion,
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
Object.defineProperty(clone, verifiedS3ProviderProfile, {
|
|
155
|
+
configurable: false,
|
|
156
|
+
enumerable: false,
|
|
157
|
+
value: true,
|
|
158
|
+
writable: false,
|
|
159
|
+
});
|
|
160
|
+
const verified = Object.freeze(clone);
|
|
161
|
+
verifiedS3ProviderProfiles.add(verified);
|
|
162
|
+
return verified;
|
|
163
|
+
}
|
|
164
|
+
function assertVerifiedS3ProviderProfile(profile) {
|
|
165
|
+
if (typeof profile !== 'object' ||
|
|
166
|
+
profile === null ||
|
|
167
|
+
!verifiedS3ProviderProfiles.has(profile)) {
|
|
168
|
+
throw new TypeError('providerProfile must be created with defineS3ProviderProfile().');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function assertS3ProviderProfileContainedBy(profile, ceiling) {
|
|
172
|
+
if (profile.physicalKey.maxBytes > ceiling.physicalKey.maxBytes) {
|
|
173
|
+
throw new TypeError(`S3 provider profile "${profile.name}" cannot widen ${ceiling.name} physicalKey.maxBytes beyond ${ceiling.physicalKey.maxBytes}.`);
|
|
174
|
+
}
|
|
175
|
+
const operationCapabilities = [
|
|
176
|
+
['conditionalCreate', profile.conditionalCreate, ceiling.conditionalCreate],
|
|
177
|
+
[
|
|
178
|
+
'conditionalReplace',
|
|
179
|
+
profile.conditionalReplace,
|
|
180
|
+
ceiling.conditionalReplace,
|
|
181
|
+
],
|
|
182
|
+
['conditionalDelete', profile.conditionalDelete, ceiling.conditionalDelete],
|
|
183
|
+
['conditionalRead', profile.conditionalRead, ceiling.conditionalRead],
|
|
184
|
+
[
|
|
185
|
+
'conditionalCopySource',
|
|
186
|
+
profile.conditionalCopySource,
|
|
187
|
+
ceiling.conditionalCopySource,
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'conditionalCopyDestination',
|
|
191
|
+
profile.conditionalCopyDestination,
|
|
192
|
+
ceiling.conditionalCopyDestination,
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
'conditionalMultipartCompletion',
|
|
196
|
+
profile.conditionalMultipartCompletion,
|
|
197
|
+
ceiling.conditionalMultipartCompletion,
|
|
198
|
+
],
|
|
199
|
+
];
|
|
200
|
+
for (const [name, declared, supported] of operationCapabilities) {
|
|
201
|
+
if (declared !== undefined && supported === undefined) {
|
|
202
|
+
throw new TypeError(`S3 provider profile "${profile.name}" cannot widen ${ceiling.name} with ${name}.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const booleanClaims = [
|
|
206
|
+
[
|
|
207
|
+
'signedUploadPolicy.contentType',
|
|
208
|
+
profile.signedUploadPolicy.contentType,
|
|
209
|
+
ceiling.signedUploadPolicy.contentType,
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
'signedUploadPolicy.sizeRange',
|
|
213
|
+
profile.signedUploadPolicy.sizeRange,
|
|
214
|
+
ceiling.signedUploadPolicy.sizeRange,
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'conditionalCreate.resultEtag',
|
|
218
|
+
profile.conditionalCreate?.resultEtag,
|
|
219
|
+
ceiling.conditionalCreate?.resultEtag,
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
'conditionalReplace.resultEtag',
|
|
223
|
+
profile.conditionalReplace?.resultEtag,
|
|
224
|
+
ceiling.conditionalReplace?.resultEtag,
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
'conditionalDelete.etag',
|
|
228
|
+
profile.conditionalDelete?.etag,
|
|
229
|
+
ceiling.conditionalDelete?.etag,
|
|
230
|
+
],
|
|
231
|
+
[
|
|
232
|
+
'conditionalRead.etag',
|
|
233
|
+
profile.conditionalRead?.etag,
|
|
234
|
+
ceiling.conditionalRead?.etag,
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
'conditionalRead.version',
|
|
238
|
+
profile.conditionalRead?.version,
|
|
239
|
+
ceiling.conditionalRead?.version,
|
|
240
|
+
],
|
|
241
|
+
[
|
|
242
|
+
'conditionalCopySource.etag',
|
|
243
|
+
profile.conditionalCopySource?.etag,
|
|
244
|
+
ceiling.conditionalCopySource?.etag,
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
'conditionalCopySource.version',
|
|
248
|
+
profile.conditionalCopySource?.version,
|
|
249
|
+
ceiling.conditionalCopySource?.version,
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
'conditionalCopyDestination.create',
|
|
253
|
+
profile.conditionalCopyDestination?.create,
|
|
254
|
+
ceiling.conditionalCopyDestination?.create,
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
'conditionalCopyDestination.replace',
|
|
258
|
+
profile.conditionalCopyDestination?.replace,
|
|
259
|
+
ceiling.conditionalCopyDestination?.replace,
|
|
260
|
+
],
|
|
261
|
+
[
|
|
262
|
+
'conditionalCopyDestination.atomicWithSource',
|
|
263
|
+
profile.conditionalCopyDestination?.atomicWithSource,
|
|
264
|
+
ceiling.conditionalCopyDestination?.atomicWithSource,
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
'conditionalMultipartCompletion.create',
|
|
268
|
+
profile.conditionalMultipartCompletion?.create,
|
|
269
|
+
ceiling.conditionalMultipartCompletion?.create,
|
|
270
|
+
],
|
|
271
|
+
[
|
|
272
|
+
'conditionalMultipartCompletion.replace',
|
|
273
|
+
profile.conditionalMultipartCompletion?.replace,
|
|
274
|
+
ceiling.conditionalMultipartCompletion?.replace,
|
|
275
|
+
],
|
|
276
|
+
];
|
|
277
|
+
for (const [name, declared, supported] of booleanClaims) {
|
|
278
|
+
if (declared === true && supported !== true) {
|
|
279
|
+
throw new TypeError(`S3 provider profile "${profile.name}" cannot widen ${ceiling.name} with ${name}.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export const AWS_S3_PROVIDER_PROFILE = defineS3ProviderProfile({
|
|
284
|
+
name: 'aws-s3-general-purpose',
|
|
285
|
+
physicalKey: { maxBytes: 1024 },
|
|
286
|
+
signedUploadPolicy: { contentType: true, sizeRange: true },
|
|
287
|
+
conditionalCreate: { resultEtag: true },
|
|
288
|
+
conditionalReplace: { resultEtag: true },
|
|
289
|
+
conditionalDelete: { etag: true },
|
|
290
|
+
conditionalRead: { etag: true, version: true },
|
|
291
|
+
conditionalCopySource: { etag: true, version: true },
|
|
292
|
+
conditionalCopyDestination: {
|
|
293
|
+
atomicWithSource: true,
|
|
294
|
+
create: true,
|
|
295
|
+
replace: true,
|
|
296
|
+
},
|
|
297
|
+
conditionalMultipartCompletion: { create: true, replace: true },
|
|
298
|
+
});
|
|
299
|
+
export const CLOUDFLARE_R2_PROVIDER_PROFILE = defineS3ProviderProfile({
|
|
300
|
+
name: 'cloudflare-r2-stable',
|
|
301
|
+
physicalKey: { maxBytes: 1024 },
|
|
302
|
+
signedUploadPolicy: { contentType: true, sizeRange: false },
|
|
303
|
+
conditionalCreate: { resultEtag: true },
|
|
304
|
+
conditionalReplace: { resultEtag: true },
|
|
305
|
+
conditionalRead: { etag: true, version: false },
|
|
306
|
+
conditionalCopySource: { etag: true, version: false },
|
|
307
|
+
});
|
|
308
|
+
const UNVERIFIED_S3_PROVIDER_PROFILE = defineS3ProviderProfile({
|
|
309
|
+
name: 'unverified-s3-compatible',
|
|
310
|
+
physicalKey: { maxBytes: 1024 },
|
|
311
|
+
signedUploadPolicy: { contentType: false, sizeRange: false },
|
|
312
|
+
});
|
|
6
313
|
function copySource(bucket, key, version) {
|
|
7
314
|
const source = `${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;
|
|
8
315
|
return version === undefined
|
|
@@ -26,11 +333,32 @@ function maxRetries(options) {
|
|
|
26
333
|
: options.retries?.max;
|
|
27
334
|
return Math.max(0, Math.floor(configured ?? 0));
|
|
28
335
|
}
|
|
29
|
-
function etagHeader(etag) {
|
|
30
|
-
|
|
336
|
+
function etagHeader(etag, key, operation, label) {
|
|
337
|
+
const header = storageEtagHeader(etag);
|
|
338
|
+
if (header === undefined) {
|
|
339
|
+
throw new StorageError(`${label} must be a canonical storage ETag.`, {
|
|
340
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
341
|
+
key,
|
|
342
|
+
operation,
|
|
343
|
+
permanent: true,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return header;
|
|
31
347
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
348
|
+
function providerEtag(etag, key, operation) {
|
|
349
|
+
if (etag === undefined) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
const normalized = normalizeProviderStorageEtag(etag);
|
|
353
|
+
if (normalized === undefined) {
|
|
354
|
+
throw new StorageError('S3 returned an invalid ETag.', {
|
|
355
|
+
code: StorageErrorCode.PROVIDER,
|
|
356
|
+
key,
|
|
357
|
+
operation,
|
|
358
|
+
permanent: true,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return normalized;
|
|
34
362
|
}
|
|
35
363
|
function contentTypeOf(body, override) {
|
|
36
364
|
if (override !== undefined) {
|
|
@@ -97,7 +425,48 @@ async function normalizeConditionalBody(body, onProgress) {
|
|
|
97
425
|
size: () => loaded,
|
|
98
426
|
};
|
|
99
427
|
}
|
|
100
|
-
|
|
428
|
+
function s3ErrorIdentity(error) {
|
|
429
|
+
if (typeof error !== 'object' || error === null) {
|
|
430
|
+
return { code: undefined, status: undefined };
|
|
431
|
+
}
|
|
432
|
+
const candidate = error;
|
|
433
|
+
const serviceCode = candidate.name ?? candidate.Code ?? candidate.code;
|
|
434
|
+
const status = candidate.$metadata?.httpStatusCode;
|
|
435
|
+
return {
|
|
436
|
+
code: typeof serviceCode === 'string' ? serviceCode : undefined,
|
|
437
|
+
status: typeof status === 'number' ? status : undefined,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function mapS3ConditionalError(error, operation) {
|
|
441
|
+
if (isStorageError(error)) {
|
|
442
|
+
return error;
|
|
443
|
+
}
|
|
444
|
+
const identity = s3ErrorIdentity(error);
|
|
445
|
+
const provider = mapFilesSdkError(mapS3Error(error));
|
|
446
|
+
const precondition = identity.status === 412 || identity.code === 'PreconditionFailed';
|
|
447
|
+
const conditionalConflict = identity.status === 409 || identity.code === 'ConditionalRequestConflict';
|
|
448
|
+
const notFound = identity.status === 404 ||
|
|
449
|
+
identity.code === 'NoSuchKey' ||
|
|
450
|
+
identity.code === 'NoSuchUpload' ||
|
|
451
|
+
provider.code === StorageErrorCode.NOT_FOUND;
|
|
452
|
+
const code = precondition || conditionalConflict
|
|
453
|
+
? StorageErrorCode.CONFLICT
|
|
454
|
+
: notFound
|
|
455
|
+
? StorageErrorCode.NOT_FOUND
|
|
456
|
+
: provider.code;
|
|
457
|
+
return new StorageError(`Conditional S3 ${operation} failed.`, {
|
|
458
|
+
aborted: provider.aborted,
|
|
459
|
+
code,
|
|
460
|
+
operation,
|
|
461
|
+
permanent: precondition || notFound
|
|
462
|
+
? true
|
|
463
|
+
: conditionalConflict
|
|
464
|
+
? false
|
|
465
|
+
: provider.permanent,
|
|
466
|
+
timedOut: provider.timedOut,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async function withS3Retry(options, operationName, operation) {
|
|
101
470
|
const retries = maxRetries(options);
|
|
102
471
|
for (let attempt = 0;; attempt += 1) {
|
|
103
472
|
const signal = operationSignal(options);
|
|
@@ -105,20 +474,19 @@ async function withS3Retry(options, operation) {
|
|
|
105
474
|
return await operation(signal);
|
|
106
475
|
}
|
|
107
476
|
catch (error) {
|
|
108
|
-
const mapped =
|
|
477
|
+
const mapped = mapS3ConditionalError(error, operationName);
|
|
109
478
|
if (attempt >= retries ||
|
|
110
|
-
mapped.code !==
|
|
479
|
+
mapped.code !== StorageErrorCode.PROVIDER ||
|
|
111
480
|
mapped.aborted ||
|
|
112
481
|
mapped.permanent ||
|
|
113
482
|
signal?.aborted === true) {
|
|
114
483
|
throw mapped;
|
|
115
484
|
}
|
|
116
|
-
const storageError = mapFilesSdkError(mapped);
|
|
117
485
|
const delay = typeof options.retries === 'object' &&
|
|
118
486
|
options.retries.backoff !== undefined
|
|
119
487
|
? options.retries.backoff({
|
|
120
488
|
attempt: attempt + 1,
|
|
121
|
-
error:
|
|
489
|
+
error: mapped,
|
|
122
490
|
})
|
|
123
491
|
: Math.min(1000, 100 * 2 ** attempt);
|
|
124
492
|
await waitForRetry(delay, options.signal);
|
|
@@ -143,143 +511,598 @@ async function waitForRetry(milliseconds, signal) {
|
|
|
143
511
|
signal?.addEventListener('abort', abort, { once: true });
|
|
144
512
|
});
|
|
145
513
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
514
|
+
const S3_MIN_MULTIPART_PART_BYTES = 5 * 1024 * 1024;
|
|
515
|
+
const S3_MAX_MULTIPART_PARTS = 10_000;
|
|
516
|
+
function conditionalHeaders(condition, key) {
|
|
517
|
+
return condition.type === 'create'
|
|
518
|
+
? { IfNoneMatch: '*' }
|
|
519
|
+
: {
|
|
520
|
+
IfMatch: etagHeader(condition.etag, key, 'upload', 'condition.etag'),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function rangeHeader(range) {
|
|
524
|
+
if (range === undefined)
|
|
525
|
+
return undefined;
|
|
526
|
+
return `bytes=${range.start}-${range.end ?? ''}`;
|
|
527
|
+
}
|
|
528
|
+
function s3ResponseStream(body) {
|
|
529
|
+
let stream;
|
|
530
|
+
if (body instanceof ReadableStream) {
|
|
531
|
+
stream = body;
|
|
532
|
+
}
|
|
533
|
+
else if (body instanceof Readable) {
|
|
534
|
+
stream = Readable.toWeb(body);
|
|
535
|
+
}
|
|
536
|
+
else if (typeof body === 'object' &&
|
|
537
|
+
body !== null &&
|
|
538
|
+
'transformToWebStream' in body &&
|
|
539
|
+
typeof body.transformToWebStream === 'function') {
|
|
540
|
+
stream = body.transformToWebStream();
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
throw new StorageError('Conditional S3 download returned no body stream.', {
|
|
544
|
+
code: StorageErrorCode.PROVIDER,
|
|
545
|
+
operation: 'download',
|
|
546
|
+
permanent: true,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const reader = stream.getReader();
|
|
550
|
+
return new ReadableStream({
|
|
551
|
+
async cancel(reason) {
|
|
552
|
+
try {
|
|
553
|
+
await reader.cancel(reason);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw mapS3ConditionalError(error, 'download');
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
reader.releaseLock();
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
async pull(controller) {
|
|
563
|
+
try {
|
|
564
|
+
const result = await reader.read();
|
|
565
|
+
if (result.done) {
|
|
566
|
+
reader.releaseLock();
|
|
567
|
+
controller.close();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
controller.enqueue(result.value);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
reader.releaseLock();
|
|
574
|
+
controller.error(mapS3ConditionalError(error, 'download'));
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async function* multipartParts(source, partSize) {
|
|
580
|
+
if (source instanceof Uint8Array) {
|
|
581
|
+
for (let offset = 0; offset < source.byteLength; offset += partSize) {
|
|
582
|
+
yield source.subarray(offset, Math.min(offset + partSize, source.byteLength));
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const reader = source.getReader();
|
|
587
|
+
let buffered = new Uint8Array(0);
|
|
588
|
+
try {
|
|
589
|
+
for (;;) {
|
|
590
|
+
const result = await reader.read();
|
|
591
|
+
if (result.done)
|
|
592
|
+
break;
|
|
593
|
+
const combined = new Uint8Array(buffered.byteLength + result.value.byteLength);
|
|
594
|
+
combined.set(buffered);
|
|
595
|
+
combined.set(result.value, buffered.byteLength);
|
|
596
|
+
buffered = combined;
|
|
597
|
+
while (buffered.byteLength >= partSize) {
|
|
598
|
+
yield buffered.slice(0, partSize);
|
|
599
|
+
buffered = buffered.slice(partSize);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (buffered.byteLength > 0)
|
|
603
|
+
yield buffered;
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
reader.releaseLock();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function missingResultEtag(key) {
|
|
610
|
+
return new StorageError('S3 committed a conditional upload without returning the ETag required to identify its result; reconcile the destination before retrying.', {
|
|
611
|
+
code: StorageErrorCode.PROVIDER,
|
|
612
|
+
key,
|
|
613
|
+
operation: 'upload',
|
|
614
|
+
permanent: true,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
async function uploadConditionalSingle(base, key, body, conditional) {
|
|
618
|
+
const normalized = await normalizeConditionalBody(body, conditional.onProgress);
|
|
619
|
+
const contentType = contentTypeOf(body, conditional.contentType);
|
|
620
|
+
const retryOptions = normalized.contentLength === undefined
|
|
621
|
+
? { ...conditional, retries: 0 }
|
|
622
|
+
: conditional;
|
|
623
|
+
const result = await withS3Retry(retryOptions, 'upload', (signal) => base.raw.send(new PutObjectCommand({
|
|
624
|
+
Body: normalized.body,
|
|
625
|
+
Bucket: base.bucket,
|
|
626
|
+
...(conditional.cacheControl !== undefined && {
|
|
627
|
+
CacheControl: conditional.cacheControl,
|
|
163
628
|
}),
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
sizeRange: true,
|
|
629
|
+
...(normalized.contentLength !== undefined && {
|
|
630
|
+
ContentLength: normalized.contentLength,
|
|
167
631
|
}),
|
|
168
|
-
|
|
169
|
-
|
|
632
|
+
ContentType: contentType,
|
|
633
|
+
...conditionalHeaders(conditional.condition, key),
|
|
634
|
+
Key: key,
|
|
635
|
+
...(conditional.metadata !== undefined && {
|
|
636
|
+
Metadata: conditional.metadata,
|
|
170
637
|
}),
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
CopySourceIfMatch: promotion.sourceEtag,
|
|
178
|
-
}),
|
|
179
|
-
Key: destinationKey,
|
|
180
|
-
}), signal === undefined ? undefined : { abortSignal: signal });
|
|
181
|
-
});
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
if (!supportsConditionalMutation) {
|
|
185
|
-
return common;
|
|
186
|
-
}
|
|
187
|
-
return Object.assign(common, {
|
|
188
|
-
conditionalMutation: Object.freeze({
|
|
189
|
-
create: true,
|
|
190
|
-
delete: true,
|
|
191
|
-
etag: true,
|
|
192
|
-
replace: true,
|
|
638
|
+
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
639
|
+
const size = normalized.size();
|
|
640
|
+
conditional.onProgress?.({
|
|
641
|
+
loaded: size,
|
|
642
|
+
...(normalized.contentLength !== undefined && {
|
|
643
|
+
total: normalized.contentLength,
|
|
193
644
|
}),
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
645
|
+
});
|
|
646
|
+
const etag = providerEtag(result.ETag, key, 'upload');
|
|
647
|
+
if (etag === undefined)
|
|
648
|
+
throw missingResultEtag(key);
|
|
649
|
+
return { contentType, etag, key, size };
|
|
650
|
+
}
|
|
651
|
+
async function uploadConditionalMultipart(base, key, body, conditional) {
|
|
652
|
+
const configuredPartSize = typeof conditional.multipart === 'object'
|
|
653
|
+
? conditional.multipart.partSize
|
|
654
|
+
: undefined;
|
|
655
|
+
if (configuredPartSize !== undefined &&
|
|
656
|
+
(!Number.isSafeInteger(configuredPartSize) || configuredPartSize <= 0)) {
|
|
657
|
+
throw new StorageError('multipart.partSize must be a positive safe integer.', {
|
|
658
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
659
|
+
key,
|
|
660
|
+
operation: 'upload',
|
|
661
|
+
permanent: true,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const partSize = Math.max(S3_MIN_MULTIPART_PART_BYTES, configuredPartSize ?? S3_MIN_MULTIPART_PART_BYTES);
|
|
665
|
+
const normalized = await normalizeConditionalBody(body, undefined);
|
|
666
|
+
if (normalized.contentLength === 0) {
|
|
667
|
+
return uploadConditionalSingle(base, key, new Uint8Array(), {
|
|
668
|
+
...conditional,
|
|
669
|
+
multipart: false,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const contentType = contentTypeOf(body, conditional.contentType);
|
|
673
|
+
let uploadId;
|
|
674
|
+
let size = 0;
|
|
675
|
+
try {
|
|
676
|
+
const created = await withS3Retry(conditional, 'upload', (signal) => base.raw.send(new CreateMultipartUploadCommand({
|
|
677
|
+
Bucket: base.bucket,
|
|
678
|
+
...(conditional.cacheControl !== undefined && {
|
|
679
|
+
CacheControl: conditional.cacheControl,
|
|
680
|
+
}),
|
|
681
|
+
ContentType: contentType,
|
|
682
|
+
Key: key,
|
|
683
|
+
...(conditional.metadata !== undefined && {
|
|
684
|
+
Metadata: conditional.metadata,
|
|
685
|
+
}),
|
|
686
|
+
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
687
|
+
uploadId = created.UploadId;
|
|
688
|
+
if (uploadId === undefined || uploadId.length === 0) {
|
|
689
|
+
throw new StorageError('S3 did not return a multipart upload ID.', {
|
|
690
|
+
code: StorageErrorCode.PROVIDER,
|
|
691
|
+
key,
|
|
692
|
+
operation: 'upload',
|
|
693
|
+
permanent: true,
|
|
201
694
|
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
695
|
+
}
|
|
696
|
+
const completedParts = [];
|
|
697
|
+
let partNumber = 0;
|
|
698
|
+
for await (const part of multipartParts(normalized.body, partSize)) {
|
|
699
|
+
partNumber += 1;
|
|
700
|
+
if (partNumber > S3_MAX_MULTIPART_PARTS) {
|
|
701
|
+
throw new StorageError(`Conditional multipart upload exceeds the ${S3_MAX_MULTIPART_PARTS}-part S3 limit.`, {
|
|
702
|
+
code: StorageErrorCode.LIMIT_EXCEEDED,
|
|
208
703
|
key,
|
|
209
704
|
operation: 'upload',
|
|
210
705
|
permanent: true,
|
|
211
706
|
});
|
|
212
707
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
708
|
+
const uploaded = await withS3Retry(conditional, 'upload', (signal) => base.raw.send(new UploadPartCommand({
|
|
709
|
+
Body: part,
|
|
710
|
+
Bucket: base.bucket,
|
|
711
|
+
ContentLength: part.byteLength,
|
|
712
|
+
Key: key,
|
|
713
|
+
PartNumber: partNumber,
|
|
714
|
+
UploadId: uploadId,
|
|
715
|
+
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
716
|
+
const partEtag = providerEtag(uploaded.ETag, key, 'upload');
|
|
717
|
+
if (partEtag === undefined) {
|
|
718
|
+
throw new StorageError('S3 upload part returned no ETag.', {
|
|
719
|
+
code: StorageErrorCode.PROVIDER,
|
|
216
720
|
key,
|
|
217
721
|
operation: 'upload',
|
|
218
722
|
permanent: true,
|
|
219
723
|
});
|
|
220
724
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const result = await withS3Retry(retryOptions, (signal) => base.raw.send(new PutObjectCommand({
|
|
227
|
-
Body: normalized.body,
|
|
228
|
-
Bucket: base.bucket,
|
|
229
|
-
...(conditional.cacheControl !== undefined && {
|
|
230
|
-
CacheControl: conditional.cacheControl,
|
|
231
|
-
}),
|
|
232
|
-
...(normalized.contentLength !== undefined && {
|
|
233
|
-
ContentLength: normalized.contentLength,
|
|
234
|
-
}),
|
|
235
|
-
ContentType: contentType,
|
|
236
|
-
...(conditional.condition.type === 'create'
|
|
237
|
-
? { IfNoneMatch: '*' }
|
|
238
|
-
: { IfMatch: etagHeader(conditional.condition.etag) }),
|
|
239
|
-
Key: key,
|
|
240
|
-
...(conditional.metadata !== undefined && {
|
|
241
|
-
Metadata: conditional.metadata,
|
|
242
|
-
}),
|
|
243
|
-
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
244
|
-
const size = normalized.size();
|
|
725
|
+
completedParts.push({
|
|
726
|
+
ETag: storageEtagHeader(partEtag),
|
|
727
|
+
PartNumber: partNumber,
|
|
728
|
+
});
|
|
729
|
+
size += part.byteLength;
|
|
245
730
|
conditional.onProgress?.({
|
|
246
731
|
loaded: size,
|
|
247
732
|
...(normalized.contentLength !== undefined && {
|
|
248
733
|
total: normalized.contentLength,
|
|
249
734
|
}),
|
|
250
735
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
736
|
+
}
|
|
737
|
+
if (completedParts.length === 0) {
|
|
738
|
+
await base.raw.send(new AbortMultipartUploadCommand({
|
|
739
|
+
Bucket: base.bucket,
|
|
740
|
+
Key: key,
|
|
741
|
+
UploadId: uploadId,
|
|
742
|
+
}));
|
|
743
|
+
uploadId = undefined;
|
|
744
|
+
return uploadConditionalSingle(base, key, new Uint8Array(), {
|
|
745
|
+
...conditional,
|
|
746
|
+
multipart: false,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const completed = await withS3Retry(conditional, 'upload', (signal) => base.raw.send(new CompleteMultipartUploadCommand({
|
|
750
|
+
Bucket: base.bucket,
|
|
751
|
+
...conditionalHeaders(conditional.condition, key),
|
|
752
|
+
Key: key,
|
|
753
|
+
MultipartUpload: { Parts: completedParts },
|
|
754
|
+
UploadId: uploadId,
|
|
755
|
+
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
756
|
+
const etag = providerEtag(completed.ETag, key, 'upload');
|
|
757
|
+
if (etag === undefined)
|
|
758
|
+
throw missingResultEtag(key);
|
|
759
|
+
uploadId = undefined;
|
|
760
|
+
return { contentType, etag, key, size };
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
if (uploadId !== undefined) {
|
|
764
|
+
await base.raw
|
|
765
|
+
.send(new AbortMultipartUploadCommand({
|
|
766
|
+
Bucket: base.bucket,
|
|
767
|
+
Key: key,
|
|
768
|
+
UploadId: uploadId,
|
|
769
|
+
}))
|
|
770
|
+
.catch(() => undefined);
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const configuredS3Clients = new WeakSet();
|
|
776
|
+
/** Constructs the upstream S3 adapter while retaining security-relevant metadata. */
|
|
777
|
+
export function s3(options) {
|
|
778
|
+
const adapter = createFilesSdkS3Adapter(options);
|
|
779
|
+
markFilesSdkS3AdapterUndecorated(adapter);
|
|
780
|
+
recordS3ConstructionMetadata(adapter.raw, {
|
|
781
|
+
publicBaseUrlConfigured: options.publicBaseUrl !== undefined,
|
|
782
|
+
});
|
|
783
|
+
return adapter;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Adds only the exact operations declared by one verified provider profile.
|
|
787
|
+
* Each raw adapter may be decorated once so capability state cannot depend on
|
|
788
|
+
* profile application order. A native AWS SDK endpoint also bounds every
|
|
789
|
+
* explicit profile by the built-in AWS capability ceiling.
|
|
790
|
+
*/
|
|
791
|
+
export function withS3Capabilities(base, options = {}) {
|
|
792
|
+
const raw = base.raw;
|
|
793
|
+
if (configuredS3Clients.has(raw)) {
|
|
794
|
+
throw new TypeError('withS3Capabilities may only be applied once to an S3 adapter.');
|
|
795
|
+
}
|
|
796
|
+
assertFilesSdkS3AdapterHasNoReservedExtensions(base);
|
|
797
|
+
const sdkHasCustomEndpoint = raw.config.isCustomEndpoint === true;
|
|
798
|
+
if (options.endpoint !== undefined && !sdkHasCustomEndpoint) {
|
|
799
|
+
throw new TypeError('The declared S3 endpoint does not match the SDK client endpoint provenance.');
|
|
800
|
+
}
|
|
801
|
+
if (!sdkHasCustomEndpoint) {
|
|
802
|
+
// files-sdk does not expose this S3Client constructor option. Endpoint
|
|
803
|
+
// resolution is lazy, so fixing the resolved SDK config before the adapter
|
|
804
|
+
// is exposed prevents environment and shared-config endpoint redirection.
|
|
805
|
+
raw.config.ignoreConfiguredEndpointUrls = true;
|
|
806
|
+
}
|
|
807
|
+
const inferredNativeAws = base.name === 's3' && !sdkHasCustomEndpoint;
|
|
808
|
+
const profile = options.providerProfile ??
|
|
809
|
+
(inferredNativeAws
|
|
810
|
+
? AWS_S3_PROVIDER_PROFILE
|
|
811
|
+
: UNVERIFIED_S3_PROVIDER_PROFILE);
|
|
812
|
+
assertVerifiedS3ProviderProfile(profile);
|
|
813
|
+
if (!sdkHasCustomEndpoint) {
|
|
814
|
+
assertS3ProviderProfileContainedBy(profile, AWS_S3_PROVIDER_PROFILE);
|
|
815
|
+
}
|
|
816
|
+
const provenance = options.providerProfile === undefined
|
|
817
|
+
? inferredNativeAws
|
|
818
|
+
? 'native'
|
|
819
|
+
: 'unverified'
|
|
820
|
+
: inferredNativeAws
|
|
821
|
+
? 'native'
|
|
822
|
+
: 'verified';
|
|
823
|
+
const constructionMetadata = getS3ConstructionMetadata(raw);
|
|
824
|
+
const publicBaseUrlConfigured = options.publicBaseUrl !== undefined ||
|
|
825
|
+
constructionMetadata?.publicBaseUrlConfigured === true;
|
|
826
|
+
const bucket = base.bucket;
|
|
827
|
+
const requestAdapter = { bucket, raw };
|
|
828
|
+
const signedUploadUrl = async (key, signOptions) => {
|
|
829
|
+
if (signOptions.minSize !== undefined &&
|
|
830
|
+
signOptions.maxSize === undefined) {
|
|
831
|
+
throw new StorageError('S3 signed uploads cannot enforce minSize without maxSize.', {
|
|
832
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
833
|
+
key,
|
|
834
|
+
operation: 'signUpload',
|
|
835
|
+
permanent: true,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (signOptions.maxSize !== undefined && key.endsWith('${filename}')) {
|
|
839
|
+
throw new StorageError('Signed POST keys must not end with the AWS ${filename} template.', {
|
|
840
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
841
|
+
key,
|
|
842
|
+
operation: 'signUpload',
|
|
843
|
+
permanent: true,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (signOptions.contentType !== undefined &&
|
|
847
|
+
profile.signedUploadPolicy.contentType !== true) {
|
|
848
|
+
throw new StorageError(`S3 provider profile "${profile.name}" cannot enforce signed-upload contentType.`, {
|
|
849
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
850
|
+
key,
|
|
851
|
+
operation: 'signUpload',
|
|
852
|
+
permanent: true,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (signOptions.maxSize !== undefined &&
|
|
856
|
+
profile.signedUploadPolicy.sizeRange !== true) {
|
|
857
|
+
throw new StorageError(`S3 provider profile "${profile.name}" cannot enforce signed-upload sizeRange.`, {
|
|
858
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
859
|
+
key,
|
|
860
|
+
operation: 'signUpload',
|
|
861
|
+
permanent: true,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
if (signOptions.maxSize !== undefined) {
|
|
866
|
+
const conditions = [
|
|
867
|
+
[
|
|
868
|
+
'content-length-range',
|
|
869
|
+
signOptions.minSize ?? 1,
|
|
870
|
+
signOptions.maxSize,
|
|
871
|
+
],
|
|
872
|
+
];
|
|
873
|
+
if (signOptions.contentType !== undefined) {
|
|
874
|
+
conditions.push(['eq', '$Content-Type', signOptions.contentType]);
|
|
875
|
+
}
|
|
876
|
+
const post = await createPresignedPost(raw, {
|
|
877
|
+
Bucket: bucket,
|
|
878
|
+
Conditions: conditions,
|
|
879
|
+
Expires: signOptions.expiresIn,
|
|
880
|
+
Key: key,
|
|
881
|
+
...(signOptions.contentType !== undefined && {
|
|
882
|
+
Fields: { 'Content-Type': signOptions.contentType },
|
|
883
|
+
}),
|
|
258
884
|
});
|
|
885
|
+
return { fields: post.fields, method: 'POST', url: post.url };
|
|
259
886
|
}
|
|
887
|
+
const url = await getSignedUrl(raw, new PutObjectCommand({
|
|
888
|
+
Bucket: bucket,
|
|
889
|
+
Key: key,
|
|
890
|
+
...(signOptions.contentType !== undefined && {
|
|
891
|
+
ContentType: signOptions.contentType,
|
|
892
|
+
}),
|
|
893
|
+
}), {
|
|
894
|
+
expiresIn: signOptions.expiresIn,
|
|
895
|
+
...(signOptions.contentType !== undefined && {
|
|
896
|
+
signableHeaders: new Set(['content-type']),
|
|
897
|
+
}),
|
|
898
|
+
});
|
|
260
899
|
return {
|
|
261
|
-
contentType
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
900
|
+
...(signOptions.contentType !== undefined && {
|
|
901
|
+
headers: { 'Content-Type': signOptions.contentType },
|
|
902
|
+
}),
|
|
903
|
+
method: 'PUT',
|
|
904
|
+
url,
|
|
265
905
|
};
|
|
266
|
-
}
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
throw mapS3Error(error);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const adapter = Object.assign(base, {
|
|
912
|
+
physicalKey: profile.physicalKey,
|
|
913
|
+
signedUploadPolicy: profile.signedUploadPolicy,
|
|
914
|
+
signedUploadUrl,
|
|
915
|
+
signedDownloadPolicy: Object.freeze({
|
|
916
|
+
expiresIn: constructionMetadata !== undefined && !publicBaseUrlConfigured,
|
|
917
|
+
}),
|
|
267
918
|
});
|
|
919
|
+
if (profile.conditionalCopySource !== undefined ||
|
|
920
|
+
profile.conditionalCopyDestination !== undefined) {
|
|
921
|
+
Object.assign(adapter, {
|
|
922
|
+
...(profile.conditionalCopySource !== undefined && {
|
|
923
|
+
conditionalCopySource: profile.conditionalCopySource,
|
|
924
|
+
}),
|
|
925
|
+
...(profile.conditionalCopyDestination !== undefined && {
|
|
926
|
+
conditionalCopyDestination: profile.conditionalCopyDestination,
|
|
927
|
+
}),
|
|
928
|
+
async promote(sourceKey, destinationKey, promotion) {
|
|
929
|
+
const destination = promotion.destination;
|
|
930
|
+
if (destination !== undefined &&
|
|
931
|
+
(typeof destination !== 'object' ||
|
|
932
|
+
destination === null ||
|
|
933
|
+
(destination.type !== 'create' && destination.type !== 'replace'))) {
|
|
934
|
+
throw new StorageError('destination.type must be "create" or "replace".', {
|
|
935
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
936
|
+
key: destinationKey,
|
|
937
|
+
operation: 'promote',
|
|
938
|
+
permanent: true,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if ((promotion.sourceEtag !== undefined &&
|
|
942
|
+
profile.conditionalCopySource?.etag !== true) ||
|
|
943
|
+
(promotion.sourceVersion !== undefined &&
|
|
944
|
+
profile.conditionalCopySource?.version !== true) ||
|
|
945
|
+
(promotion.destination?.type === 'create' &&
|
|
946
|
+
profile.conditionalCopyDestination?.create !== true) ||
|
|
947
|
+
(promotion.destination?.type === 'replace' &&
|
|
948
|
+
profile.conditionalCopyDestination?.replace !== true) ||
|
|
949
|
+
(promotion.destination !== undefined &&
|
|
950
|
+
(promotion.sourceEtag !== undefined ||
|
|
951
|
+
promotion.sourceVersion !== undefined) &&
|
|
952
|
+
profile.conditionalCopyDestination?.atomicWithSource !== true)) {
|
|
953
|
+
throw new StorageError(`S3 provider profile "${profile.name}" does not support the requested conditional promotion.`, {
|
|
954
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
955
|
+
key: sourceKey,
|
|
956
|
+
operation: 'promote',
|
|
957
|
+
permanent: true,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
await withS3Retry(promotion, 'copy', async (signal) => {
|
|
961
|
+
await raw.send(new CopyObjectCommand({
|
|
962
|
+
Bucket: bucket,
|
|
963
|
+
CopySource: copySource(bucket, sourceKey, promotion.sourceVersion),
|
|
964
|
+
...(promotion.sourceEtag !== undefined && {
|
|
965
|
+
CopySourceIfMatch: etagHeader(promotion.sourceEtag, sourceKey, 'copy', 'sourceEtag'),
|
|
966
|
+
}),
|
|
967
|
+
...(promotion.destination?.type === 'create'
|
|
968
|
+
? { IfNoneMatch: '*' }
|
|
969
|
+
: promotion.destination?.type === 'replace'
|
|
970
|
+
? {
|
|
971
|
+
IfMatch: etagHeader(promotion.destination.etag, destinationKey, 'copy', 'destination.etag'),
|
|
972
|
+
}
|
|
973
|
+
: {}),
|
|
974
|
+
Key: destinationKey,
|
|
975
|
+
}), signal === undefined ? undefined : { abortSignal: signal });
|
|
976
|
+
});
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (profile.conditionalRead !== undefined) {
|
|
981
|
+
Object.assign(adapter, {
|
|
982
|
+
conditionalRead: profile.conditionalRead,
|
|
983
|
+
async downloadConditional(key, options) {
|
|
984
|
+
if ((options.condition.etag !== undefined &&
|
|
985
|
+
profile.conditionalRead?.etag !== true) ||
|
|
986
|
+
(options.condition.version !== undefined &&
|
|
987
|
+
profile.conditionalRead?.version !== true)) {
|
|
988
|
+
throw new StorageError(`S3 provider profile "${profile.name}" does not support the requested conditional read.`, {
|
|
989
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
990
|
+
key,
|
|
991
|
+
operation: 'download',
|
|
992
|
+
permanent: true,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
const result = await withS3Retry(options, 'download', (signal) => raw.send(new GetObjectCommand({
|
|
996
|
+
Bucket: bucket,
|
|
997
|
+
...(options.condition.etag !== undefined && {
|
|
998
|
+
IfMatch: etagHeader(options.condition.etag, key, 'download', 'condition.etag'),
|
|
999
|
+
}),
|
|
1000
|
+
Key: key,
|
|
1001
|
+
...(rangeHeader(options.range) !== undefined && {
|
|
1002
|
+
Range: rangeHeader(options.range),
|
|
1003
|
+
}),
|
|
1004
|
+
...(options.condition.version !== undefined && {
|
|
1005
|
+
VersionId: options.condition.version,
|
|
1006
|
+
}),
|
|
1007
|
+
}), signal === undefined ? undefined : { abortSignal: signal }));
|
|
1008
|
+
const etag = providerEtag(result.ETag, key, 'download');
|
|
1009
|
+
return {
|
|
1010
|
+
body: s3ResponseStream(result.Body),
|
|
1011
|
+
contentType: result.ContentType ?? 'application/octet-stream',
|
|
1012
|
+
...(etag !== undefined && { etag }),
|
|
1013
|
+
key,
|
|
1014
|
+
...(result.LastModified !== undefined && {
|
|
1015
|
+
lastModified: result.LastModified,
|
|
1016
|
+
}),
|
|
1017
|
+
...(result.Metadata !== undefined && { metadata: result.Metadata }),
|
|
1018
|
+
name: key.split('/').at(-1) ?? key,
|
|
1019
|
+
size: Number(result.ContentLength ?? 0),
|
|
1020
|
+
};
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (profile.conditionalDelete !== undefined) {
|
|
1025
|
+
Object.assign(adapter, {
|
|
1026
|
+
conditionalDelete: profile.conditionalDelete,
|
|
1027
|
+
async deleteConditional(key, options) {
|
|
1028
|
+
await withS3Retry(options, 'delete', async (signal) => {
|
|
1029
|
+
await raw.send(new DeleteObjectCommand({
|
|
1030
|
+
Bucket: bucket,
|
|
1031
|
+
IfMatch: etagHeader(options.condition.etag, key, 'delete', 'condition.etag'),
|
|
1032
|
+
Key: key,
|
|
1033
|
+
}), signal === undefined ? undefined : { abortSignal: signal });
|
|
1034
|
+
});
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (profile.conditionalCreate !== undefined ||
|
|
1039
|
+
profile.conditionalReplace !== undefined) {
|
|
1040
|
+
Object.assign(adapter, {
|
|
1041
|
+
...(profile.conditionalCreate !== undefined && {
|
|
1042
|
+
conditionalCreate: profile.conditionalCreate,
|
|
1043
|
+
}),
|
|
1044
|
+
...(profile.conditionalReplace !== undefined && {
|
|
1045
|
+
conditionalReplace: profile.conditionalReplace,
|
|
1046
|
+
}),
|
|
1047
|
+
...(profile.conditionalMultipartCompletion !== undefined && {
|
|
1048
|
+
conditionalMultipartCompletion: profile.conditionalMultipartCompletion,
|
|
1049
|
+
}),
|
|
1050
|
+
async uploadConditional(key, body, conditional) {
|
|
1051
|
+
if (conditional.condition.type === 'replace') {
|
|
1052
|
+
etagHeader(conditional.condition.etag, key, 'upload', 'condition.etag');
|
|
1053
|
+
}
|
|
1054
|
+
const supported = conditional.condition.type === 'create'
|
|
1055
|
+
? profile.conditionalCreate !== undefined
|
|
1056
|
+
: profile.conditionalReplace !== undefined;
|
|
1057
|
+
const multipartRequested = conditional.multipart !== undefined &&
|
|
1058
|
+
conditional.multipart !== false;
|
|
1059
|
+
const multipartSupported = !multipartRequested ||
|
|
1060
|
+
(conditional.condition.type === 'create'
|
|
1061
|
+
? profile.conditionalMultipartCompletion?.create === true
|
|
1062
|
+
: profile.conditionalMultipartCompletion?.replace === true);
|
|
1063
|
+
if (!supported || !multipartSupported) {
|
|
1064
|
+
throw new StorageError(`S3 provider profile "${profile.name}" does not support the requested conditional upload.`, {
|
|
1065
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
1066
|
+
key,
|
|
1067
|
+
operation: 'upload',
|
|
1068
|
+
permanent: true,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (conditional.control !== undefined) {
|
|
1072
|
+
throw new StorageError('Conditional S3 uploads do not support resumable control.', {
|
|
1073
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
1074
|
+
key,
|
|
1075
|
+
operation: 'upload',
|
|
1076
|
+
permanent: true,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return multipartRequested
|
|
1080
|
+
? uploadConditionalMultipart(requestAdapter, key, body, conditional)
|
|
1081
|
+
: uploadConditionalSingle(requestAdapter, key, body, conditional);
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
Object.freeze(adapter);
|
|
1086
|
+
markFilesSdkS3RawClientProvenance(raw, adapter, provenance);
|
|
1087
|
+
configuredS3Clients.add(raw);
|
|
1088
|
+
return adapter;
|
|
268
1089
|
}
|
|
269
1090
|
/**
|
|
270
1091
|
* Creates the files-sdk S3 driver from the storage package's own dependency
|
|
271
|
-
* context and
|
|
1092
|
+
* context and exposes only the operations in its verified provider profile.
|
|
272
1093
|
*/
|
|
273
1094
|
export function createS3StorageDriver(options) {
|
|
274
1095
|
const { adapter: adapterOptions, ...filesOptions } = options;
|
|
275
|
-
const {
|
|
1096
|
+
const { providerProfile, ...plainFilesOptions } = filesOptions;
|
|
1097
|
+
const unverifiedCustomEndpoint = adapterOptions.endpoint !== undefined && providerProfile === undefined;
|
|
276
1098
|
return createFilesSdkDriver({
|
|
277
1099
|
...plainFilesOptions,
|
|
1100
|
+
...(unverifiedCustomEndpoint && { readonly: true }),
|
|
278
1101
|
adapter: withS3Capabilities(s3(adapterOptions), {
|
|
279
1102
|
...adapterOptions,
|
|
280
|
-
...(
|
|
1103
|
+
...(providerProfile !== undefined && { providerProfile }),
|
|
281
1104
|
}),
|
|
282
1105
|
});
|
|
283
1106
|
}
|
|
284
|
-
export { mapS3Error
|
|
1107
|
+
export { mapS3Error };
|
|
285
1108
|
//# sourceMappingURL=index.js.map
|