@lossless.org/client 2.1.0 → 2.3.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/objectstorage/classes.bucket.d.ts +9 -0
- package/dist_ts/objectstorage/classes.bucket.js +50 -3
- package/dist_ts/objectstorage/interfaces.d.ts +16 -2
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +8 -1
- package/dist_ts/objectstorage/internal.exactupload.operations.js +150 -79
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -2
- package/dist_ts/objectstorage/internal.multipart.planner.js +30 -1
- package/dist_ts/objectstorage/internal.multipart.stream.js +68 -19
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +32 -3
- package/dist_ts/objectstorage/internal.multipartcopy.d.ts +38 -0
- package/dist_ts/objectstorage/internal.multipartcopy.js +98 -0
- package/package.json +2 -2
- package/readme.md +4 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/objectstorage/classes.bucket.ts +51 -2
- package/ts/objectstorage/interfaces.ts +16 -2
- package/ts/objectstorage/internal.exactupload.operations.ts +194 -100
- package/ts/objectstorage/internal.multipart.planner.ts +46 -2
- package/ts/objectstorage/internal.multipart.stream.ts +79 -21
- package/ts/objectstorage/internal.multipart.types.ts +35 -2
- package/ts/objectstorage/internal.multipartcopy.ts +169 -0
|
@@ -60,6 +60,7 @@ const failureMessages: Record<TMultipartStreamFailureKind, string> = {
|
|
|
60
60
|
'The multipart source emitted too many consecutive empty chunks.',
|
|
61
61
|
'source-short': 'The multipart source ended before the declared content length.',
|
|
62
62
|
'source-extra': 'The multipart source exceeded the declared content length.',
|
|
63
|
+
'source-over-maximum': 'The multipart source exceeded the maximum content length.',
|
|
63
64
|
'part-callback': 'A multipart part callback failed.',
|
|
64
65
|
};
|
|
65
66
|
const failureSettlementMaximumTimeoutMs = 30_000;
|
|
@@ -96,13 +97,6 @@ export class MultipartStreamConsumptionError extends Error {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
const validatePlan = (planArg: TMultipartUploadPlan): void => {
|
|
99
|
-
if (
|
|
100
|
-
!Number.isSafeInteger(planArg.objectSize) ||
|
|
101
|
-
planArg.objectSize < 0 ||
|
|
102
|
-
planArg.objectSize > MULTIPART_MAX_OBJECT_SIZE
|
|
103
|
-
) {
|
|
104
|
-
throw new TypeError('plan.objectSize is invalid.');
|
|
105
|
-
}
|
|
106
100
|
if (
|
|
107
101
|
!Number.isSafeInteger(planArg.concurrency) ||
|
|
108
102
|
planArg.concurrency < 1 ||
|
|
@@ -110,6 +104,32 @@ const validatePlan = (planArg: TMultipartUploadPlan): void => {
|
|
|
110
104
|
) {
|
|
111
105
|
throw new TypeError('plan.concurrency is invalid.');
|
|
112
106
|
}
|
|
107
|
+
if (planArg.classification === 'bounded') {
|
|
108
|
+
if (
|
|
109
|
+
!Number.isSafeInteger(planArg.maxObjectSize) ||
|
|
110
|
+
planArg.maxObjectSize < 1 ||
|
|
111
|
+
planArg.maxObjectSize > MULTIPART_MAX_OBJECT_SIZE ||
|
|
112
|
+
!Number.isSafeInteger(planArg.partSize) ||
|
|
113
|
+
planArg.partSize < MULTIPART_MIN_PART_SIZE ||
|
|
114
|
+
planArg.partSize > MULTIPART_MAX_PART_SIZE ||
|
|
115
|
+
planArg.maxPartCount !== Math.ceil(planArg.maxObjectSize / planArg.partSize) ||
|
|
116
|
+
planArg.maxPartCount > MULTIPART_MAX_PART_COUNT ||
|
|
117
|
+
planArg.concurrency > planArg.maxPartCount ||
|
|
118
|
+
planArg.maximumBufferedBytes !==
|
|
119
|
+
Math.min(planArg.maxObjectSize, planArg.partSize * planArg.concurrency) +
|
|
120
|
+
Math.min(planArg.maxObjectSize, planArg.partSize)
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError('The bounded multipart plan is inconsistent.');
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
!Number.isSafeInteger(planArg.objectSize) ||
|
|
128
|
+
planArg.objectSize < 0 ||
|
|
129
|
+
planArg.objectSize > MULTIPART_MAX_OBJECT_SIZE
|
|
130
|
+
) {
|
|
131
|
+
throw new TypeError('plan.objectSize is invalid.');
|
|
132
|
+
}
|
|
113
133
|
if (planArg.classification === 'zero') {
|
|
114
134
|
if (
|
|
115
135
|
planArg.objectSize !== 0 ||
|
|
@@ -165,6 +185,14 @@ export const consumeExactMultipartStream = async (
|
|
|
165
185
|
);
|
|
166
186
|
}
|
|
167
187
|
|
|
188
|
+
const plan = optionsArg.plan;
|
|
189
|
+
// A bounded plan reads parts until the source ends and refuses more than
|
|
190
|
+
// its maximum; an exact plan reads exactly the declared length.
|
|
191
|
+
const bounded = plan.classification === 'bounded';
|
|
192
|
+
const sizeLimit = bounded ? plan.maxObjectSize : plan.objectSize;
|
|
193
|
+
const overLimitKind: TMultipartStreamFailureKind = bounded
|
|
194
|
+
? 'source-over-maximum'
|
|
195
|
+
: 'source-extra';
|
|
168
196
|
const reader = optionsArg.readableStream.getReader();
|
|
169
197
|
const callbackAbortController = new AbortController();
|
|
170
198
|
const inFlightParts = new Map<symbol, IInFlightPart>();
|
|
@@ -289,7 +317,11 @@ export const consumeExactMultipartStream = async (
|
|
|
289
317
|
}
|
|
290
318
|
};
|
|
291
319
|
|
|
292
|
-
const dispatchPart = (
|
|
320
|
+
const dispatchPart = (
|
|
321
|
+
dataArg: Uint8Array,
|
|
322
|
+
partNumberArg: number,
|
|
323
|
+
isLastPartArg: boolean,
|
|
324
|
+
): void => {
|
|
293
325
|
const token = Symbol(`multipart-part-${partNumberArg}`);
|
|
294
326
|
partCallbacksStarted++;
|
|
295
327
|
currentPartBytes = 0;
|
|
@@ -299,8 +331,8 @@ export const consumeExactMultipartStream = async (
|
|
|
299
331
|
.then(() =>
|
|
300
332
|
optionsArg.onPart({
|
|
301
333
|
partNumber: partNumberArg,
|
|
302
|
-
byteOffset: (partNumberArg - 1) *
|
|
303
|
-
isLastPart:
|
|
334
|
+
byteOffset: (partNumberArg - 1) * plan.partSize,
|
|
335
|
+
isLastPart: isLastPartArg,
|
|
304
336
|
data: dataArg,
|
|
305
337
|
signal: callbackAbortController.signal,
|
|
306
338
|
}),
|
|
@@ -347,15 +379,19 @@ export const consumeExactMultipartStream = async (
|
|
|
347
379
|
|
|
348
380
|
for (
|
|
349
381
|
let partNumber = 1;
|
|
350
|
-
partNumber <=
|
|
382
|
+
bounded ? !sourceClosed : partNumber <= plan.partCount;
|
|
351
383
|
partNumber++
|
|
352
384
|
) {
|
|
353
385
|
await waitForCapacity();
|
|
354
386
|
const partByteLength = Math.min(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
(partNumber - 1) * optionsArg.plan.partSize,
|
|
387
|
+
plan.partSize,
|
|
388
|
+
sizeLimit - (partNumber - 1) * plan.partSize,
|
|
358
389
|
);
|
|
390
|
+
if (partByteLength <= 0) {
|
|
391
|
+
// A bounded source filled its maximum, which ends at or inside the
|
|
392
|
+
// previous part; the trailing read below refuses any further byte.
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
359
395
|
if (
|
|
360
396
|
inFlightBytes + partByteLength >
|
|
361
397
|
optionsArg.plan.maximumBufferedBytes
|
|
@@ -396,6 +432,9 @@ export const consumeExactMultipartStream = async (
|
|
|
396
432
|
const readResult = await readWhileObservingCallbacks();
|
|
397
433
|
if (readResult.done) {
|
|
398
434
|
sourceClosed = true;
|
|
435
|
+
if (bounded) {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
399
438
|
throw {
|
|
400
439
|
kind: 'source-short',
|
|
401
440
|
} satisfies IPrimaryFailure;
|
|
@@ -410,16 +449,16 @@ export const consumeExactMultipartStream = async (
|
|
|
410
449
|
continue;
|
|
411
450
|
}
|
|
412
451
|
observedBytes += readResult.value.byteLength;
|
|
413
|
-
if (observedBytes >
|
|
452
|
+
if (observedBytes > sizeLimit) {
|
|
414
453
|
throw {
|
|
415
|
-
kind:
|
|
454
|
+
kind: overLimitKind,
|
|
416
455
|
} satisfies IPrimaryFailure;
|
|
417
456
|
}
|
|
418
|
-
if (readResult.value.byteLength >
|
|
457
|
+
if (readResult.value.byteLength > plan.partSize) {
|
|
419
458
|
throw {
|
|
420
459
|
kind: 'source-buffer-limit',
|
|
421
460
|
cause: new RangeError(
|
|
422
|
-
`Source chunks must not exceed ${
|
|
461
|
+
`Source chunks must not exceed ${plan.partSize} bytes.`,
|
|
423
462
|
),
|
|
424
463
|
} satisfies IPrimaryFailure;
|
|
425
464
|
}
|
|
@@ -428,7 +467,24 @@ export const consumeExactMultipartStream = async (
|
|
|
428
467
|
observeBufferedBytes();
|
|
429
468
|
}
|
|
430
469
|
|
|
431
|
-
|
|
470
|
+
if (partOffset === 0) {
|
|
471
|
+
// A bounded source ended at a part boundary.
|
|
472
|
+
currentPartBytes = 0;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
if (partOffset < partByteLength) {
|
|
476
|
+
// Only a bounded source ends inside a part; its last part keeps the
|
|
477
|
+
// bytes read, as an exact copy.
|
|
478
|
+
dispatchPart(partData.slice(0, partOffset), partNumber, true);
|
|
479
|
+
} else {
|
|
480
|
+
dispatchPart(
|
|
481
|
+
partData,
|
|
482
|
+
partNumber,
|
|
483
|
+
bounded
|
|
484
|
+
? sourceClosed || observedBytes === sizeLimit
|
|
485
|
+
: partNumber === plan.partCount,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
432
488
|
}
|
|
433
489
|
|
|
434
490
|
while (!sourceClosed) {
|
|
@@ -448,7 +504,7 @@ export const consumeExactMultipartStream = async (
|
|
|
448
504
|
}
|
|
449
505
|
observedBytes += readResult.value.byteLength;
|
|
450
506
|
throw {
|
|
451
|
-
kind:
|
|
507
|
+
kind: overLimitKind,
|
|
452
508
|
} satisfies IPrimaryFailure;
|
|
453
509
|
}
|
|
454
510
|
|
|
@@ -571,7 +627,9 @@ export const consumeExactMultipartStream = async (
|
|
|
571
627
|
completionAuthorized: false,
|
|
572
628
|
kind: failure.kind,
|
|
573
629
|
cause: failure.cause,
|
|
574
|
-
|
|
630
|
+
...(bounded
|
|
631
|
+
? { maxBytes: plan.maxObjectSize }
|
|
632
|
+
: { expectedBytes: plan.objectSize }),
|
|
575
633
|
observedBytes,
|
|
576
634
|
partCallbacksStarted,
|
|
577
635
|
completedPartNumbers: [...completedPartNumbers].sort(
|
|
@@ -4,6 +4,12 @@ export interface IMultipartPlanningOptions {
|
|
|
4
4
|
requestedConcurrency?: number;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface IMultipartBoundedPlanningOptions {
|
|
8
|
+
maxObjectSize: number;
|
|
9
|
+
requestedPartSize?: number;
|
|
10
|
+
requestedConcurrency?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
interface IMultipartUploadPlanBase {
|
|
8
14
|
objectSize: number;
|
|
9
15
|
concurrency: number;
|
|
@@ -24,11 +30,34 @@ export interface IMultipartDataUploadPlan extends IMultipartUploadPlanBase {
|
|
|
24
30
|
partCount: number;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A plan for a source of undeclared length: full parts are read until the
|
|
35
|
+
* source ends, and a source longer than `maxObjectSize` fails. The part size
|
|
36
|
+
* is chosen so that `maxObjectSize` fits into the multipart part limit.
|
|
37
|
+
*/
|
|
38
|
+
export interface IMultipartBoundedUploadPlan {
|
|
39
|
+
classification: 'bounded';
|
|
40
|
+
maxObjectSize: number;
|
|
41
|
+
partSize: number;
|
|
42
|
+
/** Parts that `maxObjectSize` needs; the source may need fewer. */
|
|
43
|
+
maxPartCount: number;
|
|
44
|
+
concurrency: number;
|
|
45
|
+
maximumBufferedBytes: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A plan for a source of declared length. */
|
|
49
|
+
export type TMultipartDeclaredUploadPlan = IMultipartZeroUploadPlan | IMultipartDataUploadPlan;
|
|
50
|
+
|
|
51
|
+
export type TMultipartUploadPlan = TMultipartDeclaredUploadPlan | IMultipartBoundedUploadPlan;
|
|
28
52
|
|
|
29
53
|
export interface IMultipartStreamPart {
|
|
30
54
|
partNumber: number;
|
|
31
55
|
byteOffset: number;
|
|
56
|
+
/**
|
|
57
|
+
* Under a bounded plan a part is only known to be the last when the source
|
|
58
|
+
* ended inside it or it reached the maximum; a source that ends exactly at
|
|
59
|
+
* a part boundary leaves its last part marked false.
|
|
60
|
+
*/
|
|
32
61
|
isLastPart: boolean;
|
|
33
62
|
/**
|
|
34
63
|
* An exact, exclusively owned copy. The consumer never mutates or reuses it.
|
|
@@ -61,6 +90,7 @@ export type TMultipartStreamFailureKind =
|
|
|
61
90
|
| 'source-empty-chunk-limit'
|
|
62
91
|
| 'source-short'
|
|
63
92
|
| 'source-extra'
|
|
93
|
+
| 'source-over-maximum'
|
|
64
94
|
| 'part-callback';
|
|
65
95
|
|
|
66
96
|
export type TMultipartOwnedReaderCleanup =
|
|
@@ -97,7 +127,10 @@ export interface IMultipartStreamFailureState {
|
|
|
97
127
|
completionAuthorized: false;
|
|
98
128
|
kind: TMultipartStreamFailureKind;
|
|
99
129
|
cause?: unknown;
|
|
100
|
-
|
|
130
|
+
/** The declared length; absent under a bounded plan. */
|
|
131
|
+
expectedBytes?: number;
|
|
132
|
+
/** The maximum length of a bounded plan. */
|
|
133
|
+
maxBytes?: number;
|
|
101
134
|
observedBytes: number;
|
|
102
135
|
partCallbacksStarted: number;
|
|
103
136
|
completedPartNumbers: number[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IBasicBucketOperation } from './internal.exactupload.capability.js';
|
|
3
|
+
import { abortMultipartUpload } from './internal.exactupload.operations.js';
|
|
4
|
+
|
|
5
|
+
const MiB = 1024 * 1024;
|
|
6
|
+
const GiB = 1024 * MiB;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sources larger than this are copied with UploadPartCopy. It is the largest
|
|
10
|
+
* object one CopyObject may copy, so every copy that one CopyObject can do
|
|
11
|
+
* stays one. A capped MinIO (2 CPUs, 2 GiB) answered a single CopyObject after
|
|
12
|
+
* about 3 s per GiB, inside the default 60 s request timeout at this size.
|
|
13
|
+
*/
|
|
14
|
+
export const multipartCopyThresholdBytes = 5 * GiB;
|
|
15
|
+
/** One 256 MiB UploadPartCopy answered within 0.9 s on the same MinIO. */
|
|
16
|
+
export const multipartCopyPartSizeBytes = 256 * MiB;
|
|
17
|
+
const maximumCopyPartSizeBytes = 5 * GiB;
|
|
18
|
+
const maximumCopyPartCount = 10_000;
|
|
19
|
+
const maximumCopyObjectSizeBytes = 5 * 1024 * GiB;
|
|
20
|
+
|
|
21
|
+
export interface IMultipartCopyPlan {
|
|
22
|
+
partSize: number;
|
|
23
|
+
partCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Grows the part size so the object fits in 10,000 parts and refuses objects
|
|
28
|
+
* that S3 cannot hold.
|
|
29
|
+
*/
|
|
30
|
+
export const planMultipartCopy = (
|
|
31
|
+
objectSizeArg: number,
|
|
32
|
+
partSizeArg = multipartCopyPartSizeBytes,
|
|
33
|
+
): IMultipartCopyPlan => {
|
|
34
|
+
if (
|
|
35
|
+
!Number.isSafeInteger(objectSizeArg) ||
|
|
36
|
+
objectSizeArg <= 0 ||
|
|
37
|
+
objectSizeArg > maximumCopyObjectSizeBytes
|
|
38
|
+
) {
|
|
39
|
+
throw new RangeError(
|
|
40
|
+
`A multipart copy needs a source of 1 byte to ${maximumCopyObjectSizeBytes} bytes, not ${objectSizeArg}.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const partSize = Math.max(
|
|
44
|
+
partSizeArg,
|
|
45
|
+
Math.ceil(objectSizeArg / maximumCopyPartCount),
|
|
46
|
+
);
|
|
47
|
+
if (partSize > maximumCopyPartSizeBytes) {
|
|
48
|
+
throw new RangeError(
|
|
49
|
+
`A multipart copy part cannot exceed ${maximumCopyPartSizeBytes} bytes.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
partSize,
|
|
54
|
+
partCount: Math.ceil(objectSizeArg / partSize),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface IMultipartCopyOptions {
|
|
59
|
+
copySource: string;
|
|
60
|
+
sourceSize: number;
|
|
61
|
+
/** Every part copy requires the source to still carry this ETag. */
|
|
62
|
+
sourceEtag: string;
|
|
63
|
+
targetBucketName: string;
|
|
64
|
+
targetKey: string;
|
|
65
|
+
/** Object properties of the new object; a multipart upload copies none itself. */
|
|
66
|
+
objectProperties: Pick<
|
|
67
|
+
plugins.s3.CreateMultipartUploadCommandInput,
|
|
68
|
+
| 'Metadata'
|
|
69
|
+
| 'ContentType'
|
|
70
|
+
| 'CacheControl'
|
|
71
|
+
| 'ContentDisposition'
|
|
72
|
+
| 'ContentEncoding'
|
|
73
|
+
| 'ContentLanguage'
|
|
74
|
+
| 'Expires'
|
|
75
|
+
>;
|
|
76
|
+
partSize?: number;
|
|
77
|
+
/** Receives cleanup that outlived its deadline; the operation must not finish before it settles. */
|
|
78
|
+
onDeferredCleanup: (settlementArg: Promise<void>) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copies one object part by part. Any failure or abort aborts the multipart
|
|
83
|
+
* upload instead of completing it.
|
|
84
|
+
*/
|
|
85
|
+
export const copyObjectMultipart = async (
|
|
86
|
+
operationArg: IBasicBucketOperation,
|
|
87
|
+
optionsArg: IMultipartCopyOptions,
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
const plan = planMultipartCopy(optionsArg.sourceSize, optionsArg.partSize);
|
|
90
|
+
const abortUpload = (uploadIdArg: string) =>
|
|
91
|
+
abortMultipartUpload(
|
|
92
|
+
operationArg,
|
|
93
|
+
optionsArg.targetKey,
|
|
94
|
+
uploadIdArg,
|
|
95
|
+
optionsArg.targetBucketName,
|
|
96
|
+
);
|
|
97
|
+
const createOutput = await operationArg.runProvider(
|
|
98
|
+
() => operationArg.client.send(
|
|
99
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
100
|
+
Bucket: optionsArg.targetBucketName,
|
|
101
|
+
Key: optionsArg.targetKey,
|
|
102
|
+
...optionsArg.objectProperties,
|
|
103
|
+
}),
|
|
104
|
+
{ abortSignal: operationArg.signal },
|
|
105
|
+
),
|
|
106
|
+
{
|
|
107
|
+
// A create that ignored cancellation still owns an upload.
|
|
108
|
+
disposeLateResult: async (lateOutputArg) => {
|
|
109
|
+
if (lateOutputArg.UploadId) {
|
|
110
|
+
const abortResult = await abortUpload(lateOutputArg.UploadId);
|
|
111
|
+
await abortResult.settlement;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
const uploadId = createOutput.UploadId;
|
|
117
|
+
if (typeof uploadId !== 'string' || uploadId.length === 0) {
|
|
118
|
+
throw new Error('CreateMultipartUpload returned no UploadId.');
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const parts: plugins.s3.CompletedPart[] = [];
|
|
122
|
+
for (let partNumber = 1; partNumber <= plan.partCount; partNumber++) {
|
|
123
|
+
const start = (partNumber - 1) * plan.partSize;
|
|
124
|
+
const end = Math.min(optionsArg.sourceSize, start + plan.partSize) - 1;
|
|
125
|
+
const partOutput = await operationArg.runProvider(
|
|
126
|
+
() => operationArg.client.send(
|
|
127
|
+
new plugins.s3.UploadPartCopyCommand({
|
|
128
|
+
Bucket: optionsArg.targetBucketName,
|
|
129
|
+
Key: optionsArg.targetKey,
|
|
130
|
+
UploadId: uploadId,
|
|
131
|
+
PartNumber: partNumber,
|
|
132
|
+
CopySource: optionsArg.copySource,
|
|
133
|
+
CopySourceRange: `bytes=${start}-${end}`,
|
|
134
|
+
CopySourceIfMatch: optionsArg.sourceEtag,
|
|
135
|
+
}),
|
|
136
|
+
{ abortSignal: operationArg.signal },
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
const etag = partOutput.CopyPartResult?.ETag;
|
|
140
|
+
if (typeof etag !== 'string' || etag.length === 0) {
|
|
141
|
+
throw new Error('UploadPartCopy returned no ETag.');
|
|
142
|
+
}
|
|
143
|
+
parts.push({ PartNumber: partNumber, ETag: etag });
|
|
144
|
+
}
|
|
145
|
+
await operationArg.runProvider(
|
|
146
|
+
() => operationArg.client.send(
|
|
147
|
+
new plugins.s3.CompleteMultipartUploadCommand({
|
|
148
|
+
Bucket: optionsArg.targetBucketName,
|
|
149
|
+
Key: optionsArg.targetKey,
|
|
150
|
+
UploadId: uploadId,
|
|
151
|
+
MultipartUpload: { Parts: parts },
|
|
152
|
+
}),
|
|
153
|
+
{ abortSignal: operationArg.signal },
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const abortResult = await abortUpload(uploadId);
|
|
158
|
+
if (abortResult.cleanup.status === 'abort-failed') {
|
|
159
|
+
throw new AggregateError(
|
|
160
|
+
[error, abortResult.cleanup.cause],
|
|
161
|
+
`The multipart copy failed and its upload '${uploadId}' could not be aborted.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (abortResult.timedOut) {
|
|
165
|
+
optionsArg.onDeferredCleanup(abortResult.settlement);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
};
|