@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
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
purgeExactPath as purgeExactPathOperation,
|
|
19
19
|
} from './internal.exactpathpurge.operations.js';
|
|
20
20
|
import { beginBasicBucketOperation } from './internal.exactupload.capability.js';
|
|
21
|
+
import {
|
|
22
|
+
copyObjectMultipart,
|
|
23
|
+
multipartCopyThresholdBytes,
|
|
24
|
+
} from './internal.multipartcopy.js';
|
|
21
25
|
import {
|
|
22
26
|
getProviderStatus,
|
|
23
27
|
headBucketExists,
|
|
@@ -930,8 +934,18 @@ export class Bucket {
|
|
|
930
934
|
return purgeExactPathOperation(this, optionsArg);
|
|
931
935
|
}
|
|
932
936
|
|
|
937
|
+
/**
|
|
938
|
+
* Copies an object within the storage account. Sources up to 5 GiB are
|
|
939
|
+
* copied with one CopyObject; larger sources, which CopyObject refuses, are
|
|
940
|
+
* copied part by part with UploadPartCopy, each part conditional on the
|
|
941
|
+
* source ETag read first.
|
|
942
|
+
* A failed or aborted part copy aborts the multipart upload. Object tags,
|
|
943
|
+
* storage class, server-side encryption settings and a website redirect are
|
|
944
|
+
* not carried by a part-by-part copy.
|
|
945
|
+
*/
|
|
933
946
|
public async fastCopy(optionsArg: interfaces.IFastCopyOptions): Promise<void> {
|
|
934
947
|
const operation = beginBasicBucketOperation(this, optionsArg.signal);
|
|
948
|
+
const deferredCleanups: Promise<void>[] = [];
|
|
935
949
|
try {
|
|
936
950
|
const targetBucketName = optionsArg.targetBucket ? optionsArg.targetBucket.name : this.name;
|
|
937
951
|
|
|
@@ -951,10 +965,41 @@ export class Bucket {
|
|
|
951
965
|
|
|
952
966
|
// Define the copy operation
|
|
953
967
|
const copySource = `${operation.bucketName}/${optionsArg.sourcePath}`;
|
|
968
|
+
const targetKey = optionsArg.destinationPath || optionsArg.sourcePath;
|
|
969
|
+
const sourceSize = currentObjInfo.ContentLength;
|
|
970
|
+
if (typeof sourceSize === 'number' && sourceSize > multipartCopyThresholdBytes) {
|
|
971
|
+
if (typeof currentObjInfo.ETag !== 'string' || currentObjInfo.ETag.length === 0) {
|
|
972
|
+
throw new Error('HeadObject returned no ETag for the copy source.');
|
|
973
|
+
}
|
|
974
|
+
await copyObjectMultipart(operation, {
|
|
975
|
+
copySource,
|
|
976
|
+
sourceSize,
|
|
977
|
+
sourceEtag: currentObjInfo.ETag,
|
|
978
|
+
targetBucketName,
|
|
979
|
+
targetKey,
|
|
980
|
+
// Mirrors CopyObject: COPY keeps the source metadata and content
|
|
981
|
+
// headers, REPLACE sets only the given metadata.
|
|
982
|
+
objectProperties: optionsArg.deleteExistingNativeMetadata
|
|
983
|
+
? { Metadata: newNativeMetadata }
|
|
984
|
+
: {
|
|
985
|
+
Metadata: currentObjInfo.Metadata,
|
|
986
|
+
ContentType: currentObjInfo.ContentType,
|
|
987
|
+
CacheControl: currentObjInfo.CacheControl,
|
|
988
|
+
ContentDisposition: currentObjInfo.ContentDisposition,
|
|
989
|
+
ContentEncoding: currentObjInfo.ContentEncoding,
|
|
990
|
+
ContentLanguage: currentObjInfo.ContentLanguage,
|
|
991
|
+
Expires: currentObjInfo.Expires,
|
|
992
|
+
},
|
|
993
|
+
onDeferredCleanup: (settlementArg) => {
|
|
994
|
+
deferredCleanups.push(settlementArg);
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
954
999
|
const command = new plugins.s3.CopyObjectCommand({
|
|
955
1000
|
Bucket: targetBucketName,
|
|
956
1001
|
CopySource: copySource,
|
|
957
|
-
Key:
|
|
1002
|
+
Key: targetKey,
|
|
958
1003
|
Metadata: newNativeMetadata,
|
|
959
1004
|
MetadataDirective: optionsArg.deleteExistingNativeMetadata ? 'REPLACE' : 'COPY',
|
|
960
1005
|
});
|
|
@@ -962,7 +1007,11 @@ export class Bucket {
|
|
|
962
1007
|
() => operation.client.send(command, { abortSignal: operation.signal }),
|
|
963
1008
|
);
|
|
964
1009
|
} finally {
|
|
965
|
-
|
|
1010
|
+
if (deferredCleanups.length > 0) {
|
|
1011
|
+
void Promise.allSettled(deferredCleanups).then(() => operation.finish());
|
|
1012
|
+
} else {
|
|
1013
|
+
operation.finish();
|
|
1014
|
+
}
|
|
966
1015
|
}
|
|
967
1016
|
}
|
|
968
1017
|
|
|
@@ -264,7 +264,18 @@ export interface IFastPutStreamExactOptions {
|
|
|
264
264
|
* size (5 MiB by default).
|
|
265
265
|
*/
|
|
266
266
|
readableStream: Readable | ReadableStream<Uint8Array>;
|
|
267
|
-
|
|
267
|
+
/**
|
|
268
|
+
* The exact source length. Exactly one of `contentLength` and
|
|
269
|
+
* `maxContentLength` must be given.
|
|
270
|
+
*/
|
|
271
|
+
contentLength?: number;
|
|
272
|
+
/**
|
|
273
|
+
* The upper bound for a source of undeclared length. The part size grows so
|
|
274
|
+
* that the maximum fits in 10,000 parts, and buffering stays bounded to the
|
|
275
|
+
* planned parts in flight. A source that exceeds the maximum fails the
|
|
276
|
+
* upload, and the multipart upload is aborted instead of completed.
|
|
277
|
+
*/
|
|
278
|
+
maxContentLength?: number;
|
|
268
279
|
nativeMetadata?: Record<string, string>;
|
|
269
280
|
/**
|
|
270
281
|
* Native S3 object properties that must retain their semantic header
|
|
@@ -316,7 +327,10 @@ export type TExactUploadMultipartCleanup =
|
|
|
316
327
|
export interface IExactUploadFailureState {
|
|
317
328
|
phase: TExactUploadFailurePhase;
|
|
318
329
|
destinationPath: string;
|
|
319
|
-
|
|
330
|
+
/** The declared source length, when the upload declared one. */
|
|
331
|
+
expectedContentLength?: number;
|
|
332
|
+
/** The source maximum, when the upload had an undeclared length. */
|
|
333
|
+
maxContentLength?: number;
|
|
320
334
|
/**
|
|
321
335
|
* Whether this operation can prove that it did not publish the destination.
|
|
322
336
|
* A conditional PutObject or CompleteMultipartUpload response can be lost
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
mintExactUploadCapability,
|
|
16
16
|
type IExactUploadOperation,
|
|
17
17
|
} from './internal.exactupload.capability.js';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
planBoundedMultipartUpload,
|
|
20
|
+
planMultipartUpload,
|
|
21
|
+
} from './internal.multipart.planner.js';
|
|
19
22
|
import {
|
|
20
23
|
consumeExactMultipartStream,
|
|
21
24
|
MultipartStreamConsumptionError,
|
|
@@ -44,7 +47,7 @@ interface IIndependentCleanupResult {
|
|
|
44
47
|
settlement: Promise<void>;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
interface IMultipartAbortResult {
|
|
50
|
+
export interface IMultipartAbortResult {
|
|
48
51
|
cleanup: TExactUploadMultipartCleanup;
|
|
49
52
|
timedOut: boolean;
|
|
50
53
|
settlement: Promise<void>;
|
|
@@ -1263,10 +1266,11 @@ export const probeExactUploadCapability = async (
|
|
|
1263
1266
|
}
|
|
1264
1267
|
};
|
|
1265
1268
|
|
|
1266
|
-
const abortMultipartUpload = async (
|
|
1269
|
+
export const abortMultipartUpload = async (
|
|
1267
1270
|
operationArg: IExactUploadOperation,
|
|
1268
1271
|
keyArg: string,
|
|
1269
1272
|
uploadIdArg: string,
|
|
1273
|
+
bucketNameArg = operationArg.bucketName,
|
|
1270
1274
|
): Promise<IMultipartAbortResult> => {
|
|
1271
1275
|
let reconciliationResult: 'aborted' | 'absent' | undefined;
|
|
1272
1276
|
const cleanupResult = await runIndependentCleanup(
|
|
@@ -1274,7 +1278,7 @@ const abortMultipartUpload = async (
|
|
|
1274
1278
|
async (signalArg) => {
|
|
1275
1279
|
reconciliationResult = await reconcileMultipartUploadAbort(
|
|
1276
1280
|
operationArg.client,
|
|
1277
|
-
|
|
1281
|
+
bucketNameArg,
|
|
1278
1282
|
keyArg,
|
|
1279
1283
|
uploadIdArg,
|
|
1280
1284
|
signalArg,
|
|
@@ -1306,6 +1310,69 @@ const abortMultipartUpload = async (
|
|
|
1306
1310
|
};
|
|
1307
1311
|
};
|
|
1308
1312
|
|
|
1313
|
+
/**
|
|
1314
|
+
* Plans the upload from its declared length, or from the maximum of a source
|
|
1315
|
+
* of undeclared length, and names that length for failure states.
|
|
1316
|
+
*/
|
|
1317
|
+
const planExactUpload = (optionsArg: IFastPutStreamExactOptions) => {
|
|
1318
|
+
const declaresLength = optionsArg.contentLength !== undefined;
|
|
1319
|
+
if (declaresLength === (optionsArg.maxContentLength !== undefined)) {
|
|
1320
|
+
throw new TypeError(
|
|
1321
|
+
'Exactly one of contentLength and maxContentLength must be given.',
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
if (declaresLength) {
|
|
1325
|
+
return {
|
|
1326
|
+
plan: planMultipartUpload({
|
|
1327
|
+
objectSize: optionsArg.contentLength!,
|
|
1328
|
+
requestedPartSize: optionsArg.requestedPartSize,
|
|
1329
|
+
requestedConcurrency: optionsArg.requestedConcurrency,
|
|
1330
|
+
}),
|
|
1331
|
+
lengthState: {
|
|
1332
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
plan: planBoundedMultipartUpload({
|
|
1338
|
+
maxObjectSize: optionsArg.maxContentLength!,
|
|
1339
|
+
requestedPartSize: optionsArg.requestedPartSize,
|
|
1340
|
+
requestedConcurrency: optionsArg.requestedConcurrency,
|
|
1341
|
+
}),
|
|
1342
|
+
lengthState: {
|
|
1343
|
+
maxContentLength: optionsArg.maxContentLength,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const sendConditionalZeroPut = (
|
|
1349
|
+
operationArg: IExactUploadOperation,
|
|
1350
|
+
optionsArg: IFastPutStreamExactOptions,
|
|
1351
|
+
contentTypeArg: string | undefined,
|
|
1352
|
+
onAttemptArg: () => void,
|
|
1353
|
+
) => {
|
|
1354
|
+
return sendOperationCommand(
|
|
1355
|
+
operationArg,
|
|
1356
|
+
(signalArg) => {
|
|
1357
|
+
onAttemptArg();
|
|
1358
|
+
return operationArg.client.send(
|
|
1359
|
+
new plugins.s3.PutObjectCommand({
|
|
1360
|
+
Bucket: operationArg.bucketName,
|
|
1361
|
+
Key: optionsArg.path,
|
|
1362
|
+
Body: new Uint8Array(0),
|
|
1363
|
+
ContentLength: 0,
|
|
1364
|
+
Metadata: optionsArg.nativeMetadata,
|
|
1365
|
+
ContentType: contentTypeArg,
|
|
1366
|
+
IfNoneMatch: '*',
|
|
1367
|
+
}),
|
|
1368
|
+
{
|
|
1369
|
+
abortSignal: signalArg,
|
|
1370
|
+
},
|
|
1371
|
+
);
|
|
1372
|
+
},
|
|
1373
|
+
);
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1309
1376
|
export const fastPutStreamExact = async (
|
|
1310
1377
|
bucketRefArg: IExactUploadBucketRef,
|
|
1311
1378
|
optionsArg: IFastPutStreamExactOptions,
|
|
@@ -1316,11 +1383,7 @@ export const fastPutStreamExact = async (
|
|
|
1316
1383
|
const nativeObjectProperties = snapshotNativeObjectProperties(
|
|
1317
1384
|
optionsArg.nativeObjectProperties,
|
|
1318
1385
|
);
|
|
1319
|
-
const plan =
|
|
1320
|
-
objectSize: optionsArg.contentLength,
|
|
1321
|
-
requestedPartSize: optionsArg.requestedPartSize,
|
|
1322
|
-
requestedConcurrency: optionsArg.requestedConcurrency,
|
|
1323
|
-
});
|
|
1386
|
+
const { plan, lengthState } = planExactUpload(optionsArg);
|
|
1324
1387
|
|
|
1325
1388
|
let operation: IExactUploadOperation;
|
|
1326
1389
|
try {
|
|
@@ -1334,7 +1397,7 @@ export const fastPutStreamExact = async (
|
|
|
1334
1397
|
{
|
|
1335
1398
|
phase: 'capability',
|
|
1336
1399
|
destinationPath: optionsArg.path,
|
|
1337
|
-
|
|
1400
|
+
...lengthState,
|
|
1338
1401
|
destinationPublication: 'not-published',
|
|
1339
1402
|
multipartCleanup: {
|
|
1340
1403
|
status: 'not-required',
|
|
@@ -1353,7 +1416,7 @@ export const fastPutStreamExact = async (
|
|
|
1353
1416
|
{
|
|
1354
1417
|
phase: 'source',
|
|
1355
1418
|
destinationPath: optionsArg.path,
|
|
1356
|
-
|
|
1419
|
+
...lengthState,
|
|
1357
1420
|
destinationPublication: 'not-published',
|
|
1358
1421
|
multipartCleanup: {
|
|
1359
1422
|
status: 'not-required',
|
|
@@ -1393,7 +1456,7 @@ export const fastPutStreamExact = async (
|
|
|
1393
1456
|
{
|
|
1394
1457
|
phase: 'source',
|
|
1395
1458
|
destinationPath: optionsArg.path,
|
|
1396
|
-
|
|
1459
|
+
...lengthState,
|
|
1397
1460
|
destinationPublication: 'not-published',
|
|
1398
1461
|
multipartCleanup: {
|
|
1399
1462
|
status: 'not-required',
|
|
@@ -1404,24 +1467,12 @@ export const fastPutStreamExact = async (
|
|
|
1404
1467
|
}
|
|
1405
1468
|
|
|
1406
1469
|
try {
|
|
1407
|
-
const putOutput = await
|
|
1470
|
+
const putOutput = await sendConditionalZeroPut(
|
|
1408
1471
|
operation,
|
|
1409
|
-
|
|
1472
|
+
optionsArg,
|
|
1473
|
+
nativeObjectProperties?.contentType,
|
|
1474
|
+
() => {
|
|
1410
1475
|
putRequestAttempted = true;
|
|
1411
|
-
return operation.client.send(
|
|
1412
|
-
new plugins.s3.PutObjectCommand({
|
|
1413
|
-
Bucket: operation.bucketName,
|
|
1414
|
-
Key: optionsArg.path,
|
|
1415
|
-
Body: new Uint8Array(0),
|
|
1416
|
-
ContentLength: 0,
|
|
1417
|
-
Metadata: optionsArg.nativeMetadata,
|
|
1418
|
-
ContentType: nativeObjectProperties?.contentType,
|
|
1419
|
-
IfNoneMatch: '*',
|
|
1420
|
-
}),
|
|
1421
|
-
{
|
|
1422
|
-
abortSignal: signalArg,
|
|
1423
|
-
},
|
|
1424
|
-
);
|
|
1425
1476
|
},
|
|
1426
1477
|
);
|
|
1427
1478
|
return {
|
|
@@ -1441,7 +1492,7 @@ export const fastPutStreamExact = async (
|
|
|
1441
1492
|
{
|
|
1442
1493
|
phase: 'put-zero',
|
|
1443
1494
|
destinationPath: optionsArg.path,
|
|
1444
|
-
|
|
1495
|
+
...lengthState,
|
|
1445
1496
|
destinationPublication:
|
|
1446
1497
|
putRequestAttempted &&
|
|
1447
1498
|
!isDefinitiveConditionalWriteRejection(error)
|
|
@@ -1465,36 +1516,64 @@ export const fastPutStreamExact = async (
|
|
|
1465
1516
|
}
|
|
1466
1517
|
}
|
|
1467
1518
|
|
|
1468
|
-
|
|
1519
|
+
const bounded = plan.classification === 'bounded';
|
|
1520
|
+
let phase: TExactUploadFailurePhase = bounded ? 'source' : 'create-multipart';
|
|
1469
1521
|
let uploadId: string | undefined;
|
|
1470
1522
|
let createRequestAttempted = false;
|
|
1523
|
+
let createFailed = false;
|
|
1524
|
+
let createSettled = false;
|
|
1525
|
+
let zeroPutAttempted = false;
|
|
1471
1526
|
let completeRequestAttempted = false;
|
|
1472
1527
|
let operationFinishSettlement: Promise<void> | undefined;
|
|
1473
1528
|
let detachedStreamError: MultipartStreamConsumptionError | undefined;
|
|
1474
|
-
|
|
1475
|
-
|
|
1529
|
+
let multipartUploadCreation: Promise<string> | undefined;
|
|
1530
|
+
// A source of undeclared length creates the multipart upload with its first
|
|
1531
|
+
// part, so an empty source never leaves one behind.
|
|
1532
|
+
const ensureMultipartUpload = (): Promise<string> => {
|
|
1533
|
+
multipartUploadCreation ??= sendOperationCommand(
|
|
1476
1534
|
operation,
|
|
1477
1535
|
(signalArg) => {
|
|
1478
1536
|
createRequestAttempted = true;
|
|
1479
|
-
return operation.client
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1537
|
+
return operation.client
|
|
1538
|
+
.send(
|
|
1539
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
1540
|
+
Bucket: operation.bucketName,
|
|
1541
|
+
Key: optionsArg.path,
|
|
1542
|
+
Metadata: optionsArg.nativeMetadata,
|
|
1543
|
+
ContentType: nativeObjectProperties?.contentType,
|
|
1544
|
+
}),
|
|
1545
|
+
{
|
|
1546
|
+
abortSignal: signalArg,
|
|
1547
|
+
},
|
|
1548
|
+
)
|
|
1549
|
+
.then((createOutputArg) => {
|
|
1550
|
+
// A create that ignored cancellation still owns its upload, so
|
|
1551
|
+
// deferred cleanup aborts it by this id.
|
|
1552
|
+
uploadId = getOptionalNonEmptyString(createOutputArg.UploadId);
|
|
1553
|
+
return createOutputArg;
|
|
1554
|
+
});
|
|
1555
|
+
},
|
|
1556
|
+
).then(
|
|
1557
|
+
(createOutputArg) => {
|
|
1558
|
+
createSettled = true;
|
|
1559
|
+
return requireNonEmptyString(
|
|
1560
|
+
createOutputArg.UploadId,
|
|
1561
|
+
'CreateMultipartUpload UploadId',
|
|
1489
1562
|
);
|
|
1490
1563
|
},
|
|
1564
|
+
(error: unknown) => {
|
|
1565
|
+
createSettled = true;
|
|
1566
|
+
createFailed = true;
|
|
1567
|
+
throw error;
|
|
1568
|
+
},
|
|
1491
1569
|
);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
)
|
|
1496
|
-
|
|
1497
|
-
|
|
1570
|
+
return multipartUploadCreation;
|
|
1571
|
+
};
|
|
1572
|
+
try {
|
|
1573
|
+
if (!bounded) {
|
|
1574
|
+
await ensureMultipartUpload();
|
|
1575
|
+
phase = 'source';
|
|
1576
|
+
}
|
|
1498
1577
|
const completedParts: plugins.s3.CompletedPart[] = [];
|
|
1499
1578
|
let consumptionResult;
|
|
1500
1579
|
try {
|
|
@@ -1505,12 +1584,14 @@ export const fastPutStreamExact = async (
|
|
|
1505
1584
|
failureSettlementTimeoutMs:
|
|
1506
1585
|
optionsArg.failureSettlementTimeoutMs,
|
|
1507
1586
|
async onPart(partArg) {
|
|
1587
|
+
operation.assertCurrent();
|
|
1588
|
+
const partUploadId = await ensureMultipartUpload();
|
|
1508
1589
|
operation.assertCurrent();
|
|
1509
1590
|
const uploadPartOutput = await operation.client.send(
|
|
1510
1591
|
new plugins.s3.UploadPartCommand({
|
|
1511
1592
|
Bucket: operation.bucketName,
|
|
1512
1593
|
Key: optionsArg.path,
|
|
1513
|
-
UploadId:
|
|
1594
|
+
UploadId: partUploadId,
|
|
1514
1595
|
PartNumber: partArg.partNumber,
|
|
1515
1596
|
Body: partArg.data,
|
|
1516
1597
|
ContentLength: partArg.data.byteLength,
|
|
@@ -1532,9 +1613,15 @@ export const fastPutStreamExact = async (
|
|
|
1532
1613
|
} catch (error) {
|
|
1533
1614
|
if (
|
|
1534
1615
|
error instanceof MultipartStreamConsumptionError &&
|
|
1535
|
-
error.state.kind === 'part-callback'
|
|
1616
|
+
(error.state.kind === 'part-callback' || error.state.kind === 'aborted')
|
|
1536
1617
|
) {
|
|
1537
|
-
|
|
1618
|
+
// A source of undeclared length creates its upload with the first
|
|
1619
|
+
// part, so a failed or still outstanding create owns the failure.
|
|
1620
|
+
if (createFailed || (multipartUploadCreation && !createSettled)) {
|
|
1621
|
+
phase = 'create-multipart';
|
|
1622
|
+
} else if (error.state.kind === 'part-callback') {
|
|
1623
|
+
phase = 'upload-part';
|
|
1624
|
+
}
|
|
1538
1625
|
}
|
|
1539
1626
|
if (
|
|
1540
1627
|
error instanceof MultipartStreamConsumptionError &&
|
|
@@ -1548,6 +1635,28 @@ export const fastPutStreamExact = async (
|
|
|
1548
1635
|
throw error;
|
|
1549
1636
|
}
|
|
1550
1637
|
|
|
1638
|
+
if (!multipartUploadCreation) {
|
|
1639
|
+
// Only an empty source of undeclared length reaches this point.
|
|
1640
|
+
phase = 'put-zero';
|
|
1641
|
+
const putOutput = await sendConditionalZeroPut(
|
|
1642
|
+
operation,
|
|
1643
|
+
optionsArg,
|
|
1644
|
+
nativeObjectProperties?.contentType,
|
|
1645
|
+
() => {
|
|
1646
|
+
zeroPutAttempted = true;
|
|
1647
|
+
},
|
|
1648
|
+
);
|
|
1649
|
+
return {
|
|
1650
|
+
destinationPath: optionsArg.path,
|
|
1651
|
+
contentLength: 0,
|
|
1652
|
+
etag: requireNonEmptyString(putOutput.ETag, 'PutObject ETag'),
|
|
1653
|
+
versionId: getOptionalNonEmptyString(putOutput.VersionId),
|
|
1654
|
+
partCount: 0,
|
|
1655
|
+
peakBufferedBytes: consumptionResult.peakBufferedBytes,
|
|
1656
|
+
maximumBufferedBytes: consumptionResult.maximumBufferedBytes,
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
const completeUploadId = await multipartUploadCreation;
|
|
1551
1660
|
phase = 'complete-multipart';
|
|
1552
1661
|
const completeOutput = await sendOperationCommand(
|
|
1553
1662
|
operation,
|
|
@@ -1557,12 +1666,12 @@ export const fastPutStreamExact = async (
|
|
|
1557
1666
|
new plugins.s3.CompleteMultipartUploadCommand({
|
|
1558
1667
|
Bucket: operation.bucketName,
|
|
1559
1668
|
Key: optionsArg.path,
|
|
1560
|
-
UploadId:
|
|
1669
|
+
UploadId: completeUploadId,
|
|
1561
1670
|
MultipartUpload: {
|
|
1562
1671
|
Parts: completedParts,
|
|
1563
1672
|
},
|
|
1564
1673
|
IfNoneMatch: '*',
|
|
1565
|
-
MpuObjectSize:
|
|
1674
|
+
MpuObjectSize: consumptionResult.bytesConsumed,
|
|
1566
1675
|
}),
|
|
1567
1676
|
{
|
|
1568
1677
|
abortSignal: signalArg,
|
|
@@ -1572,7 +1681,7 @@ export const fastPutStreamExact = async (
|
|
|
1572
1681
|
);
|
|
1573
1682
|
return {
|
|
1574
1683
|
destinationPath: optionsArg.path,
|
|
1575
|
-
contentLength:
|
|
1684
|
+
contentLength: consumptionResult.bytesConsumed,
|
|
1576
1685
|
etag: requireNonEmptyString(
|
|
1577
1686
|
completeOutput.ETag,
|
|
1578
1687
|
'CompleteMultipartUpload ETag',
|
|
@@ -1586,21 +1695,21 @@ export const fastPutStreamExact = async (
|
|
|
1586
1695
|
let multipartCleanup: TExactUploadMultipartCleanup;
|
|
1587
1696
|
const primaryProviderSettlement =
|
|
1588
1697
|
getDetachedOperationSendSettlement(operation.signal);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
multipartCleanup = {
|
|
1594
|
-
status: 'deferred',
|
|
1595
|
-
};
|
|
1596
|
-
const callbackGatedCleanup = detachedStreamError.callbackSettlement
|
|
1698
|
+
// Aborts the multipart upload once the given work settles. A create that
|
|
1699
|
+
// was still in flight may only report its UploadId by then.
|
|
1700
|
+
const abortAfterSettlement = (settlementArg: Promise<unknown>) =>
|
|
1701
|
+
settlementArg
|
|
1597
1702
|
.then(async () => {
|
|
1703
|
+
await getDetachedOperationSendSettlement(operation.signal);
|
|
1704
|
+
if (!uploadId) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1598
1707
|
try {
|
|
1599
1708
|
operation.assertLifecycleCurrent();
|
|
1600
1709
|
const abortResult = await abortMultipartUpload(
|
|
1601
1710
|
operation,
|
|
1602
1711
|
optionsArg.path,
|
|
1603
|
-
uploadId
|
|
1712
|
+
uploadId,
|
|
1604
1713
|
);
|
|
1605
1714
|
await abortResult.settlement;
|
|
1606
1715
|
} catch {
|
|
@@ -1611,35 +1720,27 @@ export const fastPutStreamExact = async (
|
|
|
1611
1720
|
() => undefined,
|
|
1612
1721
|
() => undefined,
|
|
1613
1722
|
);
|
|
1723
|
+
if (
|
|
1724
|
+
(uploadId || createRequestAttempted) &&
|
|
1725
|
+
detachedStreamError?.state.cleanup.callbacks === 'detached'
|
|
1726
|
+
) {
|
|
1727
|
+
multipartCleanup = {
|
|
1728
|
+
status: 'deferred',
|
|
1729
|
+
};
|
|
1614
1730
|
operationFinishSettlement = Promise.allSettled([
|
|
1615
|
-
|
|
1731
|
+
abortAfterSettlement(detachedStreamError.callbackSettlement),
|
|
1616
1732
|
detachedStreamError.ownedSettlement,
|
|
1617
1733
|
]).then(() => undefined);
|
|
1618
|
-
} else if (
|
|
1734
|
+
} else if (
|
|
1735
|
+
(uploadId || createRequestAttempted) &&
|
|
1736
|
+
primaryProviderSettlement
|
|
1737
|
+
) {
|
|
1619
1738
|
multipartCleanup = {
|
|
1620
1739
|
status: 'deferred',
|
|
1621
1740
|
};
|
|
1622
|
-
const providerGatedCleanup = primaryProviderSettlement
|
|
1623
|
-
.then(async () => {
|
|
1624
|
-
try {
|
|
1625
|
-
operation.assertLifecycleCurrent();
|
|
1626
|
-
const abortResult = await abortMultipartUpload(
|
|
1627
|
-
operation,
|
|
1628
|
-
optionsArg.path,
|
|
1629
|
-
uploadId!,
|
|
1630
|
-
);
|
|
1631
|
-
await abortResult.settlement;
|
|
1632
|
-
} catch {
|
|
1633
|
-
// Lifecycle close makes deferred cleanup indeterminate.
|
|
1634
|
-
}
|
|
1635
|
-
})
|
|
1636
|
-
.then(
|
|
1637
|
-
() => undefined,
|
|
1638
|
-
() => undefined,
|
|
1639
|
-
);
|
|
1640
1741
|
operationFinishSettlement = Promise.allSettled([
|
|
1641
1742
|
primaryProviderSettlement,
|
|
1642
|
-
|
|
1743
|
+
abortAfterSettlement(primaryProviderSettlement),
|
|
1643
1744
|
]).then(() => undefined);
|
|
1644
1745
|
} else if (uploadId) {
|
|
1645
1746
|
const abortResult = await abortMultipartUpload(
|
|
@@ -1653,20 +1754,11 @@ export const fastPutStreamExact = async (
|
|
|
1653
1754
|
}
|
|
1654
1755
|
} else if (createRequestAttempted) {
|
|
1655
1756
|
multipartCleanup = {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
: {
|
|
1661
|
-
status: 'abort-failed' as const,
|
|
1662
|
-
cause: new Error(
|
|
1663
|
-
'The multipart upload could not be aborted because no UploadId was returned.',
|
|
1664
|
-
),
|
|
1665
|
-
}),
|
|
1757
|
+
status: 'abort-failed',
|
|
1758
|
+
cause: new Error(
|
|
1759
|
+
'The multipart upload could not be aborted because no UploadId was returned.',
|
|
1760
|
+
),
|
|
1666
1761
|
};
|
|
1667
|
-
if (primaryProviderSettlement) {
|
|
1668
|
-
operationFinishSettlement = primaryProviderSettlement;
|
|
1669
|
-
}
|
|
1670
1762
|
} else {
|
|
1671
1763
|
multipartCleanup = {
|
|
1672
1764
|
status: 'not-required',
|
|
@@ -1691,10 +1783,12 @@ export const fastPutStreamExact = async (
|
|
|
1691
1783
|
{
|
|
1692
1784
|
phase,
|
|
1693
1785
|
destinationPath: optionsArg.path,
|
|
1694
|
-
|
|
1786
|
+
...lengthState,
|
|
1695
1787
|
destinationPublication:
|
|
1696
|
-
|
|
1697
|
-
|
|
1788
|
+
(
|
|
1789
|
+
(phase === 'complete-multipart' && completeRequestAttempted) ||
|
|
1790
|
+
(phase === 'put-zero' && zeroPutAttempted)
|
|
1791
|
+
) &&
|
|
1698
1792
|
!isDefinitiveConditionalWriteRejection(error)
|
|
1699
1793
|
? 'ambiguous'
|
|
1700
1794
|
: 'not-published',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
IMultipartBoundedPlanningOptions,
|
|
3
|
+
IMultipartBoundedUploadPlan,
|
|
2
4
|
IMultipartPlanningOptions,
|
|
3
|
-
|
|
5
|
+
TMultipartDeclaredUploadPlan,
|
|
4
6
|
} from './internal.multipart.types.js';
|
|
5
7
|
|
|
6
8
|
export const MULTIPART_MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
@@ -43,7 +45,7 @@ const normalizeRequestedConcurrency = (
|
|
|
43
45
|
|
|
44
46
|
export const planMultipartUpload = (
|
|
45
47
|
optionsArg: IMultipartPlanningOptions,
|
|
46
|
-
):
|
|
48
|
+
): TMultipartDeclaredUploadPlan => {
|
|
47
49
|
requireSafeNonNegativeInteger(optionsArg.objectSize, 'objectSize');
|
|
48
50
|
if (optionsArg.objectSize > MULTIPART_MAX_OBJECT_SIZE) {
|
|
49
51
|
throw new RangeError(
|
|
@@ -88,3 +90,45 @@ export const planMultipartUpload = (
|
|
|
88
90
|
) + Math.min(optionsArg.objectSize, partSize),
|
|
89
91
|
};
|
|
90
92
|
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Plans an upload of undeclared length that must not exceed
|
|
96
|
+
* `maxObjectSize`. The part size grows with the maximum so that the largest
|
|
97
|
+
* allowed source still fits into the multipart part limit.
|
|
98
|
+
*/
|
|
99
|
+
export const planBoundedMultipartUpload = (
|
|
100
|
+
optionsArg: IMultipartBoundedPlanningOptions,
|
|
101
|
+
): IMultipartBoundedUploadPlan => {
|
|
102
|
+
if (!Number.isSafeInteger(optionsArg.maxObjectSize) || optionsArg.maxObjectSize < 1) {
|
|
103
|
+
throw new TypeError('maxObjectSize must be a positive safe integer.');
|
|
104
|
+
}
|
|
105
|
+
if (optionsArg.maxObjectSize > MULTIPART_MAX_OBJECT_SIZE) {
|
|
106
|
+
throw new RangeError(
|
|
107
|
+
`maxObjectSize exceeds the ${MULTIPART_MAX_OBJECT_SIZE} byte multipart limit.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const requestedConcurrency = normalizeRequestedConcurrency(
|
|
111
|
+
optionsArg.requestedConcurrency,
|
|
112
|
+
);
|
|
113
|
+
const preferredPartSize = normalizeRequestedPartSize(optionsArg.requestedPartSize);
|
|
114
|
+
const partSize = Math.max(
|
|
115
|
+
preferredPartSize,
|
|
116
|
+
Math.ceil(optionsArg.maxObjectSize / MULTIPART_MAX_PART_COUNT),
|
|
117
|
+
);
|
|
118
|
+
if (partSize > MULTIPART_MAX_PART_SIZE) {
|
|
119
|
+
throw new RangeError('The maximum cannot be represented by legal multipart parts.');
|
|
120
|
+
}
|
|
121
|
+
const maxPartCount = Math.ceil(optionsArg.maxObjectSize / partSize);
|
|
122
|
+
const concurrency = Math.min(requestedConcurrency, maxPartCount);
|
|
123
|
+
return {
|
|
124
|
+
classification: 'bounded',
|
|
125
|
+
maxObjectSize: optionsArg.maxObjectSize,
|
|
126
|
+
partSize,
|
|
127
|
+
maxPartCount,
|
|
128
|
+
concurrency,
|
|
129
|
+
maximumBufferedBytes: Math.min(
|
|
130
|
+
optionsArg.maxObjectSize,
|
|
131
|
+
partSize * concurrency,
|
|
132
|
+
) + Math.min(optionsArg.maxObjectSize, partSize),
|
|
133
|
+
};
|
|
134
|
+
};
|