@chidchanun/bcp 0.1.26 → 0.1.27
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 +176 -518
- package/docs/README.md +71 -59
- package/docs/releases/0.1.27.md +350 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +33 -2
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CopyObjectCommand,
|
|
3
|
+
DeleteObjectsCommand,
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
HeadObjectCommand,
|
|
6
|
+
ListObjectsV2Command,
|
|
7
|
+
PutObjectCommand,
|
|
8
|
+
} from "@aws-sdk/client-s3";
|
|
9
|
+
import {
|
|
10
|
+
getSignedUrl,
|
|
11
|
+
} from "@aws-sdk/s3-request-presigner";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createS3Storage as createBaseS3Storage,
|
|
15
|
+
type S3StorageAdapter,
|
|
16
|
+
type S3StorageOptions,
|
|
17
|
+
} from "./storage-s3.js";
|
|
18
|
+
import {
|
|
19
|
+
normalizeStorageKey,
|
|
20
|
+
StorageError,
|
|
21
|
+
type StorageObjectMetadata,
|
|
22
|
+
} from "./storage.js";
|
|
23
|
+
import {
|
|
24
|
+
normalizeStorageListLimit,
|
|
25
|
+
normalizeStorageListPrefix,
|
|
26
|
+
normalizeStorageSignedUrlExpiry,
|
|
27
|
+
normalizeStorageUserMetadata,
|
|
28
|
+
type StorageCopyOptions,
|
|
29
|
+
type StorageDeleteManyFailure,
|
|
30
|
+
type StorageDeleteManyResult,
|
|
31
|
+
type StorageEcosystemAdapter,
|
|
32
|
+
type StorageEcosystemPutOptions,
|
|
33
|
+
type StorageEcosystemPutStreamOptions,
|
|
34
|
+
type StorageListOptions,
|
|
35
|
+
type StorageListResult,
|
|
36
|
+
type StorageUserMetadata,
|
|
37
|
+
} from "./storage-ecosystem.js";
|
|
38
|
+
|
|
39
|
+
const S3_DELETE_BATCH_SIZE =
|
|
40
|
+
1000;
|
|
41
|
+
|
|
42
|
+
export type S3StorageEcosystemAdapter =
|
|
43
|
+
Omit<
|
|
44
|
+
S3StorageAdapter,
|
|
45
|
+
"put" |
|
|
46
|
+
"putStream"
|
|
47
|
+
> &
|
|
48
|
+
StorageEcosystemAdapter;
|
|
49
|
+
|
|
50
|
+
export function createS3Storage(
|
|
51
|
+
options: S3StorageOptions
|
|
52
|
+
): S3StorageEcosystemAdapter {
|
|
53
|
+
const base =
|
|
54
|
+
createBaseS3Storage(
|
|
55
|
+
options
|
|
56
|
+
);
|
|
57
|
+
const adapter =
|
|
58
|
+
base as unknown as
|
|
59
|
+
S3StorageEcosystemAdapter;
|
|
60
|
+
const bucket =
|
|
61
|
+
assertNonEmptyString(
|
|
62
|
+
options.bucket,
|
|
63
|
+
"bucket"
|
|
64
|
+
);
|
|
65
|
+
const prefix =
|
|
66
|
+
normalizeS3Prefix(
|
|
67
|
+
options.prefix
|
|
68
|
+
);
|
|
69
|
+
const basePut =
|
|
70
|
+
base.put.bind(
|
|
71
|
+
base
|
|
72
|
+
);
|
|
73
|
+
const basePutStream =
|
|
74
|
+
base.putStream?.bind(
|
|
75
|
+
base
|
|
76
|
+
);
|
|
77
|
+
const baseDelete =
|
|
78
|
+
base.delete.bind(
|
|
79
|
+
base
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
adapter.capabilities = {
|
|
83
|
+
...base.capabilities,
|
|
84
|
+
listing: true,
|
|
85
|
+
signedUrls: true,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
adapter.put =
|
|
89
|
+
async (
|
|
90
|
+
key,
|
|
91
|
+
value,
|
|
92
|
+
putOptions:
|
|
93
|
+
StorageEcosystemPutOptions = {}
|
|
94
|
+
) => {
|
|
95
|
+
const stored =
|
|
96
|
+
await basePut(
|
|
97
|
+
key,
|
|
98
|
+
value,
|
|
99
|
+
putOptions
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
putOptions.metadata !==
|
|
104
|
+
undefined
|
|
105
|
+
) {
|
|
106
|
+
await setS3Metadata(
|
|
107
|
+
adapter,
|
|
108
|
+
bucket,
|
|
109
|
+
prefix,
|
|
110
|
+
stored.key,
|
|
111
|
+
putOptions.metadata
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
await adapter.stat(
|
|
117
|
+
stored.key
|
|
118
|
+
)
|
|
119
|
+
) ?? stored;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (basePutStream) {
|
|
123
|
+
adapter.putStream =
|
|
124
|
+
async (
|
|
125
|
+
key,
|
|
126
|
+
stream,
|
|
127
|
+
putOptions:
|
|
128
|
+
StorageEcosystemPutStreamOptions = {}
|
|
129
|
+
) => {
|
|
130
|
+
const stored =
|
|
131
|
+
await basePutStream(
|
|
132
|
+
key,
|
|
133
|
+
stream,
|
|
134
|
+
putOptions
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
putOptions.metadata !==
|
|
139
|
+
undefined
|
|
140
|
+
) {
|
|
141
|
+
await setS3Metadata(
|
|
142
|
+
adapter,
|
|
143
|
+
bucket,
|
|
144
|
+
prefix,
|
|
145
|
+
stored.key,
|
|
146
|
+
putOptions.metadata
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
await adapter.stat(
|
|
152
|
+
stored.key
|
|
153
|
+
)
|
|
154
|
+
) ?? stored;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
adapter.getMetadata =
|
|
159
|
+
(key) =>
|
|
160
|
+
getS3Metadata(
|
|
161
|
+
adapter,
|
|
162
|
+
bucket,
|
|
163
|
+
prefix,
|
|
164
|
+
key
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
adapter.setMetadata =
|
|
168
|
+
(
|
|
169
|
+
key,
|
|
170
|
+
metadata
|
|
171
|
+
) =>
|
|
172
|
+
setS3Metadata(
|
|
173
|
+
adapter,
|
|
174
|
+
bucket,
|
|
175
|
+
prefix,
|
|
176
|
+
key,
|
|
177
|
+
metadata
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
adapter.list =
|
|
181
|
+
(listOptions = {}) =>
|
|
182
|
+
listS3Objects(
|
|
183
|
+
adapter,
|
|
184
|
+
bucket,
|
|
185
|
+
prefix,
|
|
186
|
+
listOptions
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
adapter.copy =
|
|
190
|
+
(
|
|
191
|
+
sourceKey,
|
|
192
|
+
destinationKey,
|
|
193
|
+
copyOptions = {}
|
|
194
|
+
) =>
|
|
195
|
+
copyS3Object(
|
|
196
|
+
adapter,
|
|
197
|
+
bucket,
|
|
198
|
+
prefix,
|
|
199
|
+
sourceKey,
|
|
200
|
+
destinationKey,
|
|
201
|
+
copyOptions
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
adapter.move =
|
|
205
|
+
async (
|
|
206
|
+
sourceKey,
|
|
207
|
+
destinationKey,
|
|
208
|
+
copyOptions = {}
|
|
209
|
+
) => {
|
|
210
|
+
const source =
|
|
211
|
+
normalizeStorageKey(
|
|
212
|
+
sourceKey
|
|
213
|
+
);
|
|
214
|
+
const destination =
|
|
215
|
+
normalizeStorageKey(
|
|
216
|
+
destinationKey
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (source === destination) {
|
|
220
|
+
return requireS3Object(
|
|
221
|
+
adapter,
|
|
222
|
+
source
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const stored =
|
|
227
|
+
await copyS3Object(
|
|
228
|
+
adapter,
|
|
229
|
+
bucket,
|
|
230
|
+
prefix,
|
|
231
|
+
source,
|
|
232
|
+
destination,
|
|
233
|
+
copyOptions
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await baseDelete(
|
|
237
|
+
source
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return stored;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
adapter.deleteMany =
|
|
244
|
+
(keys) =>
|
|
245
|
+
deleteS3Objects(
|
|
246
|
+
adapter,
|
|
247
|
+
bucket,
|
|
248
|
+
prefix,
|
|
249
|
+
keys
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
adapter.createSignedReadUrl =
|
|
253
|
+
async (
|
|
254
|
+
key,
|
|
255
|
+
signedOptions = {}
|
|
256
|
+
) => {
|
|
257
|
+
const resolved =
|
|
258
|
+
resolveS3Key(
|
|
259
|
+
prefix,
|
|
260
|
+
key
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
return getSignedUrl(
|
|
264
|
+
adapter.client,
|
|
265
|
+
new GetObjectCommand({
|
|
266
|
+
Bucket:
|
|
267
|
+
bucket,
|
|
268
|
+
Key:
|
|
269
|
+
resolved.providerKey,
|
|
270
|
+
}),
|
|
271
|
+
{
|
|
272
|
+
expiresIn:
|
|
273
|
+
normalizeStorageSignedUrlExpiry(
|
|
274
|
+
signedOptions.expiresIn
|
|
275
|
+
),
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
adapter.createSignedWriteUrl =
|
|
281
|
+
async (
|
|
282
|
+
key,
|
|
283
|
+
signedOptions = {}
|
|
284
|
+
) => {
|
|
285
|
+
const resolved =
|
|
286
|
+
resolveS3Key(
|
|
287
|
+
prefix,
|
|
288
|
+
key
|
|
289
|
+
);
|
|
290
|
+
const metadata =
|
|
291
|
+
normalizeStorageUserMetadata(
|
|
292
|
+
signedOptions.metadata
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
return getSignedUrl(
|
|
296
|
+
adapter.client,
|
|
297
|
+
new PutObjectCommand({
|
|
298
|
+
Bucket:
|
|
299
|
+
bucket,
|
|
300
|
+
Key:
|
|
301
|
+
resolved.providerKey,
|
|
302
|
+
...(signedOptions.contentType
|
|
303
|
+
? {
|
|
304
|
+
ContentType:
|
|
305
|
+
normalizeContentType(
|
|
306
|
+
signedOptions.contentType
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
310
|
+
...(Object.keys(
|
|
311
|
+
metadata
|
|
312
|
+
).length > 0
|
|
313
|
+
? {
|
|
314
|
+
Metadata:
|
|
315
|
+
metadata,
|
|
316
|
+
}
|
|
317
|
+
: {}),
|
|
318
|
+
}),
|
|
319
|
+
{
|
|
320
|
+
expiresIn:
|
|
321
|
+
normalizeStorageSignedUrlExpiry(
|
|
322
|
+
signedOptions.expiresIn
|
|
323
|
+
),
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return adapter;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function listS3Objects(
|
|
332
|
+
adapter: S3StorageEcosystemAdapter,
|
|
333
|
+
bucket: string,
|
|
334
|
+
prefix: string,
|
|
335
|
+
options: StorageListOptions
|
|
336
|
+
): Promise<StorageListResult> {
|
|
337
|
+
const logicalPrefix =
|
|
338
|
+
normalizeStorageListPrefix(
|
|
339
|
+
options.prefix
|
|
340
|
+
);
|
|
341
|
+
const providerPrefix =
|
|
342
|
+
combineS3Prefix(
|
|
343
|
+
prefix,
|
|
344
|
+
logicalPrefix
|
|
345
|
+
);
|
|
346
|
+
const response =
|
|
347
|
+
await adapter.client.send(
|
|
348
|
+
new ListObjectsV2Command({
|
|
349
|
+
Bucket:
|
|
350
|
+
bucket,
|
|
351
|
+
...(providerPrefix
|
|
352
|
+
? {
|
|
353
|
+
Prefix:
|
|
354
|
+
providerPrefix,
|
|
355
|
+
}
|
|
356
|
+
: {}),
|
|
357
|
+
MaxKeys:
|
|
358
|
+
normalizeStorageListLimit(
|
|
359
|
+
options.limit
|
|
360
|
+
),
|
|
361
|
+
...(options.cursor
|
|
362
|
+
? {
|
|
363
|
+
ContinuationToken:
|
|
364
|
+
options.cursor,
|
|
365
|
+
}
|
|
366
|
+
: {}),
|
|
367
|
+
})
|
|
368
|
+
);
|
|
369
|
+
const objects:
|
|
370
|
+
StorageObjectMetadata[] = [];
|
|
371
|
+
|
|
372
|
+
for (
|
|
373
|
+
const object
|
|
374
|
+
of response.Contents ?? []
|
|
375
|
+
) {
|
|
376
|
+
if (!object.Key) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const logicalKey =
|
|
381
|
+
toLogicalS3Key(
|
|
382
|
+
prefix,
|
|
383
|
+
object.Key
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (!logicalKey) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const size =
|
|
391
|
+
object.Size ?? 0;
|
|
392
|
+
const lastModified =
|
|
393
|
+
object.LastModified ??
|
|
394
|
+
new Date(0);
|
|
395
|
+
|
|
396
|
+
objects.push({
|
|
397
|
+
key:
|
|
398
|
+
logicalKey,
|
|
399
|
+
size,
|
|
400
|
+
contentType:
|
|
401
|
+
"application/octet-stream",
|
|
402
|
+
lastModified:
|
|
403
|
+
new Date(
|
|
404
|
+
lastModified.getTime()
|
|
405
|
+
),
|
|
406
|
+
etag:
|
|
407
|
+
object.ETag ??
|
|
408
|
+
`W/"${size}-${Math.trunc(
|
|
409
|
+
lastModified.getTime()
|
|
410
|
+
)}"`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
objects,
|
|
416
|
+
...(response.IsTruncated &&
|
|
417
|
+
response.NextContinuationToken
|
|
418
|
+
? {
|
|
419
|
+
cursor:
|
|
420
|
+
response.NextContinuationToken,
|
|
421
|
+
}
|
|
422
|
+
: {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function copyS3Object(
|
|
427
|
+
adapter: S3StorageEcosystemAdapter,
|
|
428
|
+
bucket: string,
|
|
429
|
+
prefix: string,
|
|
430
|
+
sourceKey: string,
|
|
431
|
+
destinationKey: string,
|
|
432
|
+
options: StorageCopyOptions
|
|
433
|
+
): Promise<StorageObjectMetadata> {
|
|
434
|
+
const source =
|
|
435
|
+
resolveS3Key(
|
|
436
|
+
prefix,
|
|
437
|
+
sourceKey
|
|
438
|
+
);
|
|
439
|
+
const destination =
|
|
440
|
+
resolveS3Key(
|
|
441
|
+
prefix,
|
|
442
|
+
destinationKey
|
|
443
|
+
);
|
|
444
|
+
const sourceMetadata =
|
|
445
|
+
await requireS3Object(
|
|
446
|
+
adapter,
|
|
447
|
+
source.normalizedKey
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
source.normalizedKey ===
|
|
452
|
+
destination.normalizedKey
|
|
453
|
+
) {
|
|
454
|
+
return sourceMetadata;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
!options.overwrite &&
|
|
459
|
+
await adapter.exists(
|
|
460
|
+
destination.normalizedKey
|
|
461
|
+
)
|
|
462
|
+
) {
|
|
463
|
+
throw new StorageError(
|
|
464
|
+
"OBJECT_EXISTS",
|
|
465
|
+
`BCP Framework: storage object "${destination.normalizedKey}" already exists.`,
|
|
466
|
+
409
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await adapter.client.send(
|
|
471
|
+
new CopyObjectCommand({
|
|
472
|
+
Bucket:
|
|
473
|
+
bucket,
|
|
474
|
+
Key:
|
|
475
|
+
destination.providerKey,
|
|
476
|
+
CopySource:
|
|
477
|
+
createCopySource(
|
|
478
|
+
bucket,
|
|
479
|
+
source.providerKey
|
|
480
|
+
),
|
|
481
|
+
MetadataDirective:
|
|
482
|
+
"COPY",
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return requireS3Object(
|
|
487
|
+
adapter,
|
|
488
|
+
destination.normalizedKey
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function getS3Metadata(
|
|
493
|
+
adapter: S3StorageEcosystemAdapter,
|
|
494
|
+
bucket: string,
|
|
495
|
+
prefix: string,
|
|
496
|
+
key: string
|
|
497
|
+
): Promise<StorageUserMetadata> {
|
|
498
|
+
const resolved =
|
|
499
|
+
resolveS3Key(
|
|
500
|
+
prefix,
|
|
501
|
+
key
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const response =
|
|
506
|
+
await adapter.client.send(
|
|
507
|
+
new HeadObjectCommand({
|
|
508
|
+
Bucket:
|
|
509
|
+
bucket,
|
|
510
|
+
Key:
|
|
511
|
+
resolved.providerKey,
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
const metadata:
|
|
515
|
+
StorageUserMetadata = {};
|
|
516
|
+
|
|
517
|
+
for (
|
|
518
|
+
const [rawKey, rawValue]
|
|
519
|
+
of Object.entries(
|
|
520
|
+
response.Metadata ?? {}
|
|
521
|
+
)
|
|
522
|
+
) {
|
|
523
|
+
if (
|
|
524
|
+
rawKey.toLowerCase() ===
|
|
525
|
+
"bcp_sha256" ||
|
|
526
|
+
rawValue === undefined
|
|
527
|
+
) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
metadata[
|
|
532
|
+
rawKey.toLowerCase()
|
|
533
|
+
] = rawValue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return normalizeStorageUserMetadata(
|
|
537
|
+
metadata
|
|
538
|
+
);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (
|
|
541
|
+
isS3Status(
|
|
542
|
+
error,
|
|
543
|
+
404
|
|
544
|
+
)
|
|
545
|
+
) {
|
|
546
|
+
throw objectNotFound(
|
|
547
|
+
resolved.normalizedKey
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function setS3Metadata(
|
|
556
|
+
adapter: S3StorageEcosystemAdapter,
|
|
557
|
+
bucket: string,
|
|
558
|
+
prefix: string,
|
|
559
|
+
key: string,
|
|
560
|
+
metadata: StorageUserMetadata
|
|
561
|
+
): Promise<StorageUserMetadata> {
|
|
562
|
+
const resolved =
|
|
563
|
+
resolveS3Key(
|
|
564
|
+
prefix,
|
|
565
|
+
key
|
|
566
|
+
);
|
|
567
|
+
let head;
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
head =
|
|
571
|
+
await adapter.client.send(
|
|
572
|
+
new HeadObjectCommand({
|
|
573
|
+
Bucket:
|
|
574
|
+
bucket,
|
|
575
|
+
Key:
|
|
576
|
+
resolved.providerKey,
|
|
577
|
+
})
|
|
578
|
+
);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (
|
|
581
|
+
isS3Status(
|
|
582
|
+
error,
|
|
583
|
+
404
|
|
584
|
+
)
|
|
585
|
+
) {
|
|
586
|
+
throw objectNotFound(
|
|
587
|
+
resolved.normalizedKey
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const userMetadata =
|
|
595
|
+
normalizeStorageUserMetadata(
|
|
596
|
+
metadata
|
|
597
|
+
);
|
|
598
|
+
const replacement:
|
|
599
|
+
Record<string, string> = {
|
|
600
|
+
...userMetadata,
|
|
601
|
+
};
|
|
602
|
+
const checksumSha256 =
|
|
603
|
+
head.Metadata
|
|
604
|
+
?.bcp_sha256;
|
|
605
|
+
|
|
606
|
+
if (checksumSha256) {
|
|
607
|
+
replacement.bcp_sha256 =
|
|
608
|
+
checksumSha256;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
await adapter.client.send(
|
|
612
|
+
new CopyObjectCommand({
|
|
613
|
+
Bucket:
|
|
614
|
+
bucket,
|
|
615
|
+
Key:
|
|
616
|
+
resolved.providerKey,
|
|
617
|
+
CopySource:
|
|
618
|
+
createCopySource(
|
|
619
|
+
bucket,
|
|
620
|
+
resolved.providerKey
|
|
621
|
+
),
|
|
622
|
+
MetadataDirective:
|
|
623
|
+
"REPLACE",
|
|
624
|
+
Metadata:
|
|
625
|
+
replacement,
|
|
626
|
+
ContentType:
|
|
627
|
+
head.ContentType ??
|
|
628
|
+
"application/octet-stream",
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
...userMetadata,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function deleteS3Objects(
|
|
638
|
+
adapter: S3StorageEcosystemAdapter,
|
|
639
|
+
bucket: string,
|
|
640
|
+
prefix: string,
|
|
641
|
+
keys: readonly string[]
|
|
642
|
+
): Promise<StorageDeleteManyResult> {
|
|
643
|
+
const normalizedKeys =
|
|
644
|
+
[
|
|
645
|
+
...new Set(
|
|
646
|
+
keys.map(
|
|
647
|
+
(key) =>
|
|
648
|
+
normalizeStorageKey(
|
|
649
|
+
key
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
),
|
|
653
|
+
];
|
|
654
|
+
const deleted:
|
|
655
|
+
string[] = [];
|
|
656
|
+
const failed:
|
|
657
|
+
StorageDeleteManyFailure[] = [];
|
|
658
|
+
|
|
659
|
+
for (
|
|
660
|
+
let offset = 0;
|
|
661
|
+
offset < normalizedKeys.length;
|
|
662
|
+
offset += S3_DELETE_BATCH_SIZE
|
|
663
|
+
) {
|
|
664
|
+
const batch =
|
|
665
|
+
normalizedKeys.slice(
|
|
666
|
+
offset,
|
|
667
|
+
offset +
|
|
668
|
+
S3_DELETE_BATCH_SIZE
|
|
669
|
+
);
|
|
670
|
+
const byProviderKey =
|
|
671
|
+
new Map<string, string>();
|
|
672
|
+
|
|
673
|
+
for (const key of batch) {
|
|
674
|
+
byProviderKey.set(
|
|
675
|
+
resolveS3Key(
|
|
676
|
+
prefix,
|
|
677
|
+
key
|
|
678
|
+
).providerKey,
|
|
679
|
+
key
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const response =
|
|
684
|
+
await adapter.client.send(
|
|
685
|
+
new DeleteObjectsCommand({
|
|
686
|
+
Bucket:
|
|
687
|
+
bucket,
|
|
688
|
+
Delete: {
|
|
689
|
+
Quiet: false,
|
|
690
|
+
Objects:
|
|
691
|
+
[
|
|
692
|
+
...byProviderKey.keys(),
|
|
693
|
+
].map(
|
|
694
|
+
(providerKey) => ({
|
|
695
|
+
Key:
|
|
696
|
+
providerKey,
|
|
697
|
+
})
|
|
698
|
+
),
|
|
699
|
+
},
|
|
700
|
+
})
|
|
701
|
+
);
|
|
702
|
+
const failedKeys =
|
|
703
|
+
new Set<string>();
|
|
704
|
+
|
|
705
|
+
for (
|
|
706
|
+
const error
|
|
707
|
+
of response.Errors ?? []
|
|
708
|
+
) {
|
|
709
|
+
if (!error.Key) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const logicalKey =
|
|
714
|
+
byProviderKey.get(
|
|
715
|
+
error.Key
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
if (!logicalKey) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
failedKeys.add(
|
|
723
|
+
logicalKey
|
|
724
|
+
);
|
|
725
|
+
failed.push({
|
|
726
|
+
key:
|
|
727
|
+
logicalKey,
|
|
728
|
+
message:
|
|
729
|
+
error.Message ??
|
|
730
|
+
error.Code ??
|
|
731
|
+
"S3 delete failed.",
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const key of batch) {
|
|
736
|
+
if (
|
|
737
|
+
!failedKeys.has(
|
|
738
|
+
key
|
|
739
|
+
)
|
|
740
|
+
) {
|
|
741
|
+
deleted.push(
|
|
742
|
+
key
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
deleted,
|
|
750
|
+
missing: [],
|
|
751
|
+
failed,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function requireS3Object(
|
|
756
|
+
adapter: S3StorageAdapter,
|
|
757
|
+
key: string
|
|
758
|
+
): Promise<StorageObjectMetadata> {
|
|
759
|
+
const metadata =
|
|
760
|
+
await adapter.stat(
|
|
761
|
+
key
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
if (!metadata) {
|
|
765
|
+
throw objectNotFound(
|
|
766
|
+
key
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return metadata;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function resolveS3Key(
|
|
774
|
+
prefix: string,
|
|
775
|
+
key: string
|
|
776
|
+
): {
|
|
777
|
+
normalizedKey: string;
|
|
778
|
+
providerKey: string;
|
|
779
|
+
} {
|
|
780
|
+
const normalizedKey =
|
|
781
|
+
normalizeStorageKey(
|
|
782
|
+
key
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
normalizedKey,
|
|
787
|
+
providerKey:
|
|
788
|
+
prefix
|
|
789
|
+
? `${prefix}/${normalizedKey}`
|
|
790
|
+
: normalizedKey,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function normalizeS3Prefix(
|
|
795
|
+
value: string | undefined
|
|
796
|
+
): string {
|
|
797
|
+
if (value === undefined) {
|
|
798
|
+
return "";
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const normalized =
|
|
802
|
+
value
|
|
803
|
+
.trim()
|
|
804
|
+
.replace(
|
|
805
|
+
/\\/g,
|
|
806
|
+
"/"
|
|
807
|
+
)
|
|
808
|
+
.replace(
|
|
809
|
+
/^\/+|\/+$/g,
|
|
810
|
+
""
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
return normalized
|
|
814
|
+
? normalizeStorageKey(
|
|
815
|
+
normalized
|
|
816
|
+
)
|
|
817
|
+
: "";
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function combineS3Prefix(
|
|
821
|
+
prefix: string,
|
|
822
|
+
logicalPrefix: string
|
|
823
|
+
): string {
|
|
824
|
+
if (!prefix) {
|
|
825
|
+
return logicalPrefix;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return logicalPrefix
|
|
829
|
+
? `${prefix}/${logicalPrefix}`
|
|
830
|
+
: `${prefix}/`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function toLogicalS3Key(
|
|
834
|
+
prefix: string,
|
|
835
|
+
providerKey: string
|
|
836
|
+
): string | null {
|
|
837
|
+
const value =
|
|
838
|
+
prefix
|
|
839
|
+
? (
|
|
840
|
+
providerKey.startsWith(
|
|
841
|
+
`${prefix}/`
|
|
842
|
+
)
|
|
843
|
+
? providerKey.slice(
|
|
844
|
+
prefix.length + 1
|
|
845
|
+
)
|
|
846
|
+
: ""
|
|
847
|
+
)
|
|
848
|
+
: providerKey;
|
|
849
|
+
|
|
850
|
+
if (!value) {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
return normalizeStorageKey(
|
|
856
|
+
value
|
|
857
|
+
);
|
|
858
|
+
} catch {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function createCopySource(
|
|
864
|
+
bucket: string,
|
|
865
|
+
providerKey: string
|
|
866
|
+
): string {
|
|
867
|
+
return `/${encodeURIComponent(
|
|
868
|
+
bucket
|
|
869
|
+
)}/${providerKey
|
|
870
|
+
.split("/")
|
|
871
|
+
.map(
|
|
872
|
+
encodeURIComponent
|
|
873
|
+
)
|
|
874
|
+
.join("/")}`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function normalizeContentType(
|
|
878
|
+
value: string
|
|
879
|
+
): string {
|
|
880
|
+
const normalized =
|
|
881
|
+
value
|
|
882
|
+
.trim()
|
|
883
|
+
.toLowerCase();
|
|
884
|
+
|
|
885
|
+
return (
|
|
886
|
+
!normalized ||
|
|
887
|
+
/[\r\n]/.test(
|
|
888
|
+
normalized
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
? "application/octet-stream"
|
|
892
|
+
: normalized;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function objectNotFound(
|
|
896
|
+
key: string
|
|
897
|
+
): StorageError {
|
|
898
|
+
return new StorageError(
|
|
899
|
+
"OBJECT_NOT_FOUND",
|
|
900
|
+
`BCP Framework: storage object "${key}" was not found.`,
|
|
901
|
+
404
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function isS3Status(
|
|
906
|
+
error: unknown,
|
|
907
|
+
status: number
|
|
908
|
+
): boolean {
|
|
909
|
+
if (
|
|
910
|
+
!error ||
|
|
911
|
+
typeof error !==
|
|
912
|
+
"object"
|
|
913
|
+
) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const metadata =
|
|
918
|
+
"$metadata" in error
|
|
919
|
+
? (
|
|
920
|
+
error as {
|
|
921
|
+
$metadata?: {
|
|
922
|
+
httpStatusCode?: number;
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
).$metadata
|
|
926
|
+
: undefined;
|
|
927
|
+
|
|
928
|
+
return metadata
|
|
929
|
+
?.httpStatusCode ===
|
|
930
|
+
status;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function assertNonEmptyString(
|
|
934
|
+
value: string,
|
|
935
|
+
label: string
|
|
936
|
+
): string {
|
|
937
|
+
if (
|
|
938
|
+
typeof value !== "string" ||
|
|
939
|
+
value.trim().length === 0
|
|
940
|
+
) {
|
|
941
|
+
throw new TypeError(
|
|
942
|
+
`BCP Framework: S3 storage ${label} must be a non-empty string.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return value.trim();
|
|
947
|
+
}
|