@lossless.org/client 0.1.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/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
|
@@ -0,0 +1,1715 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { ExactUploadError } from './classes.exactuploaderror.js';
|
|
3
|
+
import type {
|
|
4
|
+
IExactUploadCapabilityProbeOptions,
|
|
5
|
+
IExactUploadResult,
|
|
6
|
+
IFastPutStreamExactOptions,
|
|
7
|
+
TExactUploadCapabilityProbeFailureReason,
|
|
8
|
+
TExactUploadCapabilityProbeResult,
|
|
9
|
+
TExactUploadFailurePhase,
|
|
10
|
+
TExactUploadMultipartCleanup,
|
|
11
|
+
} from './interfaces.js';
|
|
12
|
+
import {
|
|
13
|
+
beginExactUploadCapabilityOperation,
|
|
14
|
+
beginExactUploadProbeOperation,
|
|
15
|
+
mintExactUploadCapability,
|
|
16
|
+
type IExactUploadOperation,
|
|
17
|
+
} from './internal.exactupload.capability.js';
|
|
18
|
+
import { planMultipartUpload } from './internal.multipart.planner.js';
|
|
19
|
+
import {
|
|
20
|
+
consumeExactMultipartStream,
|
|
21
|
+
MultipartStreamConsumptionError,
|
|
22
|
+
} from './internal.multipart.stream.js';
|
|
23
|
+
|
|
24
|
+
interface IExactUploadBucketRef {
|
|
25
|
+
name: string;
|
|
26
|
+
smartbucketRef: {
|
|
27
|
+
readonly storageClient: plugins.s3.S3Client;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IProbeObject {
|
|
32
|
+
key: string;
|
|
33
|
+
versionId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IProbeMultipartUpload {
|
|
37
|
+
key: string;
|
|
38
|
+
uploadId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IIndependentCleanupResult {
|
|
42
|
+
errors: unknown[];
|
|
43
|
+
timedOut: boolean;
|
|
44
|
+
settlement: Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface IMultipartAbortResult {
|
|
48
|
+
cleanup: TExactUploadMultipartCleanup;
|
|
49
|
+
timedOut: boolean;
|
|
50
|
+
settlement: Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class ExactUploadProbeSemanticError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
public readonly reason: Extract<
|
|
56
|
+
TExactUploadCapabilityProbeFailureReason,
|
|
57
|
+
'conditional-put-not-enforced' | 'conditional-complete-not-enforced'
|
|
58
|
+
>,
|
|
59
|
+
) {
|
|
60
|
+
super(reason);
|
|
61
|
+
this.name = 'ExactUploadProbeSemanticError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cleanupTimeoutMs = 30_000;
|
|
66
|
+
const cleanupVerificationMaximumPages = 100;
|
|
67
|
+
const multipartAbortMaximumAttempts = 64;
|
|
68
|
+
const multipartAbortRetryDelayMs = 100;
|
|
69
|
+
const probeMetadata = {
|
|
70
|
+
'smartbucket-probe': 'exact-upload-v1',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const snapshotNativeObjectProperties = (
|
|
74
|
+
valueArg: IFastPutStreamExactOptions['nativeObjectProperties'],
|
|
75
|
+
): IFastPutStreamExactOptions['nativeObjectProperties'] => {
|
|
76
|
+
if (valueArg === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
valueArg === null
|
|
81
|
+
|| typeof valueArg !== 'object'
|
|
82
|
+
|| Array.isArray(valueArg)
|
|
83
|
+
) {
|
|
84
|
+
throw new TypeError('nativeObjectProperties must be an object.');
|
|
85
|
+
}
|
|
86
|
+
const keys = Object.keys(valueArg);
|
|
87
|
+
if (keys.some((keyArg) => keyArg !== 'contentType')) {
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
'nativeObjectProperties contains an unsupported property.',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
valueArg.contentType !== undefined
|
|
94
|
+
&& (
|
|
95
|
+
typeof valueArg.contentType !== 'string'
|
|
96
|
+
|| valueArg.contentType.length === 0
|
|
97
|
+
|| /[\0\r\n]/.test(valueArg.contentType)
|
|
98
|
+
)
|
|
99
|
+
) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
'nativeObjectProperties.contentType must be a non-empty HTTP header value.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
...(valueArg.contentType === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: { contentType: valueArg.contentType }),
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const detachedOperationSendSettlements = new WeakMap<
|
|
112
|
+
AbortSignal,
|
|
113
|
+
Set<Promise<void>>
|
|
114
|
+
>();
|
|
115
|
+
|
|
116
|
+
const trackDetachedOperationSend = (
|
|
117
|
+
signalArg: AbortSignal,
|
|
118
|
+
settlementArg: Promise<void>,
|
|
119
|
+
): void => {
|
|
120
|
+
let settlements = detachedOperationSendSettlements.get(signalArg);
|
|
121
|
+
if (!settlements) {
|
|
122
|
+
settlements = new Set();
|
|
123
|
+
detachedOperationSendSettlements.set(signalArg, settlements);
|
|
124
|
+
}
|
|
125
|
+
settlements.add(settlementArg);
|
|
126
|
+
void settlementArg.then(() => {
|
|
127
|
+
settlements?.delete(settlementArg);
|
|
128
|
+
if (settlements?.size === 0) {
|
|
129
|
+
detachedOperationSendSettlements.delete(signalArg);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const getDetachedOperationSendSettlement = (
|
|
135
|
+
signalArg: AbortSignal,
|
|
136
|
+
): Promise<void> | undefined => {
|
|
137
|
+
const settlements = detachedOperationSendSettlements.get(signalArg);
|
|
138
|
+
if (!settlements || settlements.size === 0) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return Promise.allSettled([...settlements]).then(() => undefined);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const finishOperationAfterOwnedSettlements = (
|
|
145
|
+
operationArg: IExactUploadOperation,
|
|
146
|
+
additionalSettlementsArg: Promise<void>[] = [],
|
|
147
|
+
): void => {
|
|
148
|
+
const detachedSendSettlement = getDetachedOperationSendSettlement(
|
|
149
|
+
operationArg.signal,
|
|
150
|
+
);
|
|
151
|
+
const ownedSettlements = detachedSendSettlement
|
|
152
|
+
? [detachedSendSettlement, ...additionalSettlementsArg]
|
|
153
|
+
: additionalSettlementsArg;
|
|
154
|
+
if (ownedSettlements.length === 0) {
|
|
155
|
+
operationArg.finish();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
void Promise.allSettled(ownedSettlements).then(() => {
|
|
159
|
+
operationArg.finish();
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const sendOperationCommand = async <T>(
|
|
164
|
+
operationArg: IExactUploadOperation,
|
|
165
|
+
sendArg: (signalArg: AbortSignal) => Promise<T>,
|
|
166
|
+
): Promise<T> => {
|
|
167
|
+
operationArg.assertCurrent();
|
|
168
|
+
const sendOutcome = Promise.resolve()
|
|
169
|
+
.then(() => {
|
|
170
|
+
operationArg.assertCurrent();
|
|
171
|
+
return sendArg(operationArg.signal);
|
|
172
|
+
})
|
|
173
|
+
.then<
|
|
174
|
+
{ status: 'fulfilled'; value: T },
|
|
175
|
+
{ status: 'rejected'; cause: unknown }
|
|
176
|
+
>(
|
|
177
|
+
(valueArg) => ({
|
|
178
|
+
status: 'fulfilled',
|
|
179
|
+
value: valueArg,
|
|
180
|
+
}),
|
|
181
|
+
(causeArg: unknown) => ({
|
|
182
|
+
status: 'rejected',
|
|
183
|
+
cause: causeArg,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
let abortListener: (() => void) | undefined;
|
|
187
|
+
const abortOutcome = new Promise<{ status: 'aborted' }>((resolveArg) => {
|
|
188
|
+
abortListener = () => {
|
|
189
|
+
resolveArg({
|
|
190
|
+
status: 'aborted',
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
operationArg.signal.addEventListener('abort', abortListener, {
|
|
194
|
+
once: true,
|
|
195
|
+
});
|
|
196
|
+
if (operationArg.signal.aborted) {
|
|
197
|
+
abortListener();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const outcome = await Promise.race([sendOutcome, abortOutcome]);
|
|
201
|
+
if (abortListener) {
|
|
202
|
+
operationArg.signal.removeEventListener('abort', abortListener);
|
|
203
|
+
}
|
|
204
|
+
if (outcome.status === 'aborted') {
|
|
205
|
+
trackDetachedOperationSend(
|
|
206
|
+
operationArg.signal,
|
|
207
|
+
sendOutcome.then(() => undefined),
|
|
208
|
+
);
|
|
209
|
+
operationArg.assertCurrent();
|
|
210
|
+
throw new Error('Unreachable exact-upload abort state.');
|
|
211
|
+
}
|
|
212
|
+
if (outcome.status === 'rejected') {
|
|
213
|
+
throw outcome.cause;
|
|
214
|
+
}
|
|
215
|
+
operationArg.assertCurrent();
|
|
216
|
+
return outcome.value;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const isHttpStatus = (errorArg: unknown, statusCodeArg: number): boolean => {
|
|
220
|
+
if (typeof errorArg !== 'object' || errorArg === null) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const metadata = (errorArg as {
|
|
224
|
+
$metadata?: {
|
|
225
|
+
httpStatusCode?: unknown;
|
|
226
|
+
};
|
|
227
|
+
}).$metadata;
|
|
228
|
+
return metadata?.httpStatusCode === statusCodeArg;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isDefinitiveConditionalWriteRejection = (
|
|
232
|
+
errorArg: unknown,
|
|
233
|
+
): boolean => {
|
|
234
|
+
return isHttpStatus(errorArg, 409) || isHttpStatus(errorArg, 412);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const requireNonEmptyString = (
|
|
238
|
+
valueArg: unknown,
|
|
239
|
+
fieldNameArg: string,
|
|
240
|
+
): string => {
|
|
241
|
+
if (typeof valueArg !== 'string' || valueArg.length === 0) {
|
|
242
|
+
throw new Error(`${fieldNameArg} was not returned by the object provider.`);
|
|
243
|
+
}
|
|
244
|
+
return valueArg;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const getOptionalNonEmptyString = (valueArg: unknown): string | undefined => {
|
|
248
|
+
return typeof valueArg === 'string' && valueArg.length > 0
|
|
249
|
+
? valueArg
|
|
250
|
+
: undefined;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const toWebReadableStream = (
|
|
254
|
+
readableStreamArg: plugins.stream.Readable | ReadableStream<Uint8Array>,
|
|
255
|
+
): ReadableStream<Uint8Array> => {
|
|
256
|
+
if (
|
|
257
|
+
typeof readableStreamArg === 'object' &&
|
|
258
|
+
readableStreamArg !== null &&
|
|
259
|
+
typeof (readableStreamArg as ReadableStream<Uint8Array>).getReader ===
|
|
260
|
+
'function'
|
|
261
|
+
) {
|
|
262
|
+
return readableStreamArg as ReadableStream<Uint8Array>;
|
|
263
|
+
}
|
|
264
|
+
if (readableStreamArg instanceof plugins.stream.Readable) {
|
|
265
|
+
return plugins.stream.Readable.toWeb(
|
|
266
|
+
readableStreamArg,
|
|
267
|
+
) as ReadableStream<Uint8Array>;
|
|
268
|
+
}
|
|
269
|
+
throw new TypeError(
|
|
270
|
+
'readableStream must be a Node.js Readable or a web ReadableStream.',
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const runIndependentCleanup = async (
|
|
275
|
+
cleanupTasksArg: Array<(signalArg: AbortSignal) => Promise<unknown>>,
|
|
276
|
+
operationArg: IExactUploadOperation,
|
|
277
|
+
): Promise<IIndependentCleanupResult> => {
|
|
278
|
+
const cleanupErrors: unknown[] = [];
|
|
279
|
+
if (cleanupTasksArg.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
errors: cleanupErrors,
|
|
282
|
+
timedOut: false,
|
|
283
|
+
settlement: Promise.resolve(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const cleanupAbortController = new AbortController();
|
|
287
|
+
const cleanupDeadlineToken = Symbol('exact-upload-cleanup-deadline');
|
|
288
|
+
let resolveCleanupDeadline!: (
|
|
289
|
+
valueArg: typeof cleanupDeadlineToken,
|
|
290
|
+
) => void;
|
|
291
|
+
const cleanupDeadlineOutcome = new Promise<typeof cleanupDeadlineToken>(
|
|
292
|
+
(resolveArg) => {
|
|
293
|
+
resolveCleanupDeadline = resolveArg;
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
const cleanupDeadline = setTimeout(() => {
|
|
297
|
+
const deadlineError = new Error(
|
|
298
|
+
`Exact-upload cleanup exceeded its ${cleanupTimeoutMs}ms deadline.`,
|
|
299
|
+
);
|
|
300
|
+
cleanupAbortController.abort(deadlineError);
|
|
301
|
+
resolveCleanupDeadline(cleanupDeadlineToken);
|
|
302
|
+
}, cleanupTimeoutMs);
|
|
303
|
+
let cleanupDeadlineDisposed = false;
|
|
304
|
+
const disposeCleanupDeadline = (): void => {
|
|
305
|
+
if (cleanupDeadlineDisposed) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
cleanupDeadlineDisposed = true;
|
|
309
|
+
clearTimeout(cleanupDeadline);
|
|
310
|
+
};
|
|
311
|
+
let releaseOwnedAbort: () => void;
|
|
312
|
+
try {
|
|
313
|
+
releaseOwnedAbort = operationArg.registerOwnedIndependentAbort(
|
|
314
|
+
cleanupAbortController,
|
|
315
|
+
disposeCleanupDeadline,
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
cleanupAbortController.abort(error);
|
|
319
|
+
disposeCleanupDeadline();
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
const cleanupTaskSequence = Promise.resolve().then(async () => {
|
|
323
|
+
for (const cleanupTask of cleanupTasksArg) {
|
|
324
|
+
if (cleanupAbortController.signal.aborted) {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
await cleanupTask(cleanupAbortController.signal);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
cleanupErrors.push(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const normalizedTaskSequence = cleanupTaskSequence.then<
|
|
335
|
+
{ status: 'settled' },
|
|
336
|
+
{ status: 'settled' }
|
|
337
|
+
>(
|
|
338
|
+
() => ({
|
|
339
|
+
status: 'settled',
|
|
340
|
+
}),
|
|
341
|
+
(causeArg: unknown) => {
|
|
342
|
+
cleanupErrors.push(causeArg);
|
|
343
|
+
return {
|
|
344
|
+
status: 'settled',
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
const settlement = normalizedTaskSequence.then(() => undefined);
|
|
349
|
+
void settlement.then(() => {
|
|
350
|
+
releaseOwnedAbort();
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
const outcome = await Promise.race([
|
|
354
|
+
normalizedTaskSequence,
|
|
355
|
+
cleanupDeadlineOutcome,
|
|
356
|
+
]);
|
|
357
|
+
if (outcome === cleanupDeadlineToken) {
|
|
358
|
+
cleanupErrors.push(
|
|
359
|
+
cleanupAbortController.signal.reason ??
|
|
360
|
+
new Error('Exact-upload cleanup exceeded its deadline.'),
|
|
361
|
+
);
|
|
362
|
+
return {
|
|
363
|
+
errors: cleanupErrors,
|
|
364
|
+
timedOut: true,
|
|
365
|
+
settlement,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
errors: cleanupErrors,
|
|
370
|
+
timedOut: false,
|
|
371
|
+
settlement,
|
|
372
|
+
};
|
|
373
|
+
} finally {
|
|
374
|
+
disposeCleanupDeadline();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const assertCleanupActive = (
|
|
379
|
+
signalArg: AbortSignal,
|
|
380
|
+
assertCurrentArg: () => void,
|
|
381
|
+
): void => {
|
|
382
|
+
if (signalArg.aborted) {
|
|
383
|
+
throw (
|
|
384
|
+
signalArg.reason ??
|
|
385
|
+
new Error('Exact-upload cleanup was aborted.')
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
assertCurrentArg();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const sendCleanupRequest = async <T>(
|
|
392
|
+
signalArg: AbortSignal,
|
|
393
|
+
assertCurrentArg: () => void,
|
|
394
|
+
sendArg: () => Promise<T>,
|
|
395
|
+
): Promise<T> => {
|
|
396
|
+
assertCleanupActive(signalArg, assertCurrentArg);
|
|
397
|
+
const output = await sendArg();
|
|
398
|
+
assertCleanupActive(signalArg, assertCurrentArg);
|
|
399
|
+
return output;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const listProbeMultipartUploads = async (
|
|
403
|
+
clientArg: plugins.s3.S3Client,
|
|
404
|
+
bucketNameArg: string,
|
|
405
|
+
probeRootArg: string,
|
|
406
|
+
signalArg: AbortSignal,
|
|
407
|
+
assertCurrentArg: () => void,
|
|
408
|
+
): Promise<IProbeMultipartUpload[]> => {
|
|
409
|
+
const uploads: IProbeMultipartUpload[] = [];
|
|
410
|
+
let keyMarker: string | undefined;
|
|
411
|
+
let uploadIdMarker: string | undefined;
|
|
412
|
+
for (
|
|
413
|
+
let pageNumber = 1;
|
|
414
|
+
pageNumber <= cleanupVerificationMaximumPages;
|
|
415
|
+
pageNumber++
|
|
416
|
+
) {
|
|
417
|
+
const output = await sendCleanupRequest(
|
|
418
|
+
signalArg,
|
|
419
|
+
assertCurrentArg,
|
|
420
|
+
() =>
|
|
421
|
+
clientArg.send(
|
|
422
|
+
new plugins.s3.ListMultipartUploadsCommand({
|
|
423
|
+
Bucket: bucketNameArg,
|
|
424
|
+
Prefix: probeRootArg,
|
|
425
|
+
KeyMarker: keyMarker,
|
|
426
|
+
UploadIdMarker: uploadIdMarker,
|
|
427
|
+
}),
|
|
428
|
+
{
|
|
429
|
+
abortSignal: signalArg,
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
for (const upload of output.Uploads ?? []) {
|
|
434
|
+
if (
|
|
435
|
+
typeof upload.Key !== 'string' ||
|
|
436
|
+
!upload.Key.startsWith(probeRootArg)
|
|
437
|
+
) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
'Multipart cleanup reconciliation returned an invalid prefixed entry.',
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
typeof upload.UploadId !== 'string' ||
|
|
444
|
+
upload.UploadId.length === 0
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Probe multipart upload '${upload.Key}' has no UploadId.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
uploads.push({
|
|
451
|
+
key: upload.Key,
|
|
452
|
+
uploadId: upload.UploadId,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!output.IsTruncated) {
|
|
456
|
+
return uploads;
|
|
457
|
+
}
|
|
458
|
+
if (
|
|
459
|
+
!output.NextKeyMarker ||
|
|
460
|
+
(
|
|
461
|
+
output.NextKeyMarker === keyMarker &&
|
|
462
|
+
output.NextUploadIdMarker === uploadIdMarker
|
|
463
|
+
)
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
'Multipart cleanup reconciliation returned a non-advancing page.',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
keyMarker = output.NextKeyMarker;
|
|
470
|
+
uploadIdMarker = output.NextUploadIdMarker;
|
|
471
|
+
}
|
|
472
|
+
throw new Error('Multipart cleanup reconciliation exceeded its page bound.');
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const listProbeObjectVersions = async (
|
|
476
|
+
clientArg: plugins.s3.S3Client,
|
|
477
|
+
bucketNameArg: string,
|
|
478
|
+
probeRootArg: string,
|
|
479
|
+
signalArg: AbortSignal,
|
|
480
|
+
assertCurrentArg: () => void,
|
|
481
|
+
): Promise<Array<Required<IProbeObject>>> => {
|
|
482
|
+
const versions: Array<Required<IProbeObject>> = [];
|
|
483
|
+
let keyMarker: string | undefined;
|
|
484
|
+
let versionIdMarker: string | undefined;
|
|
485
|
+
for (
|
|
486
|
+
let pageNumber = 1;
|
|
487
|
+
pageNumber <= cleanupVerificationMaximumPages;
|
|
488
|
+
pageNumber++
|
|
489
|
+
) {
|
|
490
|
+
const output = await sendCleanupRequest(
|
|
491
|
+
signalArg,
|
|
492
|
+
assertCurrentArg,
|
|
493
|
+
() =>
|
|
494
|
+
clientArg.send(
|
|
495
|
+
new plugins.s3.ListObjectVersionsCommand({
|
|
496
|
+
Bucket: bucketNameArg,
|
|
497
|
+
Prefix: probeRootArg,
|
|
498
|
+
KeyMarker: keyMarker,
|
|
499
|
+
VersionIdMarker: versionIdMarker,
|
|
500
|
+
}),
|
|
501
|
+
{
|
|
502
|
+
abortSignal: signalArg,
|
|
503
|
+
},
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
for (const version of [
|
|
507
|
+
...(output.Versions ?? []),
|
|
508
|
+
...(output.DeleteMarkers ?? []),
|
|
509
|
+
]) {
|
|
510
|
+
if (
|
|
511
|
+
typeof version.Key !== 'string' ||
|
|
512
|
+
!version.Key.startsWith(probeRootArg)
|
|
513
|
+
) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
'Version cleanup reconciliation returned an invalid prefixed entry.',
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (
|
|
519
|
+
typeof version.VersionId !== 'string' ||
|
|
520
|
+
version.VersionId.length === 0
|
|
521
|
+
) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Probe object version '${version.Key}' has no VersionId.`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
versions.push({
|
|
527
|
+
key: version.Key,
|
|
528
|
+
versionId: version.VersionId,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (!output.IsTruncated) {
|
|
532
|
+
return versions;
|
|
533
|
+
}
|
|
534
|
+
if (
|
|
535
|
+
!output.NextKeyMarker ||
|
|
536
|
+
(
|
|
537
|
+
output.NextKeyMarker === keyMarker &&
|
|
538
|
+
output.NextVersionIdMarker === versionIdMarker
|
|
539
|
+
)
|
|
540
|
+
) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
'Version cleanup reconciliation returned a non-advancing page.',
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
keyMarker = output.NextKeyMarker;
|
|
546
|
+
versionIdMarker = output.NextVersionIdMarker;
|
|
547
|
+
}
|
|
548
|
+
throw new Error('Version cleanup reconciliation exceeded its page bound.');
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const listProbeObjectKeys = async (
|
|
552
|
+
clientArg: plugins.s3.S3Client,
|
|
553
|
+
bucketNameArg: string,
|
|
554
|
+
probeRootArg: string,
|
|
555
|
+
signalArg: AbortSignal,
|
|
556
|
+
assertCurrentArg: () => void,
|
|
557
|
+
): Promise<string[]> => {
|
|
558
|
+
const keys: string[] = [];
|
|
559
|
+
let continuationToken: string | undefined;
|
|
560
|
+
for (
|
|
561
|
+
let pageNumber = 1;
|
|
562
|
+
pageNumber <= cleanupVerificationMaximumPages;
|
|
563
|
+
pageNumber++
|
|
564
|
+
) {
|
|
565
|
+
const output = await sendCleanupRequest(
|
|
566
|
+
signalArg,
|
|
567
|
+
assertCurrentArg,
|
|
568
|
+
() =>
|
|
569
|
+
clientArg.send(
|
|
570
|
+
new plugins.s3.ListObjectsV2Command({
|
|
571
|
+
Bucket: bucketNameArg,
|
|
572
|
+
Prefix: probeRootArg,
|
|
573
|
+
ContinuationToken: continuationToken,
|
|
574
|
+
}),
|
|
575
|
+
{
|
|
576
|
+
abortSignal: signalArg,
|
|
577
|
+
},
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
for (const object of output.Contents ?? []) {
|
|
581
|
+
if (
|
|
582
|
+
typeof object.Key !== 'string' ||
|
|
583
|
+
!object.Key.startsWith(probeRootArg)
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
'Object cleanup reconciliation returned an invalid prefixed entry.',
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
keys.push(object.Key);
|
|
590
|
+
}
|
|
591
|
+
if (!output.IsTruncated) {
|
|
592
|
+
return keys;
|
|
593
|
+
}
|
|
594
|
+
if (
|
|
595
|
+
!output.NextContinuationToken ||
|
|
596
|
+
output.NextContinuationToken === continuationToken
|
|
597
|
+
) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
'Object cleanup reconciliation returned a non-advancing page.',
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
continuationToken = output.NextContinuationToken;
|
|
603
|
+
}
|
|
604
|
+
throw new Error('Object cleanup reconciliation exceeded its page bound.');
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const getHttpStatus = (errorArg: unknown): number | undefined => {
|
|
608
|
+
if (typeof errorArg !== 'object' || errorArg === null) {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
const statusCode = (errorArg as {
|
|
612
|
+
$metadata?: {
|
|
613
|
+
httpStatusCode?: unknown;
|
|
614
|
+
};
|
|
615
|
+
}).$metadata?.httpStatusCode;
|
|
616
|
+
return typeof statusCode === 'number' ? statusCode : undefined;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const isRetryableCleanupError = (errorArg: unknown): boolean => {
|
|
620
|
+
const statusCode = getHttpStatus(errorArg);
|
|
621
|
+
return (
|
|
622
|
+
statusCode === undefined ||
|
|
623
|
+
statusCode === 409 ||
|
|
624
|
+
statusCode === 429 ||
|
|
625
|
+
statusCode >= 500
|
|
626
|
+
);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const waitForMultipartAbortRetry = (
|
|
630
|
+
signalArg: AbortSignal,
|
|
631
|
+
): Promise<void> => {
|
|
632
|
+
return new Promise<void>((resolveArg, rejectArg) => {
|
|
633
|
+
const retryTimer = setTimeout(() => {
|
|
634
|
+
signalArg.removeEventListener('abort', abortRetryWait);
|
|
635
|
+
resolveArg();
|
|
636
|
+
}, multipartAbortRetryDelayMs);
|
|
637
|
+
const abortRetryWait = (): void => {
|
|
638
|
+
clearTimeout(retryTimer);
|
|
639
|
+
rejectArg(
|
|
640
|
+
signalArg.reason ??
|
|
641
|
+
new Error('Multipart abort reconciliation was aborted.'),
|
|
642
|
+
);
|
|
643
|
+
};
|
|
644
|
+
signalArg.addEventListener('abort', abortRetryWait, { once: true });
|
|
645
|
+
if (signalArg.aborted) {
|
|
646
|
+
abortRetryWait();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const reconcileMultipartUploadAbort = async (
|
|
652
|
+
clientArg: plugins.s3.S3Client,
|
|
653
|
+
bucketNameArg: string,
|
|
654
|
+
keyArg: string,
|
|
655
|
+
uploadIdArg: string,
|
|
656
|
+
signalArg: AbortSignal,
|
|
657
|
+
assertCurrentArg: () => void,
|
|
658
|
+
): Promise<'aborted' | 'absent'> => {
|
|
659
|
+
const reconciliationErrors: unknown[] = [];
|
|
660
|
+
for (
|
|
661
|
+
let attempt = 1;
|
|
662
|
+
attempt <= multipartAbortMaximumAttempts;
|
|
663
|
+
attempt++
|
|
664
|
+
) {
|
|
665
|
+
let abortError: unknown;
|
|
666
|
+
let abortRequestSucceeded = false;
|
|
667
|
+
try {
|
|
668
|
+
await sendCleanupRequest(
|
|
669
|
+
signalArg,
|
|
670
|
+
assertCurrentArg,
|
|
671
|
+
() =>
|
|
672
|
+
clientArg.send(
|
|
673
|
+
new plugins.s3.AbortMultipartUploadCommand({
|
|
674
|
+
Bucket: bucketNameArg,
|
|
675
|
+
Key: keyArg,
|
|
676
|
+
UploadId: uploadIdArg,
|
|
677
|
+
}),
|
|
678
|
+
{
|
|
679
|
+
abortSignal: signalArg,
|
|
680
|
+
},
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
abortRequestSucceeded = true;
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (isHttpStatus(error, 404)) {
|
|
686
|
+
return 'absent';
|
|
687
|
+
}
|
|
688
|
+
abortError = error;
|
|
689
|
+
reconciliationErrors.push(error);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
await sendCleanupRequest(
|
|
694
|
+
signalArg,
|
|
695
|
+
assertCurrentArg,
|
|
696
|
+
() =>
|
|
697
|
+
clientArg.send(
|
|
698
|
+
new plugins.s3.ListPartsCommand({
|
|
699
|
+
Bucket: bucketNameArg,
|
|
700
|
+
Key: keyArg,
|
|
701
|
+
UploadId: uploadIdArg,
|
|
702
|
+
MaxParts: 1,
|
|
703
|
+
}),
|
|
704
|
+
{
|
|
705
|
+
abortSignal: signalArg,
|
|
706
|
+
},
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (isHttpStatus(error, 404)) {
|
|
711
|
+
return abortRequestSucceeded ? 'aborted' : 'absent';
|
|
712
|
+
}
|
|
713
|
+
reconciliationErrors.push(error);
|
|
714
|
+
if (!isRetryableCleanupError(error)) {
|
|
715
|
+
throw new AggregateError(
|
|
716
|
+
reconciliationErrors,
|
|
717
|
+
`Multipart upload '${uploadIdArg}' could not be verified as aborted.`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (abortError && !isRetryableCleanupError(abortError)) {
|
|
723
|
+
throw new AggregateError(
|
|
724
|
+
reconciliationErrors,
|
|
725
|
+
`Multipart upload '${uploadIdArg}' could not be aborted.`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (attempt === multipartAbortMaximumAttempts) {
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
if (attempt > 3) {
|
|
732
|
+
await waitForMultipartAbortRetry(signalArg);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
throw new AggregateError(
|
|
736
|
+
reconciliationErrors,
|
|
737
|
+
`Multipart upload '${uploadIdArg}' still exists after repeated aborts.`,
|
|
738
|
+
);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const createProbeCleanupTasks = (
|
|
742
|
+
clientArg: plugins.s3.S3Client,
|
|
743
|
+
bucketNameArg: string,
|
|
744
|
+
probeRootArg: string,
|
|
745
|
+
providerUsesVersionsArg: boolean,
|
|
746
|
+
multipartUploadsArg: IProbeMultipartUpload[],
|
|
747
|
+
objectsArg: IProbeObject[],
|
|
748
|
+
assertCurrentArg: () => void,
|
|
749
|
+
): Array<(signalArg: AbortSignal) => Promise<unknown>> => {
|
|
750
|
+
const cleanupTasks: Array<
|
|
751
|
+
(signalArg: AbortSignal) => Promise<unknown>
|
|
752
|
+
> = [];
|
|
753
|
+
for (const multipartUpload of multipartUploadsArg) {
|
|
754
|
+
cleanupTasks.push((signalArg) =>
|
|
755
|
+
reconcileMultipartUploadAbort(
|
|
756
|
+
clientArg,
|
|
757
|
+
bucketNameArg,
|
|
758
|
+
multipartUpload.key,
|
|
759
|
+
multipartUpload.uploadId,
|
|
760
|
+
signalArg,
|
|
761
|
+
assertCurrentArg,
|
|
762
|
+
),
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
for (const object of objectsArg) {
|
|
766
|
+
cleanupTasks.push((signalArg) =>
|
|
767
|
+
sendCleanupRequest(
|
|
768
|
+
signalArg,
|
|
769
|
+
assertCurrentArg,
|
|
770
|
+
() =>
|
|
771
|
+
clientArg.send(
|
|
772
|
+
new plugins.s3.DeleteObjectCommand({
|
|
773
|
+
Bucket: bucketNameArg,
|
|
774
|
+
Key: object.key,
|
|
775
|
+
VersionId: object.versionId,
|
|
776
|
+
}),
|
|
777
|
+
{
|
|
778
|
+
abortSignal: signalArg,
|
|
779
|
+
},
|
|
780
|
+
),
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
cleanupTasks.push(async (signalArg) => {
|
|
785
|
+
const discoveredUploads = await listProbeMultipartUploads(
|
|
786
|
+
clientArg,
|
|
787
|
+
bucketNameArg,
|
|
788
|
+
probeRootArg,
|
|
789
|
+
signalArg,
|
|
790
|
+
assertCurrentArg,
|
|
791
|
+
);
|
|
792
|
+
for (const upload of discoveredUploads) {
|
|
793
|
+
await reconcileMultipartUploadAbort(
|
|
794
|
+
clientArg,
|
|
795
|
+
bucketNameArg,
|
|
796
|
+
upload.key,
|
|
797
|
+
upload.uploadId,
|
|
798
|
+
signalArg,
|
|
799
|
+
assertCurrentArg,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const remainingUploads = await listProbeMultipartUploads(
|
|
803
|
+
clientArg,
|
|
804
|
+
bucketNameArg,
|
|
805
|
+
probeRootArg,
|
|
806
|
+
signalArg,
|
|
807
|
+
assertCurrentArg,
|
|
808
|
+
);
|
|
809
|
+
if (remainingUploads.length > 0) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`Probe multipart upload '${remainingUploads[0].uploadId}' remains after cleanup.`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
if (providerUsesVersionsArg) {
|
|
816
|
+
cleanupTasks.push(async (signalArg) => {
|
|
817
|
+
const discoveredVersions = await listProbeObjectVersions(
|
|
818
|
+
clientArg,
|
|
819
|
+
bucketNameArg,
|
|
820
|
+
probeRootArg,
|
|
821
|
+
signalArg,
|
|
822
|
+
assertCurrentArg,
|
|
823
|
+
);
|
|
824
|
+
for (const version of discoveredVersions) {
|
|
825
|
+
await sendCleanupRequest(
|
|
826
|
+
signalArg,
|
|
827
|
+
assertCurrentArg,
|
|
828
|
+
() =>
|
|
829
|
+
clientArg.send(
|
|
830
|
+
new plugins.s3.DeleteObjectCommand({
|
|
831
|
+
Bucket: bucketNameArg,
|
|
832
|
+
Key: version.key,
|
|
833
|
+
VersionId: version.versionId,
|
|
834
|
+
}),
|
|
835
|
+
{
|
|
836
|
+
abortSignal: signalArg,
|
|
837
|
+
},
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
const remainingVersions = await listProbeObjectVersions(
|
|
842
|
+
clientArg,
|
|
843
|
+
bucketNameArg,
|
|
844
|
+
probeRootArg,
|
|
845
|
+
signalArg,
|
|
846
|
+
assertCurrentArg,
|
|
847
|
+
);
|
|
848
|
+
if (remainingVersions.length > 0) {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`Probe object version '${remainingVersions[0].versionId}' remains after cleanup.`,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
} else {
|
|
855
|
+
cleanupTasks.push(async (signalArg) => {
|
|
856
|
+
const discoveredKeys = await listProbeObjectKeys(
|
|
857
|
+
clientArg,
|
|
858
|
+
bucketNameArg,
|
|
859
|
+
probeRootArg,
|
|
860
|
+
signalArg,
|
|
861
|
+
assertCurrentArg,
|
|
862
|
+
);
|
|
863
|
+
for (const key of discoveredKeys) {
|
|
864
|
+
await sendCleanupRequest(
|
|
865
|
+
signalArg,
|
|
866
|
+
assertCurrentArg,
|
|
867
|
+
() =>
|
|
868
|
+
clientArg.send(
|
|
869
|
+
new plugins.s3.DeleteObjectCommand({
|
|
870
|
+
Bucket: bucketNameArg,
|
|
871
|
+
Key: key,
|
|
872
|
+
}),
|
|
873
|
+
{
|
|
874
|
+
abortSignal: signalArg,
|
|
875
|
+
},
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
const remainingKeys = await listProbeObjectKeys(
|
|
880
|
+
clientArg,
|
|
881
|
+
bucketNameArg,
|
|
882
|
+
probeRootArg,
|
|
883
|
+
signalArg,
|
|
884
|
+
assertCurrentArg,
|
|
885
|
+
);
|
|
886
|
+
if (remainingKeys.length > 0) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Probe object '${remainingKeys[0]}' remains after cleanup.`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return cleanupTasks;
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
export const probeExactUploadCapability = async (
|
|
897
|
+
bucketRefArg: IExactUploadBucketRef,
|
|
898
|
+
optionsArg: IExactUploadCapabilityProbeOptions,
|
|
899
|
+
): Promise<TExactUploadCapabilityProbeResult> => {
|
|
900
|
+
let operation: IExactUploadOperation;
|
|
901
|
+
try {
|
|
902
|
+
operation = beginExactUploadProbeOperation(
|
|
903
|
+
bucketRefArg,
|
|
904
|
+
optionsArg.signal,
|
|
905
|
+
);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
return {
|
|
908
|
+
supported: false,
|
|
909
|
+
reason: 'unavailable',
|
|
910
|
+
cause: error,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const probeRoot = `.smartbucket-probes/exact-upload-v1/${plugins.smartunique.uuid4()}`;
|
|
915
|
+
const conditionalPutKey = `${probeRoot}/conditional-put`;
|
|
916
|
+
const conditionalCompleteKey = `${probeRoot}/conditional-complete`;
|
|
917
|
+
const unfinishedMultipartUploads: IProbeMultipartUpload[] = [];
|
|
918
|
+
const probeObjects: IProbeObject[] = [];
|
|
919
|
+
let providerUsesVersions = false;
|
|
920
|
+
let failureReason: TExactUploadCapabilityProbeFailureReason | undefined;
|
|
921
|
+
let failureCause: unknown;
|
|
922
|
+
|
|
923
|
+
const recordProbeObject = (
|
|
924
|
+
keyArg: string,
|
|
925
|
+
outputArg: {
|
|
926
|
+
VersionId?: string;
|
|
927
|
+
},
|
|
928
|
+
): void => {
|
|
929
|
+
const versionId = getOptionalNonEmptyString(outputArg.VersionId);
|
|
930
|
+
if (versionId) {
|
|
931
|
+
providerUsesVersions = true;
|
|
932
|
+
}
|
|
933
|
+
probeObjects.push({
|
|
934
|
+
key: keyArg,
|
|
935
|
+
versionId,
|
|
936
|
+
});
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const versioningOutput = await sendOperationCommand(
|
|
941
|
+
operation,
|
|
942
|
+
(signalArg) =>
|
|
943
|
+
operation.client.send(
|
|
944
|
+
new plugins.s3.GetBucketVersioningCommand({
|
|
945
|
+
Bucket: operation.bucketName,
|
|
946
|
+
}),
|
|
947
|
+
{
|
|
948
|
+
abortSignal: signalArg,
|
|
949
|
+
},
|
|
950
|
+
),
|
|
951
|
+
);
|
|
952
|
+
providerUsesVersions =
|
|
953
|
+
versioningOutput.Status === 'Enabled' ||
|
|
954
|
+
versioningOutput.Status === 'Suspended';
|
|
955
|
+
|
|
956
|
+
const firstPutOutput = await sendOperationCommand(
|
|
957
|
+
operation,
|
|
958
|
+
(signalArg) =>
|
|
959
|
+
operation.client.send(
|
|
960
|
+
new plugins.s3.PutObjectCommand({
|
|
961
|
+
Bucket: operation.bucketName,
|
|
962
|
+
Key: conditionalPutKey,
|
|
963
|
+
Body: new Uint8Array(0),
|
|
964
|
+
ContentLength: 0,
|
|
965
|
+
Metadata: probeMetadata,
|
|
966
|
+
IfNoneMatch: '*',
|
|
967
|
+
}),
|
|
968
|
+
{
|
|
969
|
+
abortSignal: signalArg,
|
|
970
|
+
},
|
|
971
|
+
),
|
|
972
|
+
);
|
|
973
|
+
recordProbeObject(conditionalPutKey, firstPutOutput);
|
|
974
|
+
requireNonEmptyString(firstPutOutput.ETag, 'PutObject ETag');
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const duplicatePutOutput = await sendOperationCommand(
|
|
978
|
+
operation,
|
|
979
|
+
(signalArg) =>
|
|
980
|
+
operation.client.send(
|
|
981
|
+
new plugins.s3.PutObjectCommand({
|
|
982
|
+
Bucket: operation.bucketName,
|
|
983
|
+
Key: conditionalPutKey,
|
|
984
|
+
Body: new Uint8Array(0),
|
|
985
|
+
ContentLength: 0,
|
|
986
|
+
Metadata: probeMetadata,
|
|
987
|
+
IfNoneMatch: '*',
|
|
988
|
+
}),
|
|
989
|
+
{
|
|
990
|
+
abortSignal: signalArg,
|
|
991
|
+
},
|
|
992
|
+
),
|
|
993
|
+
);
|
|
994
|
+
recordProbeObject(conditionalPutKey, duplicatePutOutput);
|
|
995
|
+
throw new ExactUploadProbeSemanticError(
|
|
996
|
+
'conditional-put-not-enforced',
|
|
997
|
+
);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (error instanceof ExactUploadProbeSemanticError) {
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
operation.assertCurrent();
|
|
1003
|
+
if (!isHttpStatus(error, 412)) {
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const createFirstOutput = await sendOperationCommand(
|
|
1009
|
+
operation,
|
|
1010
|
+
(signalArg) =>
|
|
1011
|
+
operation.client.send(
|
|
1012
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
1013
|
+
Bucket: operation.bucketName,
|
|
1014
|
+
Key: conditionalCompleteKey,
|
|
1015
|
+
Metadata: probeMetadata,
|
|
1016
|
+
}),
|
|
1017
|
+
{
|
|
1018
|
+
abortSignal: signalArg,
|
|
1019
|
+
},
|
|
1020
|
+
),
|
|
1021
|
+
);
|
|
1022
|
+
const firstUploadId = requireNonEmptyString(
|
|
1023
|
+
createFirstOutput.UploadId,
|
|
1024
|
+
'CreateMultipartUpload UploadId',
|
|
1025
|
+
);
|
|
1026
|
+
unfinishedMultipartUploads.push({
|
|
1027
|
+
key: conditionalCompleteKey,
|
|
1028
|
+
uploadId: firstUploadId,
|
|
1029
|
+
});
|
|
1030
|
+
const firstPartOutput = await sendOperationCommand(
|
|
1031
|
+
operation,
|
|
1032
|
+
(signalArg) =>
|
|
1033
|
+
operation.client.send(
|
|
1034
|
+
new plugins.s3.UploadPartCommand({
|
|
1035
|
+
Bucket: operation.bucketName,
|
|
1036
|
+
Key: conditionalCompleteKey,
|
|
1037
|
+
UploadId: firstUploadId,
|
|
1038
|
+
PartNumber: 1,
|
|
1039
|
+
Body: new Uint8Array([1]),
|
|
1040
|
+
ContentLength: 1,
|
|
1041
|
+
}),
|
|
1042
|
+
{
|
|
1043
|
+
abortSignal: signalArg,
|
|
1044
|
+
},
|
|
1045
|
+
),
|
|
1046
|
+
);
|
|
1047
|
+
const firstPartEtag = requireNonEmptyString(
|
|
1048
|
+
firstPartOutput.ETag,
|
|
1049
|
+
'UploadPart ETag',
|
|
1050
|
+
);
|
|
1051
|
+
const firstCompleteOutput = await sendOperationCommand(
|
|
1052
|
+
operation,
|
|
1053
|
+
(signalArg) =>
|
|
1054
|
+
operation.client.send(
|
|
1055
|
+
new plugins.s3.CompleteMultipartUploadCommand({
|
|
1056
|
+
Bucket: operation.bucketName,
|
|
1057
|
+
Key: conditionalCompleteKey,
|
|
1058
|
+
UploadId: firstUploadId,
|
|
1059
|
+
MultipartUpload: {
|
|
1060
|
+
Parts: [
|
|
1061
|
+
{
|
|
1062
|
+
PartNumber: 1,
|
|
1063
|
+
ETag: firstPartEtag,
|
|
1064
|
+
},
|
|
1065
|
+
],
|
|
1066
|
+
},
|
|
1067
|
+
IfNoneMatch: '*',
|
|
1068
|
+
MpuObjectSize: 1,
|
|
1069
|
+
}),
|
|
1070
|
+
{
|
|
1071
|
+
abortSignal: signalArg,
|
|
1072
|
+
},
|
|
1073
|
+
),
|
|
1074
|
+
);
|
|
1075
|
+
unfinishedMultipartUploads.splice(
|
|
1076
|
+
unfinishedMultipartUploads.findIndex(
|
|
1077
|
+
(uploadArg) => uploadArg.uploadId === firstUploadId,
|
|
1078
|
+
),
|
|
1079
|
+
1,
|
|
1080
|
+
);
|
|
1081
|
+
recordProbeObject(conditionalCompleteKey, firstCompleteOutput);
|
|
1082
|
+
requireNonEmptyString(
|
|
1083
|
+
firstCompleteOutput.ETag,
|
|
1084
|
+
'CompleteMultipartUpload ETag',
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
const createSecondOutput = await sendOperationCommand(
|
|
1088
|
+
operation,
|
|
1089
|
+
(signalArg) =>
|
|
1090
|
+
operation.client.send(
|
|
1091
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
1092
|
+
Bucket: operation.bucketName,
|
|
1093
|
+
Key: conditionalCompleteKey,
|
|
1094
|
+
Metadata: probeMetadata,
|
|
1095
|
+
}),
|
|
1096
|
+
{
|
|
1097
|
+
abortSignal: signalArg,
|
|
1098
|
+
},
|
|
1099
|
+
),
|
|
1100
|
+
);
|
|
1101
|
+
const secondUploadId = requireNonEmptyString(
|
|
1102
|
+
createSecondOutput.UploadId,
|
|
1103
|
+
'CreateMultipartUpload UploadId',
|
|
1104
|
+
);
|
|
1105
|
+
unfinishedMultipartUploads.push({
|
|
1106
|
+
key: conditionalCompleteKey,
|
|
1107
|
+
uploadId: secondUploadId,
|
|
1108
|
+
});
|
|
1109
|
+
const secondPartOutput = await sendOperationCommand(
|
|
1110
|
+
operation,
|
|
1111
|
+
(signalArg) =>
|
|
1112
|
+
operation.client.send(
|
|
1113
|
+
new plugins.s3.UploadPartCommand({
|
|
1114
|
+
Bucket: operation.bucketName,
|
|
1115
|
+
Key: conditionalCompleteKey,
|
|
1116
|
+
UploadId: secondUploadId,
|
|
1117
|
+
PartNumber: 1,
|
|
1118
|
+
Body: new Uint8Array([2]),
|
|
1119
|
+
ContentLength: 1,
|
|
1120
|
+
}),
|
|
1121
|
+
{
|
|
1122
|
+
abortSignal: signalArg,
|
|
1123
|
+
},
|
|
1124
|
+
),
|
|
1125
|
+
);
|
|
1126
|
+
const secondPartEtag = requireNonEmptyString(
|
|
1127
|
+
secondPartOutput.ETag,
|
|
1128
|
+
'UploadPart ETag',
|
|
1129
|
+
);
|
|
1130
|
+
try {
|
|
1131
|
+
const duplicateCompleteOutput = await sendOperationCommand(
|
|
1132
|
+
operation,
|
|
1133
|
+
(signalArg) =>
|
|
1134
|
+
operation.client.send(
|
|
1135
|
+
new plugins.s3.CompleteMultipartUploadCommand({
|
|
1136
|
+
Bucket: operation.bucketName,
|
|
1137
|
+
Key: conditionalCompleteKey,
|
|
1138
|
+
UploadId: secondUploadId,
|
|
1139
|
+
MultipartUpload: {
|
|
1140
|
+
Parts: [
|
|
1141
|
+
{
|
|
1142
|
+
PartNumber: 1,
|
|
1143
|
+
ETag: secondPartEtag,
|
|
1144
|
+
},
|
|
1145
|
+
],
|
|
1146
|
+
},
|
|
1147
|
+
IfNoneMatch: '*',
|
|
1148
|
+
MpuObjectSize: 1,
|
|
1149
|
+
}),
|
|
1150
|
+
{
|
|
1151
|
+
abortSignal: signalArg,
|
|
1152
|
+
},
|
|
1153
|
+
),
|
|
1154
|
+
);
|
|
1155
|
+
unfinishedMultipartUploads.splice(
|
|
1156
|
+
unfinishedMultipartUploads.findIndex(
|
|
1157
|
+
(uploadArg) => uploadArg.uploadId === secondUploadId,
|
|
1158
|
+
),
|
|
1159
|
+
1,
|
|
1160
|
+
);
|
|
1161
|
+
recordProbeObject(conditionalCompleteKey, duplicateCompleteOutput);
|
|
1162
|
+
throw new ExactUploadProbeSemanticError(
|
|
1163
|
+
'conditional-complete-not-enforced',
|
|
1164
|
+
);
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
if (error instanceof ExactUploadProbeSemanticError) {
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
operation.assertCurrent();
|
|
1170
|
+
if (!isHttpStatus(error, 412)) {
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (error instanceof ExactUploadProbeSemanticError) {
|
|
1176
|
+
failureReason = error.reason;
|
|
1177
|
+
} else if (operation.signal.aborted) {
|
|
1178
|
+
failureReason = 'aborted';
|
|
1179
|
+
failureCause = operation.signal.reason;
|
|
1180
|
+
} else {
|
|
1181
|
+
failureReason = 'unavailable';
|
|
1182
|
+
failureCause = error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const probeCleanupTasks = createProbeCleanupTasks(
|
|
1187
|
+
operation.client,
|
|
1188
|
+
operation.bucketName,
|
|
1189
|
+
probeRoot,
|
|
1190
|
+
providerUsesVersions,
|
|
1191
|
+
unfinishedMultipartUploads,
|
|
1192
|
+
probeObjects,
|
|
1193
|
+
() => operation.assertLifecycleCurrent(),
|
|
1194
|
+
);
|
|
1195
|
+
const primaryProviderSettlement =
|
|
1196
|
+
getDetachedOperationSendSettlement(operation.signal);
|
|
1197
|
+
if (primaryProviderSettlement) {
|
|
1198
|
+
const deferredCleanup = primaryProviderSettlement
|
|
1199
|
+
.then(async () => {
|
|
1200
|
+
try {
|
|
1201
|
+
operation.assertLifecycleCurrent();
|
|
1202
|
+
const cleanupResult = await runIndependentCleanup(
|
|
1203
|
+
probeCleanupTasks,
|
|
1204
|
+
operation,
|
|
1205
|
+
);
|
|
1206
|
+
await cleanupResult.settlement;
|
|
1207
|
+
} catch {
|
|
1208
|
+
// Lifecycle close makes deferred probe cleanup indeterminate.
|
|
1209
|
+
}
|
|
1210
|
+
})
|
|
1211
|
+
.then(
|
|
1212
|
+
() => undefined,
|
|
1213
|
+
() => undefined,
|
|
1214
|
+
);
|
|
1215
|
+
finishOperationAfterOwnedSettlements(operation, [deferredCleanup]);
|
|
1216
|
+
return {
|
|
1217
|
+
supported: false,
|
|
1218
|
+
reason: failureReason ?? 'aborted',
|
|
1219
|
+
cause: failureCause ?? operation.signal.reason,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
const cleanupResult = await runIndependentCleanup(
|
|
1223
|
+
probeCleanupTasks,
|
|
1224
|
+
operation,
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
if (cleanupResult.errors.length > 0) {
|
|
1229
|
+
return {
|
|
1230
|
+
supported: false,
|
|
1231
|
+
reason: 'cleanup-failed',
|
|
1232
|
+
cause: new AggregateError(
|
|
1233
|
+
cleanupResult.errors,
|
|
1234
|
+
'The exact-upload capability probe could not clean up safely.',
|
|
1235
|
+
),
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (failureReason) {
|
|
1239
|
+
return {
|
|
1240
|
+
supported: false,
|
|
1241
|
+
reason: failureReason,
|
|
1242
|
+
cause: failureCause,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
supported: true,
|
|
1247
|
+
capability: mintExactUploadCapability(bucketRefArg, operation),
|
|
1248
|
+
};
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
return {
|
|
1251
|
+
supported: false,
|
|
1252
|
+
reason: operation.signal.aborted ? 'aborted' : 'unavailable',
|
|
1253
|
+
cause: error,
|
|
1254
|
+
};
|
|
1255
|
+
} finally {
|
|
1256
|
+
if (cleanupResult.timedOut) {
|
|
1257
|
+
finishOperationAfterOwnedSettlements(operation, [
|
|
1258
|
+
cleanupResult.settlement,
|
|
1259
|
+
]);
|
|
1260
|
+
} else {
|
|
1261
|
+
finishOperationAfterOwnedSettlements(operation);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const abortMultipartUpload = async (
|
|
1267
|
+
operationArg: IExactUploadOperation,
|
|
1268
|
+
keyArg: string,
|
|
1269
|
+
uploadIdArg: string,
|
|
1270
|
+
): Promise<IMultipartAbortResult> => {
|
|
1271
|
+
let reconciliationResult: 'aborted' | 'absent' | undefined;
|
|
1272
|
+
const cleanupResult = await runIndependentCleanup(
|
|
1273
|
+
[
|
|
1274
|
+
async (signalArg) => {
|
|
1275
|
+
reconciliationResult = await reconcileMultipartUploadAbort(
|
|
1276
|
+
operationArg.client,
|
|
1277
|
+
operationArg.bucketName,
|
|
1278
|
+
keyArg,
|
|
1279
|
+
uploadIdArg,
|
|
1280
|
+
signalArg,
|
|
1281
|
+
() => operationArg.assertLifecycleCurrent(),
|
|
1282
|
+
);
|
|
1283
|
+
},
|
|
1284
|
+
],
|
|
1285
|
+
operationArg,
|
|
1286
|
+
);
|
|
1287
|
+
return {
|
|
1288
|
+
cleanup:
|
|
1289
|
+
cleanupResult.timedOut
|
|
1290
|
+
? {
|
|
1291
|
+
status: 'deferred',
|
|
1292
|
+
}
|
|
1293
|
+
: cleanupResult.errors.length === 0
|
|
1294
|
+
? {
|
|
1295
|
+
status: reconciliationResult ?? 'absent',
|
|
1296
|
+
}
|
|
1297
|
+
: {
|
|
1298
|
+
status: 'abort-failed',
|
|
1299
|
+
cause: new AggregateError(
|
|
1300
|
+
cleanupResult.errors,
|
|
1301
|
+
'The multipart upload could not be aborted safely.',
|
|
1302
|
+
),
|
|
1303
|
+
},
|
|
1304
|
+
timedOut: cleanupResult.timedOut,
|
|
1305
|
+
settlement: cleanupResult.settlement,
|
|
1306
|
+
};
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
export const fastPutStreamExact = async (
|
|
1310
|
+
bucketRefArg: IExactUploadBucketRef,
|
|
1311
|
+
optionsArg: IFastPutStreamExactOptions,
|
|
1312
|
+
): Promise<IExactUploadResult> => {
|
|
1313
|
+
if (typeof optionsArg.path !== 'string' || optionsArg.path.length === 0) {
|
|
1314
|
+
throw new TypeError('path must be a non-empty string.');
|
|
1315
|
+
}
|
|
1316
|
+
const nativeObjectProperties = snapshotNativeObjectProperties(
|
|
1317
|
+
optionsArg.nativeObjectProperties,
|
|
1318
|
+
);
|
|
1319
|
+
const plan = planMultipartUpload({
|
|
1320
|
+
objectSize: optionsArg.contentLength,
|
|
1321
|
+
requestedPartSize: optionsArg.requestedPartSize,
|
|
1322
|
+
requestedConcurrency: optionsArg.requestedConcurrency,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
let operation: IExactUploadOperation;
|
|
1326
|
+
try {
|
|
1327
|
+
operation = beginExactUploadCapabilityOperation(
|
|
1328
|
+
bucketRefArg,
|
|
1329
|
+
optionsArg.capability,
|
|
1330
|
+
optionsArg.signal,
|
|
1331
|
+
);
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
throw new ExactUploadError(
|
|
1334
|
+
{
|
|
1335
|
+
phase: 'capability',
|
|
1336
|
+
destinationPath: optionsArg.path,
|
|
1337
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1338
|
+
destinationPublication: 'not-published',
|
|
1339
|
+
multipartCleanup: {
|
|
1340
|
+
status: 'not-required',
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
error,
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let readableStream: ReadableStream<Uint8Array>;
|
|
1348
|
+
try {
|
|
1349
|
+
readableStream = toWebReadableStream(optionsArg.readableStream);
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
operation.finish();
|
|
1352
|
+
throw new ExactUploadError(
|
|
1353
|
+
{
|
|
1354
|
+
phase: 'source',
|
|
1355
|
+
destinationPath: optionsArg.path,
|
|
1356
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1357
|
+
destinationPublication: 'not-published',
|
|
1358
|
+
multipartCleanup: {
|
|
1359
|
+
status: 'not-required',
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
error,
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (plan.classification === 'zero') {
|
|
1367
|
+
let putRequestAttempted = false;
|
|
1368
|
+
let zeroFinishSettlement: Promise<void> | undefined;
|
|
1369
|
+
try {
|
|
1370
|
+
let consumptionResult;
|
|
1371
|
+
try {
|
|
1372
|
+
consumptionResult = await consumeExactMultipartStream({
|
|
1373
|
+
readableStream,
|
|
1374
|
+
plan,
|
|
1375
|
+
signal: operation.signal,
|
|
1376
|
+
failureSettlementTimeoutMs:
|
|
1377
|
+
optionsArg.failureSettlementTimeoutMs,
|
|
1378
|
+
async onPart() {
|
|
1379
|
+
throw new Error('A zero-byte upload cannot contain multipart parts.');
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (
|
|
1384
|
+
error instanceof MultipartStreamConsumptionError &&
|
|
1385
|
+
(
|
|
1386
|
+
error.state.cleanup.callbacks === 'detached' ||
|
|
1387
|
+
error.state.cleanup.reader.status === 'cancel-detached'
|
|
1388
|
+
)
|
|
1389
|
+
) {
|
|
1390
|
+
zeroFinishSettlement = error.ownedSettlement;
|
|
1391
|
+
}
|
|
1392
|
+
throw new ExactUploadError(
|
|
1393
|
+
{
|
|
1394
|
+
phase: 'source',
|
|
1395
|
+
destinationPath: optionsArg.path,
|
|
1396
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1397
|
+
destinationPublication: 'not-published',
|
|
1398
|
+
multipartCleanup: {
|
|
1399
|
+
status: 'not-required',
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
error,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
try {
|
|
1407
|
+
const putOutput = await sendOperationCommand(
|
|
1408
|
+
operation,
|
|
1409
|
+
(signalArg) => {
|
|
1410
|
+
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
|
+
},
|
|
1426
|
+
);
|
|
1427
|
+
return {
|
|
1428
|
+
destinationPath: optionsArg.path,
|
|
1429
|
+
contentLength: 0,
|
|
1430
|
+
etag: requireNonEmptyString(putOutput.ETag, 'PutObject ETag'),
|
|
1431
|
+
versionId: getOptionalNonEmptyString(putOutput.VersionId),
|
|
1432
|
+
partCount: 0,
|
|
1433
|
+
peakBufferedBytes: consumptionResult.peakBufferedBytes,
|
|
1434
|
+
maximumBufferedBytes: consumptionResult.maximumBufferedBytes,
|
|
1435
|
+
};
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
if (error instanceof ExactUploadError) {
|
|
1438
|
+
throw error;
|
|
1439
|
+
}
|
|
1440
|
+
throw new ExactUploadError(
|
|
1441
|
+
{
|
|
1442
|
+
phase: 'put-zero',
|
|
1443
|
+
destinationPath: optionsArg.path,
|
|
1444
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1445
|
+
destinationPublication:
|
|
1446
|
+
putRequestAttempted &&
|
|
1447
|
+
!isDefinitiveConditionalWriteRejection(error)
|
|
1448
|
+
? 'ambiguous'
|
|
1449
|
+
: 'not-published',
|
|
1450
|
+
multipartCleanup: {
|
|
1451
|
+
status: 'not-required',
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
error,
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
} finally {
|
|
1458
|
+
if (zeroFinishSettlement) {
|
|
1459
|
+
finishOperationAfterOwnedSettlements(operation, [
|
|
1460
|
+
zeroFinishSettlement,
|
|
1461
|
+
]);
|
|
1462
|
+
} else {
|
|
1463
|
+
finishOperationAfterOwnedSettlements(operation);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
let phase: TExactUploadFailurePhase = 'create-multipart';
|
|
1469
|
+
let uploadId: string | undefined;
|
|
1470
|
+
let createRequestAttempted = false;
|
|
1471
|
+
let completeRequestAttempted = false;
|
|
1472
|
+
let operationFinishSettlement: Promise<void> | undefined;
|
|
1473
|
+
let detachedStreamError: MultipartStreamConsumptionError | undefined;
|
|
1474
|
+
try {
|
|
1475
|
+
const createOutput = await sendOperationCommand(
|
|
1476
|
+
operation,
|
|
1477
|
+
(signalArg) => {
|
|
1478
|
+
createRequestAttempted = true;
|
|
1479
|
+
return operation.client.send(
|
|
1480
|
+
new plugins.s3.CreateMultipartUploadCommand({
|
|
1481
|
+
Bucket: operation.bucketName,
|
|
1482
|
+
Key: optionsArg.path,
|
|
1483
|
+
Metadata: optionsArg.nativeMetadata,
|
|
1484
|
+
ContentType: nativeObjectProperties?.contentType,
|
|
1485
|
+
}),
|
|
1486
|
+
{
|
|
1487
|
+
abortSignal: signalArg,
|
|
1488
|
+
},
|
|
1489
|
+
);
|
|
1490
|
+
},
|
|
1491
|
+
);
|
|
1492
|
+
uploadId = requireNonEmptyString(
|
|
1493
|
+
createOutput.UploadId,
|
|
1494
|
+
'CreateMultipartUpload UploadId',
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
phase = 'source';
|
|
1498
|
+
const completedParts: plugins.s3.CompletedPart[] = [];
|
|
1499
|
+
let consumptionResult;
|
|
1500
|
+
try {
|
|
1501
|
+
consumptionResult = await consumeExactMultipartStream({
|
|
1502
|
+
readableStream,
|
|
1503
|
+
plan,
|
|
1504
|
+
signal: operation.signal,
|
|
1505
|
+
failureSettlementTimeoutMs:
|
|
1506
|
+
optionsArg.failureSettlementTimeoutMs,
|
|
1507
|
+
async onPart(partArg) {
|
|
1508
|
+
operation.assertCurrent();
|
|
1509
|
+
const uploadPartOutput = await operation.client.send(
|
|
1510
|
+
new plugins.s3.UploadPartCommand({
|
|
1511
|
+
Bucket: operation.bucketName,
|
|
1512
|
+
Key: optionsArg.path,
|
|
1513
|
+
UploadId: uploadId!,
|
|
1514
|
+
PartNumber: partArg.partNumber,
|
|
1515
|
+
Body: partArg.data,
|
|
1516
|
+
ContentLength: partArg.data.byteLength,
|
|
1517
|
+
}),
|
|
1518
|
+
{
|
|
1519
|
+
abortSignal: partArg.signal,
|
|
1520
|
+
},
|
|
1521
|
+
);
|
|
1522
|
+
operation.assertCurrent();
|
|
1523
|
+
completedParts[partArg.partNumber - 1] = {
|
|
1524
|
+
PartNumber: partArg.partNumber,
|
|
1525
|
+
ETag: requireNonEmptyString(
|
|
1526
|
+
uploadPartOutput.ETag,
|
|
1527
|
+
'UploadPart ETag',
|
|
1528
|
+
),
|
|
1529
|
+
};
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
if (
|
|
1534
|
+
error instanceof MultipartStreamConsumptionError &&
|
|
1535
|
+
error.state.kind === 'part-callback'
|
|
1536
|
+
) {
|
|
1537
|
+
phase = 'upload-part';
|
|
1538
|
+
}
|
|
1539
|
+
if (
|
|
1540
|
+
error instanceof MultipartStreamConsumptionError &&
|
|
1541
|
+
(
|
|
1542
|
+
error.state.cleanup.callbacks === 'detached' ||
|
|
1543
|
+
error.state.cleanup.reader.status === 'cancel-detached'
|
|
1544
|
+
)
|
|
1545
|
+
) {
|
|
1546
|
+
detachedStreamError = error;
|
|
1547
|
+
}
|
|
1548
|
+
throw error;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
phase = 'complete-multipart';
|
|
1552
|
+
const completeOutput = await sendOperationCommand(
|
|
1553
|
+
operation,
|
|
1554
|
+
(signalArg) => {
|
|
1555
|
+
completeRequestAttempted = true;
|
|
1556
|
+
return operation.client.send(
|
|
1557
|
+
new plugins.s3.CompleteMultipartUploadCommand({
|
|
1558
|
+
Bucket: operation.bucketName,
|
|
1559
|
+
Key: optionsArg.path,
|
|
1560
|
+
UploadId: uploadId,
|
|
1561
|
+
MultipartUpload: {
|
|
1562
|
+
Parts: completedParts,
|
|
1563
|
+
},
|
|
1564
|
+
IfNoneMatch: '*',
|
|
1565
|
+
MpuObjectSize: optionsArg.contentLength,
|
|
1566
|
+
}),
|
|
1567
|
+
{
|
|
1568
|
+
abortSignal: signalArg,
|
|
1569
|
+
},
|
|
1570
|
+
);
|
|
1571
|
+
},
|
|
1572
|
+
);
|
|
1573
|
+
return {
|
|
1574
|
+
destinationPath: optionsArg.path,
|
|
1575
|
+
contentLength: optionsArg.contentLength,
|
|
1576
|
+
etag: requireNonEmptyString(
|
|
1577
|
+
completeOutput.ETag,
|
|
1578
|
+
'CompleteMultipartUpload ETag',
|
|
1579
|
+
),
|
|
1580
|
+
versionId: getOptionalNonEmptyString(completeOutput.VersionId),
|
|
1581
|
+
partCount: completedParts.length,
|
|
1582
|
+
peakBufferedBytes: consumptionResult.peakBufferedBytes,
|
|
1583
|
+
maximumBufferedBytes: consumptionResult.maximumBufferedBytes,
|
|
1584
|
+
};
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
let multipartCleanup: TExactUploadMultipartCleanup;
|
|
1587
|
+
const primaryProviderSettlement =
|
|
1588
|
+
getDetachedOperationSendSettlement(operation.signal);
|
|
1589
|
+
if (
|
|
1590
|
+
uploadId &&
|
|
1591
|
+
detachedStreamError?.state.cleanup.callbacks === 'detached'
|
|
1592
|
+
) {
|
|
1593
|
+
multipartCleanup = {
|
|
1594
|
+
status: 'deferred',
|
|
1595
|
+
};
|
|
1596
|
+
const callbackGatedCleanup = detachedStreamError.callbackSettlement
|
|
1597
|
+
.then(async () => {
|
|
1598
|
+
try {
|
|
1599
|
+
operation.assertLifecycleCurrent();
|
|
1600
|
+
const abortResult = await abortMultipartUpload(
|
|
1601
|
+
operation,
|
|
1602
|
+
optionsArg.path,
|
|
1603
|
+
uploadId!,
|
|
1604
|
+
);
|
|
1605
|
+
await abortResult.settlement;
|
|
1606
|
+
} catch {
|
|
1607
|
+
// Lifecycle close makes deferred cleanup indeterminate.
|
|
1608
|
+
}
|
|
1609
|
+
})
|
|
1610
|
+
.then(
|
|
1611
|
+
() => undefined,
|
|
1612
|
+
() => undefined,
|
|
1613
|
+
);
|
|
1614
|
+
operationFinishSettlement = Promise.allSettled([
|
|
1615
|
+
callbackGatedCleanup,
|
|
1616
|
+
detachedStreamError.ownedSettlement,
|
|
1617
|
+
]).then(() => undefined);
|
|
1618
|
+
} else if (uploadId && primaryProviderSettlement) {
|
|
1619
|
+
multipartCleanup = {
|
|
1620
|
+
status: 'deferred',
|
|
1621
|
+
};
|
|
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
|
+
operationFinishSettlement = Promise.allSettled([
|
|
1641
|
+
primaryProviderSettlement,
|
|
1642
|
+
providerGatedCleanup,
|
|
1643
|
+
]).then(() => undefined);
|
|
1644
|
+
} else if (uploadId) {
|
|
1645
|
+
const abortResult = await abortMultipartUpload(
|
|
1646
|
+
operation,
|
|
1647
|
+
optionsArg.path,
|
|
1648
|
+
uploadId,
|
|
1649
|
+
);
|
|
1650
|
+
multipartCleanup = abortResult.cleanup;
|
|
1651
|
+
if (abortResult.timedOut) {
|
|
1652
|
+
operationFinishSettlement = abortResult.settlement;
|
|
1653
|
+
}
|
|
1654
|
+
} else if (createRequestAttempted) {
|
|
1655
|
+
multipartCleanup = {
|
|
1656
|
+
...(primaryProviderSettlement
|
|
1657
|
+
? {
|
|
1658
|
+
status: 'deferred' as const,
|
|
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
|
+
}),
|
|
1666
|
+
};
|
|
1667
|
+
if (primaryProviderSettlement) {
|
|
1668
|
+
operationFinishSettlement = primaryProviderSettlement;
|
|
1669
|
+
}
|
|
1670
|
+
} else {
|
|
1671
|
+
multipartCleanup = {
|
|
1672
|
+
status: 'not-required',
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (
|
|
1676
|
+
detachedStreamError?.state.cleanup.reader.status ===
|
|
1677
|
+
'cancel-detached' &&
|
|
1678
|
+
detachedStreamError.state.cleanup.callbacks !== 'detached'
|
|
1679
|
+
) {
|
|
1680
|
+
const ownedSettlements = [
|
|
1681
|
+
detachedStreamError.ownedSettlement,
|
|
1682
|
+
];
|
|
1683
|
+
if (operationFinishSettlement) {
|
|
1684
|
+
ownedSettlements.push(operationFinishSettlement);
|
|
1685
|
+
}
|
|
1686
|
+
operationFinishSettlement = Promise.allSettled(
|
|
1687
|
+
ownedSettlements,
|
|
1688
|
+
).then(() => undefined);
|
|
1689
|
+
}
|
|
1690
|
+
throw new ExactUploadError(
|
|
1691
|
+
{
|
|
1692
|
+
phase,
|
|
1693
|
+
destinationPath: optionsArg.path,
|
|
1694
|
+
expectedContentLength: optionsArg.contentLength,
|
|
1695
|
+
destinationPublication:
|
|
1696
|
+
phase === 'complete-multipart' &&
|
|
1697
|
+
completeRequestAttempted &&
|
|
1698
|
+
!isDefinitiveConditionalWriteRejection(error)
|
|
1699
|
+
? 'ambiguous'
|
|
1700
|
+
: 'not-published',
|
|
1701
|
+
multipartUploadId: uploadId,
|
|
1702
|
+
multipartCleanup,
|
|
1703
|
+
},
|
|
1704
|
+
error,
|
|
1705
|
+
);
|
|
1706
|
+
} finally {
|
|
1707
|
+
if (operationFinishSettlement) {
|
|
1708
|
+
finishOperationAfterOwnedSettlements(operation, [
|
|
1709
|
+
operationFinishSettlement,
|
|
1710
|
+
]);
|
|
1711
|
+
} else {
|
|
1712
|
+
finishOperationAfterOwnedSettlements(operation);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
};
|